Metadata-Version: 2.5
Name: xdog-eyes
Version: 2.6.0
Summary: Declarative local observation and source-neutral agent event delivery
License-Expression: AGPL-3.0-or-later
License-File: LICENSE
Classifier: Operating System :: POSIX :: Linux
Requires-Python: >=3.12
Requires-Dist: agent-client-protocol<0.13,>=0.12.1; python_version < '3.15'
Requires-Dist: beautifulsoup4<5,>=4.12
Requires-Dist: croniter<7,>=2.0
Requires-Dist: defusedxml<1,>=0.7
Requires-Dist: httpx<1,>=0.27
Description-Content-Type: text/markdown

# xdog-eyes

> Platform scope: **Linux only**. Non-Linux adapters and native-platform support
> are outside the current product scope; historical audit evidence is not a support promise.

A local-first, declarative observation layer, shaped like flow: versioned JSON,
frozen models, whole-definition validation, async execution, CLI and a packaged
**eyes** skill. Sources collect evidence, detectors emit events, explicit
EventRouter dispatches explicit routes to extensible EventHandler implementations.
ACP is one built-in handler, not the routing abstraction. Eyes never imports a harness or runs an agent
loop; coding never imports a concrete Eyes source.

The supported management surfaces are the **Eyes CLI** and the **installed,
registered Eyes tool**. Python engine classes, handler registries and daemon/control modules are internal implementation details, not a
public embedding SDK or an alternative supported observation-management entry point.

Only the user-local shared daemon is hosted. An explicit `--directory` may isolate
a daemon, but no project/cwd runtime or embedded `ObserverService` is maintained.
Control metadata retains a `cwd` wire field for compatibility; it always names the
daemon directory, never a project, delivery target or filesystem permission.

## Use an explicit ACP destination

An independently managed receiver and an existing session ID are prerequisites.
Copy the packaged `source-test-review.json` into your project root and replace
its Unix endpoint and `EXPLICIT_SESSION_ID`. Roots resolve relative to that file.
The v2 example uses `delivery: "enqueue"` for **durable receiver intake** while
TUI/session owners are offline. An accepted/duplicate receipt proves only that
the receiver has accepted the original message into its persistent namespace;
it does **not** prove Agent execution, model completion, a reply, or delivery to
a user channel. It negotiates durable inbox support, never loads an owner or
starts a model. If the receiver is also offline, report unavailable—not receipt.
Eyes never launches a coding CLI to provide a callback.

```bash
xdog-eyes daemon start
xdog-eyes daemon status
xdog-eyes validate eyes.json
xdog-eyes start eyes.json
xdog-eyes list  # alias for status without an ID, scoped to owner
xdog-eyes status REGISTRATION_ID
xdog-eyes resume REGISTRATION_ID
xdog-eyes pause REGISTRATION_ID
xdog-eyes stop REGISTRATION_ID  # cancel source, not shared daemon
xdog-eyes daemon stop         # explicit whole-instance shutdown
```

`validate` needs no harness/model/active SDK. `start` explicitly opens the reusable
same-user daemon; handlers mode connects its receivers before scanning. The installed Eyes tool invokes the public CLI using the same Python environment.
Only the CLI uses the private, versioned, generation-fenced Unix control protocol; there is no public Python daemon SDK. The default
private directory is `$XDG_DATA_HOME/xdog/eyes` (otherwise
`~/.local/share/xdog/eyes`). Use `--directory /private/path` or
`XDOG_EYES_RUNTIME_DIR` for isolation; an explicit directory takes precedence.
Directories/sockets/journals are UID-owned and private. No systemd service is installed;
importing/listing skills does not start a daemon. There is no daemon idle timeout.

