interlink-mcp 0.4.1

Cryptographically-authenticated, cross-machine agent-to-agent chat for Claude Code, over MCP channels
Documentation
# Auto-progress: a debounced nudge hook (design)

> Status: **implementing** (0.4.1). Additive and **wire-compatible with 0.4.0**> no protocol/domain change; a `PostToolUse` hook plus a local state file.

## Why

0.4.0 makes progress updates *structured* and *more likely*, but they're still
model-driven: a heads-down agent can go silent mid-task. We want a reliable floor
without spam. A hook that *sends* updates would be spammy and dumb (a shell script
can't judge what's notable). So the hook **nudges**, and the model **sends**:

- **Cadence** is the hook's job (deterministic, reliable).
- **Content** is the model's job (it writes "deps in, restarting ComfyUI").

## Two layers, one shared timer

- **Prose (primary, unchanged):** the SKILL / server instructions say "update at
  each milestone." Well-timed, semantic — but unreliable.
- **Hook (backstop floor):** fires only when an active task has gone quiet longer
  than an interval. It injects a reminder; the model composes and sends.

They don't double up because **any outgoing update resets the timer** — model- or
hook-prompted. A well-behaved agent updating at milestones keeps resetting the
clock, so the hook never trips; the hook only speaks in the *gaps*. Well-behaved →
hook silent. Silent agent → guaranteed heartbeat.

## Mechanism

Three small pieces, all in the state dir (`~/.local/state/interlink/`):

1. **Current-task marker** (`current-task.json` = `{ task_id, peer, since }`),
   written/cleared by the MCP server so the hook knows whether a task is running
   and who to update. The rule for "I am the executor of this task":
   - An **inbound** message with a `task_id` and **no `status`** is an *opening
     request* → I'm now executing it → write the marker `(task_id, sender)`.
     (Progress/result messages carry a `status`, so they don't (re)arm it — that's
     what distinguishes "someone asked me" from "someone's reporting to me.")
   - Cleared when I send a **terminal** status (`result`/`failed`) for that
     `task_id`, or receive a `canceled` for it.
2. **Last-update timestamp** (`last-update`), reset by the MCP whenever it enqueues
   an **outbound** message with `status = update` (or a terminal) for the active
   task. This is the shared timer.
3. **The `PostToolUse` hook** (bash, shipped in `plugin/hooks/`), which on each
   tool event:
   - no marker → exit silently (idle / non-collaboration sessions never fire);
   - marker present **and** `now − last_update > INTERVAL` **and**
     `now − last_nudge > INTERVAL` → emit a reminder to the model
     ("You're executing task '<id>' for <peer> and haven't updated in >Ns — send a
     brief `send_message(status:'update', task_id:'<id>')` on what you just did"),
     and stamp `last-nudge`;
   - else → exit silently.
   - Optional tool gate: only count `Bash`/`Edit`/`Write` events toward the timer,
     so a flurry of `Read`s doesn't trip it.

## Config

`INTERLINK_PROGRESS_INTERVAL` seconds (default **60**); `0` disables the hook. The
operator dials the chattiness.

## What ships (0.4.1)

- **MCP** (`interlink-mcp`): write/clear the marker on the inbound gate; reset
  `last-update` on outbound `update`/terminal. ~30 lines, no wire change.
- **Plugin**: a `hooks/hooks.json` + `scripts/progress-nudge.sh`, wired via
  `${CLAUDE_PLUGIN_ROOT}` (this re-introduces a hook to the plugin — it went to
  zero when the capability guards were removed; this one is a *nudge*, not a gate).
- **SKILL / instructions**: unchanged. Prose stays primary; the hook is the floor.

## Deferred

- **Cross-restart persistence.** The marker is best-effort in-session; a session
  restart loses "I was executing X" (overlaps tranche-B durable task state in
  [`docs/TASKS.md`]TASKS.md). Fine for the floor — worst case the nudge stops
  after a restart until the next task message re-arms it.
- **Semantic filtering** ("only report interesting actions") — deliberately not
  attempted; that's the model's job (it composes the update), the hook only sets
  the beat.