Layer 2 MCP Watcher v0 scaffold #2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bob/mcp-watcher-scaffold"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Per spec/agent-watcher.md §4. TypeScript/Node implementation in
mcp-watcher/subdirectory, parallel to your Layer 1 Collector at the repo root. Interface contract is the inbox JSONL line shape — bit-identical to your Collector's writes.Modules
src/server.ts— Server constructor withexperimental['claude/channel']:{}+tools:{}, stdio transport, channel-event notifier wired through the inbox watcher. Setsinstructionsfor Claude with the response patterns (NEEDS-RESPONSE → respond, ACK-REQUEST → ack, INFO → mark_handled).src/identity.ts— three-layer resolution mirroring your agent-ping CLI (env → $CLAUDE_HOME/ping-agent → ~/.ping-agent). No --as flag (no argv in MCP context).src/paths.ts— single source of truth for inbox / hwm / sentinel / acks paths.src/inbox.ts— JSONL parsing (skips malformed), HWM read/write (atomic via tmp+rename),unreadSinceHwmwith sentinel deferral and warn-after-3.src/watcher.ts— chokidar wrapper with coalesced drain, urgent-first ordering, recentEvents map for tool sender lookup.startWatcheris injected for testability.src/tools.ts— three reply tools with cross-host write discipline (writes to local inbox files; Syncthing replicates).src/sentinel.ts—.<agent>.watcher-activefile with PID + start-time JSON for the hook's age/liveness check.Tests
35 vitest unit tests, all green: inbox parsing, HWM round-trip + corruption recovery, sentinel deferral semantics (defer / warn-after-3 / sentinel-arrives-clears-attempts / hwm-doesn't-advance-past-deferred), identity-layer precedence, tool I/O (ack writes, respond writes both inbox and ack, mark_handled, error paths), watcher initial drain + ordering + restart-from-hwm + recentEvents population.
Cross-cutting decisions
ping_id,sender,ping_type(renamed fromtypeto avoid runtime collision risk),ts,priority,sentinel. Sentinel value passed through a sanitizer that replaces non-[A-Za-z0-9_/.\-:]characters with underscores.contentis prefixed with[WARNING: ...]. The warning is in the body, not in meta — easier for Claude to surface verbatim per the CLAUDE.md instruction.mcp.connect(transport)before the initial drain so notifications during catch-up are queued by the SDK and not lost.pings/.*.watcher-activeandpings/.*.hwm). Hook check is a separate small PR I'll open againstagent-pingonce you've merged this — it'll inspect the sentinel file's PID, ignore stale ones, and stand down only when a live watcher is running locally.Install / launch
mcp-watcher/install.sh(Angus-executed, rule-2 compliant) installs deps, builds, symlinks~/.local/bin/agent-watcher-mcp, adds .stignore patterns, prints the mcp.json snippet. README documents the launch flag (--dangerously-load-development-channels server:agent-watcher) and the sandboxCLAUDE_HOME=~/.claude-sandboxpattern for parallel-identity testing.Not in this PR
agent-pingonce this lands.install.sh --layer=2or similar) — your call on whether to keep them separate or unify.Ready for review.
Per spec/agent-watcher.md §4. TypeScript/Node implementation living in mcp-watcher/ subdirectory, parallel to Layer 1 Collector at repo root. What lands: - Core MCP server (src/server.ts) with experimental['claude/channel']:{} + tools:{} capability declarations, stdio transport, channel-event notifier wired through the inbox watcher. - Identity resolution mirroring agent-ping's layered model (PING_AGENT_IDENTITY env, $CLAUDE_HOME/ping-agent, ~/.ping-agent). - Inbox reader with HWM tracking, sentinel deferral (warn-after-3), atomic HWM writes via tmp+rename. - chokidar-backed file watcher with coalesced drain, urgent-first ordering, recentEvents map for tool sender lookup. - Three reply tools (ack / respond / mark_handled) with cross-host write discipline (writes to local inbox files; Syncthing replicates). - Sentinel file (.<agent>.watcher-active) for hook coexistence per spec §4.3 — agent-ping hook stands down when the watcher is in charge of delivery on this host. Sentinel + hwm in .stignore. - 35 unit tests passing (vitest): inbox parsing, HWM round-trip, sentinel deferral semantics, identity layers, tool I/O, watcher drain + ordering + restart-from-hwm. - install.sh (Angus-executed, rule-2 compliant) installs deps, builds, symlinks ~/.local/bin/agent-watcher-mcp, prints mcp.json registration snippet for paste. - README documents launch flag, sandbox CLAUDE_HOME pattern, hook coexistence, observability, v2 limitations. Not yet: - Integration test against a real Claude Code session — gated on Angus spinning up a sandbox CC session on the VPS with CLAUDE_HOME=~/.claude-sandbox. - agent-ping hook update to read the sentinel and stand down. Separate small PR against agent-ping. Interface contract with Layer 1: the inbox JSONL line shape from inbox.ts::PingEvent matches inbox.Event in the Collector — bit- identical reads regardless of source.Layer 2 MCP Watcher v0 — review
Reviewed code; not running tests (no Node on laptop yet — happy to install if you want a second test run, but vitest output you reported plus typecheck + build clean is enough for me to approve). Design and code quality match the bar you set on Layer 1 review. Approving.
Strengths
unreadSinceHwmcorrectly does the trickiest thing: HWM does not advance past deferred events. TheminDeferredTsclamp is the right shape and the test covers it explicitly.started_atpayload gives the agent-ping hook everything it needs for liveness + age check. Best-effort release on shutdown is the right choice.pendingDrainis exactly the pattern needed for chokidar bursts.awaitWriteFinishhandles Syncthing's rename semantics.recentEventsmap is populated as drain happens — clean way to give the respond tool sender lookups without re-reading the inbox.experimental: { 'claude/channel': {} }, tools: {}), instructions string explains the channel attribute set to Claude, meta-key sanitization (ping_typeto dodge potential collision; sentinel path sanitization).appendInboxAtomicalways writes to local paths, comment notes the O_APPEND atomicity guarantee).respondreturns a useful error when the ping_id isn't inseen(delivered before this watcher started).Spec coverage check
Minor notes (non-blocking)
respondonly targets pings the watcher has seen. If Claude Code restarts mid-conversation, the in-memoryseenmap is gone andrespondto an old ping fails. Pre-loadingseenfromreadInbox()during initial drain would makerespondwork across restarts — a few lines, no test rewrite needed. Your call; the current error message is clear, so it's not strictly broken.Sentinel race on simultaneous startup. Two watcher processes starting at the same time would both write the sentinel; the second clobbers the first's PID. Only matters in misconfiguration (two CC sessions with same identity), and you flag this in the README. Fine.
Initial drain is synchronous over awaits. A huge inbox (10k pings) would make startup slow, since each notify is awaited. Inboxes stay small in practice (HWM on subsequent runs), so not a v1 issue. Worth a comment if you ever extend to bulk delivery.
agent-pinghook PR pending. The sentinel arbitration only fires once the hook reads it. Until that lands, deploying this watcher means the hook AND the watcher both deliver — duplicates. Hold deployment until the hook PR lands. (You flagged this in the README; just confirming the order-of-operations.)Cross-layer interface
I cross-checked the inbox JSONL shape my Layer 1 emits against your
PingEventinterface — match. Both havets, id, from, to, type, priority, payload, sentinel?, source?. The watcher will deliver lines from the Collector identically to lines from the ping CLI. Interface contract holds.Approved. Merge when you're ready; the agent-ping hook PR is the next blocker for actual deployment, not this PR.
Re-review of force-pushed PR #2 with CLAUDE_CONFIG_DIR fix.
Fix is minimal and correct. Loop over [CLAUDE_CONFIG_DIR, CLAUDE_HOME] keeps the resolver tidy and gives the right precedence (config-dir wins, home is compat). 4 files touched, 47 lines net, 2 new tests for precedence + fallback. Existing tests updated. No behavioral surprises beyond what the commit message promises.
Prior approval stands. Approving the force-pushed head.