Owner/key plus normalized definition is idempotent across CLI processes/tools,
host retirement and daemon restart. Within one daemon instance, `owner` is the
management namespace, **not authentication, a filesystem permission boundary or
a destination constraint**; `--owner` defaults to `cli`. There is no management
`workspace` parameter or `--workspace` option. Changing the CLI current directory
does not select another inventory. Keys default to definition names; different
content requires a new key. Cancelled keys are tombstones, not silently reusable.
JSON handlers and routes alone choose delivery destinations; the CLI's cwd,
owner and currently calling application do not choose or restrict them.
Source/route/connection status is separate. Queued or
duplicate enqueue receipts are not model completion. Coding's TUI can show
pending evidence; an activated owner consumes it automatically. `/inbox list` and
`/inbox show ID` are read-only, `/inbox cancel ID` targets pending intake, and
`/inbox retry ID` explicitly retries interrupted work. There is no message approval
or human wake-up budget. Actual tool permissions remain unchanged.

`delivery: "prompt"` instead loads a live explicit session and waits for its final
ACP result. It requires an explicit handler `cwd` for standard ACP `load_session`.
A coding target persists live prompts through the same Inbox and
waits for their actual batch result. Inactive prompt targets still fail rather
than silently enqueueing offline. Generic ACP servers keep their own policy.
A lost prompt response is outcome-unknown and is never blindly resent. Enqueue
automatically reconnects and can retry the original ID/content after checking
the same persistent inbox namespace/project/branch; reconnect never approves
model execution.

### Connection faults are not user pauses

`paused` means an explicit `pause` command. An unavailable durable ACP receiver
keeps the registration `running` and reports `degraded`, with per-handler
`handler_health` diagnostics and `retry_after_seconds`. Eyes reconnects with
bounded exponential backoff (0.25–30 seconds) on its existing owned event loop.
A live observation continues sampling and retaining its original checkpoint and
bounded immutable outbox/backlog; it does not re-register or establish a new
baseline merely because a gateway restarted. Overflow remains explicit and
never drops evidence. Cold activation still waits for handlers before beginning
new observation/schedules; an offline receiver does not block the control plane
or other registrations.

Reconnect verifies the original handler binding. Durable enqueue ACK loss can
retry only the identical delivery/message ID and payload against that verified
Inbox namespace/branch/directory. Receiver deduplication handles an already
accepted message; retry never grants Agent execution permission. Safe in-flight
outboxes can survive daemon restart using their original IDs. Generic prompt
outcomes, malformed receipts, explicit receiver refusals, binding changes and
terminal/overflowed work remain `needs-review`, never blind side-effect replay.
Explicit user pause/cancel and daemon shutdown cancel/join the original reconnect
operations; a late completion cannot release that control or retarget delivery.

Registered handlers expose an explicit stable `HandlerSnapshot.binding`, separate
from diagnostic `details`. Its `retry_safe` flag defaults false and is declared
by actual trusted handler code, never inferred from JSON labels. Only the ACP
enqueue implementation declares durable same-identity retry safety. The generic
executor receives route-level retry safety and knows no ACP/Claw/Coding policy.

## Definition paths and ACP target directories

Eyes does not manage a workspace. Keep these distinct:

| Concern | Field or base |
| --- | --- |
| Registration inventory, controls and ACK cursor | Caller-chosen `owner` within the selected daemon instance |
| Tool definition file | Required absolute `definition_path` |
| CLI file definition paths | Relative source roots / handler `cwd` use the JSON file's directory |
| CLI stdin definition paths | Relative source roots / handler `cwd` use the CLI current directory |
| ACP target session directory | Handler `cwd`, under the delivery rules below |
| Stdio receiver process startup directory | Inherited from the Eyes process that spawns it, not handler `cwd` |

For `delivery: "enqueue"`, `session` identifies an **existing** receiver session.
Handler `cwd` may be omitted: the receiver resolves the session's directory. If
supplied, `cwd` is an assertion checked against that session, not a request to
move it. The client pins the returned directory, inbox namespace and branch for
later delivery/recovery; it never substitutes a definition base or daemon cwd.
For `delivery: "prompt"`, an explicit handler `cwd` is required for the standard
ACP `load_session` protocol. Neither mode creates a session from a directory.

A stdio connection has only `transport: "stdio"` and `command` (an argv array).
There is no `connection.cwd` field. Handler `cwd` is the target session directory,
**not** the subprocess startup directory; it is never passed as the stdio spawn
cwd. Use an absolute executable/script path when startup-directory independence
is needed. Unix `connection.path` remains an explicit absolute socket path.

