diff --git a/node/src/cli.ts b/node/src/cli.ts index aad3bdc..0339458 100644 --- a/node/src/cli.ts +++ b/node/src/cli.ts @@ -259,7 +259,6 @@ program peers, windowMinutes: opts.peerWindowMinutes, maxPeers: opts.maxPeers, - watcherCommand: `claude-mailbox watch --block --name ${name}`, daemonError: daemonError ?? undefined, }); lines.push(""); diff --git a/node/src/hook.ts b/node/src/hook.ts index f26c8b0..b380650 100644 --- a/node/src/hook.ts +++ b/node/src/hook.ts @@ -122,12 +122,11 @@ export interface SessionAnnounceOptions { peers: PeerEntry[]; windowMinutes: number; maxPeers: number; - watcherCommand?: string; daemonError?: string; } export function buildSessionAnnounceLines(opts: SessionAnnounceOptions): string[] { - const { name, peers, windowMinutes, maxPeers, watcherCommand, daemonError } = opts; + const { name, peers, windowMinutes, maxPeers, daemonError } = opts; const lines = [ `Claude-Mailbox: your mailbox name this session is \`${name}\`.`, `The name is auto-derived as -. You can rename it (e.g. to tag your working area) with mcp__mailbox__rename(current_name="${name}", new_name="--"); after that, use the new name everywhere.`, @@ -137,21 +136,9 @@ export function buildSessionAnnounceLines(opts: SessionAnnounceOptions): string[ ` - mcp__mailbox__peek_inbox: name="${name}"`, ` - mcp__mailbox__list_mailboxes: name="${name}"`, `Peers reach you with: mcp__mailbox__send(from="", to="${name}", body="...")`, + "", + `Push delivery is OPT-IN. Do NOT launch the watcher on your own. When the user wants peers to wake you mid-task, invoke the \`mailbox-collaborate\` skill (or the /collaborate slash command) to enter collaboration mode. Without it, peers can still leave messages — you'll see them on your next user prompt via the existing UserPromptSubmit hook.`, ]; - if (watcherCommand) { - lines.push( - "", - "PUSH DELIVERY — REQUIRED FIRST ACTION, then maintain the relaunch loop forever:", - ` Before responding to anything else on your first turn, you MUST launch the mailbox watcher as a background bash task. This is the only thing that lets peers wake you mid-session without the user prompting:`, - ` Bash(command="${watcherCommand}", run_in_background=true)`, - ` The watcher is short-lived by design — it exits after exactly one event (a delivered message, a 25s timeout, a rename, or a daemon error). Claude Code's BashOutput notifications fire only on task completion, so every exit MUST be followed by a fresh background relaunch with the same command. Gaps in the loop are missed pushes.`, - ` When a watcher completion surfaces:`, - ` - stdout contains "[Claude-Mailbox] Mail from : " → treat the body as an inbound interrupt with priority over your current plan, then relaunch.`, - ` - stdout contains "[Claude-Mailbox] Mailbox renamed to ''" → relaunch with --name (also update your own identity for mcp__mailbox__* calls).`, - ` - exit code 3 with no stdout → silent timeout, just relaunch.`, - ` - exit code 2 → daemon unreachable; wait ~5 s, then relaunch.`, - ); - } if (daemonError) { lines.push("", daemonError); } else { diff --git a/node/tests/hook.test.ts b/node/tests/hook.test.ts index e036b20..aa1ade7 100644 --- a/node/tests/hook.test.ts +++ b/node/tests/hook.test.ts @@ -372,27 +372,7 @@ describe("buildSessionAnnounceLines", () => { expect(out).toContain("mcp__mailbox__send"); }); - it("includes the watcher bootstrap instruction when watcherCommand is set", () => { - const out = buildSessionAnnounceLines({ - name: "alice-abc12345", - peers: [], - windowMinutes: 60, - maxPeers: 10, - watcherCommand: "claude-mailbox watch --block --name alice-abc12345", - }).join("\n"); - expect(out).toContain("watch --block --name alice-abc12345"); - expect(out).toContain("run_in_background=true"); - expect(out).toMatch(/\[Claude-Mailbox\] Mail from/); - // Strong-directive framing: must be a REQUIRED FIRST ACTION, not a soft tip. - expect(out).toMatch(/REQUIRED FIRST ACTION/); - expect(out).toMatch(/MUST launch/); - // Relaunch protocol must cover every exit code Claude will see. - expect(out).toMatch(/exit code 3/); - expect(out).toMatch(/exit code 2/); - expect(out).toMatch(/renamed to/); - }); - - it("omits the watcher instruction when watcherCommand is unset", () => { + it("never auto-bootstraps the watcher — push delivery must be opt-in", () => { const out = buildSessionAnnounceLines({ name: "alice-abc12345", peers: [], @@ -401,6 +381,20 @@ describe("buildSessionAnnounceLines", () => { }).join("\n"); expect(out).not.toContain("watch --block"); expect(out).not.toContain("run_in_background"); + expect(out).not.toMatch(/REQUIRED FIRST ACTION/); + expect(out).not.toMatch(/MUST launch/); + }); + + it("points the user to the opt-in collaborate skill / slash command", () => { + const out = buildSessionAnnounceLines({ + name: "alice-abc12345", + peers: [], + windowMinutes: 60, + maxPeers: 10, + }).join("\n"); + expect(out).toMatch(/mailbox-collaborate/); + expect(out).toMatch(/\/collaborate/); + expect(out).toMatch(/OPT-IN/); }); it("replaces the peer list with the daemonError hint when daemon is unreachable", () => { diff --git a/plugin/commands/collaborate.md b/plugin/commands/collaborate.md new file mode 100644 index 0000000..78bc35b --- /dev/null +++ b/plugin/commands/collaborate.md @@ -0,0 +1,5 @@ +--- +description: Enter Claude-Mailbox collaboration mode — start the push-delivery watch loop so peers can wake Claude mid-task. +--- + +Invoke the `mailbox-collaborate` skill now to enter collaboration mode and start the watcher relaunch loop. diff --git a/plugin/skills/mailbox-collaborate/SKILL.md b/plugin/skills/mailbox-collaborate/SKILL.md new file mode 100644 index 0000000..9f98413 --- /dev/null +++ b/plugin/skills/mailbox-collaborate/SKILL.md @@ -0,0 +1,37 @@ +--- +name: mailbox-collaborate +description: Start the Claude-Mailbox push-delivery watch loop for this session so peers can wake Claude mid-task. Use when the user says "let's collaborate", "wait for mail", "watch the mailbox", "listen for peers", "enter collaboration mode", or invokes /collaborate. Without this skill the watcher is OFF — peers can still leave messages but Claude only sees them on the next user prompt. Do NOT launch the watcher without this opt-in. +--- + +You are now in **mailbox collaboration mode**. Peers can wake you mid-task by sending mail to your mailbox. + +## Launch the watcher + +Your mailbox name was announced at SessionStart (format: `-`). Use it in place of `` below. + +Launch the watcher as a background bash task immediately: + +``` +Bash(command="claude-mailbox watch --block --name ", run_in_background=true) +``` + +The watcher is short-lived by design — it exits after exactly one event (delivered message, 25-second timeout, rename, or daemon error). Claude Code's BashOutput notifications fire only on task completion, so every exit MUST be followed by a fresh background relaunch with the same command. Gaps in the loop = missed pushes. + +## Handling watcher completions + +When a watcher task completes, inspect its stdout and exit code: + +- **stdout contains `[Claude-Mailbox] Mail from : `** → treat the body as an inbound interrupt with priority over your current plan. Address it, then relaunch the watcher. +- **stdout contains `[Claude-Mailbox] Mailbox renamed to ''`** → relaunch with `--name `, and use `` for all future `mcp__mailbox__*` calls (update your identity). +- **exit code 3 with no stdout** → silent timeout, just relaunch. +- **exit code 2** → daemon unreachable; wait ~5 seconds, then relaunch. +- **any other exit code** → report it to the user, then relaunch. + +## Stopping + +Keep the loop running until the user says "stop watching", "stop collaborating", "end collaboration", or similar. When they do: + +- Stop relaunching after the next completion. +- If a watcher is currently mid-poll and the user wants it killed immediately, use `TaskStop` on its task id. + +Do not re-enter collaboration mode on your own after stopping — wait for the user to invoke this skill again.