## Router, handlers and destination batches

Internally, `HandlerRegistry` binds each type's definition parser and factory.
The internal `EventHandler` lifecycle is start, validate, deliver, snapshot, check,
resume and close; ACP payload/connection details stay in its handler. These engine
contracts are not public embedding or handler-registration SDK entry points.
Unknown types fail validation rather than importing arbitrary code from JSON.
The supported daemon handlers are built-in ACP and data-only `events`; the daemon
never imports code named by a definition.

Coding selects fixed batches with `max_messages_per_turn` (default10, strict
integer1..128). Five queued messages remain five independent history inputs with
source/evidence/IDs/attachments, not concatenated text or five separate Agent
turns. They share a response/tool stream; new arrivals wait for a later turn.
A canceled peer cannot withdraw its message from an already running shared batch
or cancel other members. Tools still require the target's normal permissions;
unattended restricted tools fail closed without explicit `allow-all`.

## HTTP and GitHub observations

The same daemon can observe external facts without a model call:

- `http`: bounded GET, `status` or JSON Pointer-selected values/record arrays;
  conditional JSON requests retain the committed baseline on 304.
- `github`: explicit `issues`, `issue_comments` or `workflow_runs` for one repository;
  bounded pagination, overlap/deduplication and workflow attempt identity.
- `record.change`: created/updated, optionally removed only for complete snapshots.
- `value.condition`: finite typed scalar predicates, per-record condition edges.
- `availability`: consecutive/duration failures and real recovery, including first abnormal policy.

Copy `http-announcements.json`, `http-service-health.json`, `github-feedback.json`
or `github-actions.json` from the packaged examples. Replace the source and actual
ACP endpoint/session, validate, then use the existing tool/CLI. No new host adapter
or source-specific model tool is needed. The first batch did not include content sources; HTML, Feed and Releases are
now covered below. PR review, browser, public webhook and shell remain unsupported; local pushed events are covered below.

Sources return candidate state; the original executor commits source cursor,
detector state and immutable pending delivery together before handler IO. Partial
pages, invalid selection, oversized responses and timeouts never imply empty data
or deletion. GitHub incremental windows never infer removals. After a long gap or
clock regression, collection resumes automatically within the configured lookback,
retaining detector history and known active runs, not silently rebaselining. Status
separates current acquisition errors from historical `coverage_warnings`:
`source_health.gap` remains true and `coverage_gap` retains bounded first/latest
interruption evidence plus a count, including after daemon restart. This is not
proof of complete backfill. Capacity and pagination limits remain explicit errors. First content is a baseline,
not a historical notification storm. Actions watches a bounded created window plus
known active runs, not every later rerun of an arbitrarily old completed run.

Credentials use an optional `bearer_env` reference in the daemon's environment,
never inline secrets or implicit `gh`/host login. Bearer requires HTTPS; redirects
cannot change origin, GitHub page/redirect paths stay inside the repository, and
known credential echoes/errors are redacted. HTTPX uses the daemon environment's
proxy/certificate settings. Rate limits/backoff survive daemon restart.

Status includes independent `source_health` and `source_errors`; `degraded` means
collection is still running but unhealthy, not paused. Recovery clears the source
error, never a separate unknown handler outcome. Intake ACK still does not prove
Agent execution or user-channel delivery. Existing file/scheduler registrations
remain compatible. An older daemon lacking `remote-sources-v1` requires explicit
stop/start before accepting external sources; it is never silently replaced.
See [external-source definitions, boundaries and verification](../../docs/eyes/sources.md).

## HTML, Feed and Releases

HTTP now supports static `mode: "html"` with CSS items/field selectors, text or
attribute extraction, stable key_field and explicit allow_empty (default false).
No match/ambiguous field is a collection failure, never an empty deletion snapshot.
Relative links use the final response URL and validated HTML base; scripts never run.

The `feed` source supports RSS 2.0 / Atom 1.0 with stable entry identity, conditional
requests and bounded window deduplication. DTD/entities/external resources are
forbidden. Window rollover never implies deletion; there is no permanent global ID
index or date-only cursor. GitHub `releases` reads bounded published releases,
excludes drafts, defaults to excluding prereleases, and never downloads assets.

Each `record.change` detector accepts finite `compare_fields` JSON Pointers,
independently of its source and other detectors; full latest evidence remains durable and
value.condition still sees all fields. Missing projection fields fail explicitly.
HTML/XML/JSON parsing uses settled worker IO under the original observation;
there is no second runtime, browser, script engine, public Python SDK or host adapter.

Packaged complete examples: `html-announcements.json`, `feed-updates.json`,
`github-releases.json`. New definitions require content-sources-v1 in the live
daemon; an older instance is never automatically replaced. Old normalized
first-batch definitions and checkpoints remain compatible. See
[content sources design and verification](../../docs/eyes/sources.md).

## Composable detectors and local pushed events

Sources acquire content, detectors detect changes/conditions, routes select
handlers and handlers deliver events. One acquisition feeds multiple independent
detectors. Composition validates data contracts, not source/detector product names.
`record.change`, `value.condition` and `event.received` work across file, HTTP,
Feed, GitHub, scheduler and event records. Specialized `file.change` / `scheduler.due`
need file/occurrence facts; availability needs real acquisition health. Streams and
partial windows cannot infer removal; incompatible combinations fail validation.

For file field comparisons, set source `format: "json"` (default: `"metadata"`).
Each no-follow read supplies both digest facts and parsed record content, keyed by
relative path, with an aggregate one MiB JSON bound. Configure compare_fields on
**detectors**, never sources. The unreleased source-level option has been removed,
not kept as a compatibility alias. Full latest evidence remains available.

`event` is a local pushed source on the same daemon Unix endpoint. Register
`examples/job-events.json`, then submit a finite JSON object:

```bash
xdog-eyes --owner my-project start /absolute/path/job-events.json
xdog-eyes --owner my-project publish \
  --observer OBSERVER_ID --source jobs \
  --event-id stable-producer-id --event-file /absolute/path/event.json
```

The `eyes` tool exposes `publish` with owner/observer_id/source_id/event_id/event_path
and invokes that same public CLI. Payloads cannot override JSON destinations.
Producer event IDs provide immutable idempotency; optional source key_pointer
selects a separate entity key for comparisons. Unchanged retries are duplicates;
conflicting IDs fail, including after daemon restart. Evidence input_id retains
producer identity. Input acceptance is separate from processing, handler delivery,
Agent execution and user notification.

Input retirement and detector/output state commit atomically. Output backpressure
stops further source consumption. Defaults per source: 64 KiB/event, 128 pending
inputs, 4 MiB pending bytes, 10000 lifetime accepted IDs. Full queues reject new
inputs; hashes are never evicted. At the lifetime bound create a new registration
explicitly. Pause rejects new IDs while retaining accepted inputs for resume;
cancel retains receipts without undoing effects. Duplicate receipts remain readable.
`status` includes event_intake accepted/pending/processed counts. Unknown downstream
effects retain the existing no-automatic-replay quarantine.

Requires observation-contracts-v1 / event-sources-v1; older live daemons must be
restarted explicitly, never automatically replaced. No new public client, service,
webhook, topic bus, host adapter or Agent execution loop is introduced. See
[contracts, durability and acceptance](../../docs/eyes/observation-contracts.md).

## Scheduler sources

Sources can also generate timed observations: one-off `at`, fixed
`every_seconds`, or five-field `cron` with an explicit IANA `timezone`.
Exactly one mode is allowed per source. `scheduler.due` detectors emit ordinary
`scheduler.due` events through the same EventRouter/handlers; no agent or system
cron is called directly. File and scheduler sources can coexist in one v2 file.

```json
{"id": "daily", "type": "scheduler", "schedule": {
  "cron": "0 9 * * *", "timezone": "Asia/Shanghai"
}}
```

The packaged `scheduled-review.json` is a complete executable definition. Replace
its endpoint/session and use the same validate/start/status/stop commands.
Repeating schedules start after handler readiness; one-off start refuses past
zoned timestamps. Intervals use monotonic time (finite seconds, minimum0.001),
cron uses timezone-aware calendar instants, and suspended periods do not generate
a catch-up burst. A late active poll coalesces to its latest occurrence. Scheduled
evidence keeps occurrence identity and planned/observed UTC times, not file paths.
See [scheduler definition, timing and recovery](../../docs/eyes/sources.md)
for all modes, DST/clock semantics, mixed-source examples and bounds.

## Collection and lifetime

Regular-file content changes use bounded polling, fingerprints
and net-change/debounce detection. No symlink following, mtime-only/unchanged-save
events or historical file contents. Rename is delete+create; polling can miss
transient changes. Keep roots/globs narrow and exclude generated/cache trees.
Payloads, source labels and filenames are untrusted evidence, never permissions.

The daemon journals registrations, file fingerprints/debounce state, scheduler
cursors, immutable pending deliveries, event rows and consumer ACK cursors in
`daemon.sqlite3`. Cursor and pending evidence commit together before handler send.
Accepted registrations with `recover=True` resume from their original IDs and
baselines, not a fresh scan that discards downtime changes. No historical file
contents are retained. Registration pause skips scheduled deadlines rather than
replaying missed ticks; accepted pending evidence remains intact. Cancel is
idempotent and does not delete already committed events or destination inputs.

Non-idempotent in-flight/unknown delivery after interruption is quarantined for review,
never automatically replayed. The original ACP definition and pinned inbox/branch
or owner-generation binding must still match on recovery. Proven-not-sent delivery
may retry; a changed receiver cannot acquire an old outbox. Events-mode publication
is idempotent by original delivery ID and may be recovered safely. Whole-instance
`daemon stop` settles work and releases only its own socket/record, retaining data.
`daemon status` is read-only and never starts an absent instance.

## CLI event consumers (including Goal)

Goal uses a **real Eyes CLI subprocess** for every registration, event read,
ACK and pause/resume/cancel operation. It imports no Eyes module and does not
construct a Python control object. A Python host invokes the installed command
with an argv array, e.g. `[sys.executable, "-m", "xdog.eyes", ...]`, never a shell
command. `xdog.eyes._daemon_control.DaemonControl` is private implementation for
Eyes' CLI internals only, not a supported SDK or host integration
entry point. The former `xdog.eyes.client`, `Client` and `EyesClient` are removed;
there is deliberately no compatibility export.

```bash
# Each command uses the same explicit scope; global options also work after commands.
xdog-eyes --directory /private/eyes --owner goal:stable \
  start - --delivery events --key tasks --no-wait < tasks.json
xdog-eyes --directory /private/eyes --owner goal:stable \
  status tasks --wait-ready --timeout 10
xdog-eyes --directory /private/eyes --owner goal:stable \
  events --follow --after 0 --limit 64
# After Goal runtime has durably handled the original event:
xdog-eyes --directory /private/eyes --owner goal:stable ack SEQUENCE
xdog-eyes --directory /private/eyes --owner goal:stable pause tasks
```

Use a definition handler `{"id": "changed", "type": "events"}` with its explicit
route and `--delivery events`. Events mode **never creates an ACP handler or
wakes an Agent**. Goal receives raw event JSON through CLI stdout, handles it
inside Goal runtime and decides any later reminder under host policy. Claw does
**not** invoke this CLI itself: it loads the installed Eyes registered tool;
the registered tool itself invokes the public CLI, keeping daemon-control details inside Eyes.

### Machine-readable command contract

| Command | stdout on success |
| --- | --- |
| `start FILE` or `start -` | One registration JSON object, including `registration_id`, `key`, `state`, `duplicate` |
| `status ID`, including `--wait-ready --timeout SECONDS` | One status object for the original registration |
| `status`, `list` | One scoped `{"registrations": [...]}` inventory object |
| `events --after N --limit 64` | One `{"events": [...]}` object, including `{"events": []}` for an empty batch |
| `events --follow --after N --limit 64` | JSONL, one event object per line, no wrapper, heartbeat or empty batch |
| `ack SEQUENCE` | One `{"acknowledged": SEQUENCE}` object |
| `pause ID`, `resume ID`, `cancel ID` / `stop ID` | One registration status object |
| `daemon start/status/stop` | One daemon lifecycle/status object |

Each event object has `sequence`, `key`, `registration_id` and the full original
Delivery JSON in `delivery`. `--limit` is a per-batch integer 1..128, not a total
stream limit. An omitted `--after` starts after this owner's persistent
ACK cursor; explicit `--after 0` re-reads retained history. Follow advances only
its local read cursor and never automatically ACKs. ACK is cumulative within the
scope and idempotent; persist consumer effects before ACK and deduplicate original
delivery IDs across the effect/ACK crash window. Unacknowledged events provide
backpressure. No Python callable crosses this boundary.

`start -` reads one UTF-8 JSON object from stdin until EOF, bounded to **1,044,480
bytes**; duplicate keys, non-finite values, multiple documents and oversized input
are rejected. For `start -`, relative source roots and any explicit relative
handler `cwd` resolve against the **CLI process current directory**, not the shell
redirection file's directory or daemon startup directory. For `start FILE` (and
`validate FILE`), they resolve against the **definition file's parent directory**.
Use absolute roots when a stdin producer cannot guarantee the CLI current directory.
`--no-wait` returns the registration receipt without waiting for a baseline; it does not resume an explicitly paused
registration. Resume explicitly, then use `status ID --wait-ready --timeout 10`.
The wait is read-only and never registers or resumes anything. Optional
`--source-checkpoints-json JSON_OBJECT` supplies definition-bound scheduler cursor
mapping for already accepted source recovery, not executable code or a clock.

Commands exit 0 on success; Ctrl-C exits 130. Errors exit 2 with a bounded diagnostic only on stderr.
A timed-out wait emits no success JSON. JSON output frames are bounded to one MiB;
follow writes with pipe backpressure, not an unbounded output queue. Closing stdin
of a follow process is harmless; closing stdout, terminating it or reaching a
broken pipe only detaches the consumer. It does not ACK, pause/cancel observation,
stop the shared daemon or roll back an accepted registration. A lost mutation
response remains uncertain: inspect/reconcile its original scope/key/ID rather
than automatically retrying a different mutation.

Follow reconnects **read-only** after socket EOF, disconnection or generation
change. It waits for an absent daemon to be explicitly restarted; it never starts
one, re-registers, resumes or ACKs during reconnection. Nonempty malformed protocol
frames remain errors, not a reason to retry indefinitely. Host retirement closes
its stream only; Goal pause explicitly runs `pause`, and Goal completion/clear can
explicitly run `cancel`. Cancelled/unknown work is not blindly replayed. Scope
strings isolate accidental cross-owner access within a UID, not malicious code
with that UID's authority. Event history has no automatic retention/compaction.

The old event-handler host/capability/descriptor protocol is removed. Core local
permissions are not an OS sandbox. No TCP/HTTP/Windows transport, installed system
daemon, hosted broker, automatic coding startup, API/web source, script handler,
or harness-specific handler is added to Eyes. Claw can expose its own ACP backend
through its gateway, without an Eyes dependency or a special router path; see
[Claw/Eyes ACP](../../docs/claw/acp.md) for `group:<id>` addressing and setup.
Stdio is an optional generic ACP transport; Unix is the XDOG-local transport for
coding and Claw callbacks.

## Model-free checks without daemon access

From the repository root after `uv sync`, the following only parse help or
validate saved JSON. They do not start/connect to a daemon, contact ACP targets
or call a model:

```bash
uv run xdog-eyes --help
uv run xdog-eyes start --help
uv run xdog-eyes daemon start --help
uv run xdog-eyes validate packages/eyes/src/xdog/eyes/examples/source-test-review.json
uv run xdog-eyes validate packages/eyes/src/xdog/eyes/examples/scheduled-review.json
```

The lifecycle examples elsewhere in this README are **real management
operations**, not validation commands. Use an explicit private `--directory` for
an intentionally isolated live test; do not stop or change a user's shared
instance as part of documentation verification.

## Installation and development

The runtime dependencies are the official `agent-client-protocol` SDK, pinned
`>=0.12.1,<0.13` for Python 3.12–3.14, and `croniter>=2.0,<7` for bounded cron
validation/calendar calculations. Eyes does not depend on ai/agent/coding/claw/flow.
The marker excludes the currently unsupported ACP SDK on Python 3.15+; definition
validation remains usable. Workspace development commands run from repo root.

See [ACP/session/inbox protocol](../../docs/protocols/acp.md) for explicit
receiver startup, all commands, permission/unknown-result semantics and limits,
and [dated batch acceptance](../../docs/archive/engineering/unified-inbox-batch-verification.md) for
release evidence. The older `eyes-verification.md` is a historical checkpoint.
`uv build --package xdog-eyes` ships skill, example, py.typed and license. This
feature does not push/publish or provision a PyPI publisher/system service.


## Installed model tool

Installed metadata registers `xdog.eyes.capability:provider`. Discovery/import is
inert. Opening the tool binding only ensures the shared daemon is running; it
does not open a host journal, subscribe to application events, capture replies,
recover host observers or start a model. There is no ManagedObservers runtime.

Choose a stable owner, save a full Eyes v2 JSON, then call
`eyes(action="create", owner="my-watches", definition_path="/project/eyes.json")`.
`definition_path` must be an **absolute path** to an already saved regular,
non-symlink JSON file (maximum one MiB); relative paths are not accepted. Source
roots and any explicit relative handler `cwd` resolve beside that JSON file.
The caller owns the naming scheme; Eyes never derives owner from a host/session/group.
JSON handlers/routes select all destinations, including other endpoints/sessions.
The current host address is convenience metadata, not an allowlist. Normal
receiver authentication and protocol checks remain; the tool does not restrict
targets to the current host/group.

### Tool parameters

| Action | Required parameters in addition to `action` |
| --- | --- |
| `create` | `owner`, absolute `definition_path` |
| `list` | `owner` |
| `status`, `pause`, `resume`, `cancel` | `owner`, `observer_id` (the returned `registration_id`) |

Do not send unrelated fields or null placeholders. The tool has no `cwd`,
`workspace`, destination override or `directory` parameter; explicit daemon
isolation is selected by the CLI/environment, not an application owner string.
Create invokes public CLI `start --delivery auto --no-wait`: success is a
registration receipt, not proof of baseline readiness, target intake or execution.
Use status to inspect observation/handler state. Accepted normalized definitions
are snapshots; editing the source JSON later does not update a registration.

The registered tool returns a CLI envelope `{exit_code, stdout, stderr}`. The
CLI's JSON result remains a string in stdout; check exit_code before decoding it.
Nonzero exits preserve their diagnostics; uncertain or cancelled mutations are
not automatically retried.

The daemon executes handlers directly, like CLI `start`. Tool status is source
and handler-delivery state only. There is no model execution/report watcher or
chat notification policy. Supported actions: create/list/status/pause/
resume/cancel. `cancel_notifications` is removed. Tool/CLI controls use the same
registration IDs and owner namespace; closing a binding only detaches.
Old managed host journals are left untouched, not imported or migrated.

The provider remains application-scoped. Coding and Claw both load that scope
through normal installed-capability discovery; no Eyes-specific host adapter or
enable switch is required. The package must already be installed. All model tool
actions require caller-supplied owner; there is no context action. The
shared skill contains no application-specific naming or routing policy.


### Capability contributions and readiness

`skill_names=("eyes",)` advertises a packaged skill, not a default body or tool
permission. SkillRepo discovers it and explicit activation loads its body;
Flow follows the same policy. Capabilities can contribute skills, tools, runtime
resources or extension hooks without supplying all four. Startup and close belong
to their real scope owner; no capability creates a second Agent execution loop.

The daemon control endpoint becomes available before slow handler recovery. Its
live protocol/features are negotiated separately from the on-disk journal version.
An incompatible process/state is reported rather than automatically replaced,
migrated or replayed. Closing/cancelling a model tool invocation settles its owned
CLI process and IO; it does not stop the shared daemon, retract an accepted
registration or blindly retry a request whose outcome is uncertain.
