feat(hook): make push delivery opt-in via mailbox-collaborate skill

The SessionStart announce no longer forces a watch-loop bootstrap on every session — it now emits a short pointer instructing Claude to invoke the new mailbox-collaborate skill (or /collaborate slash command) when the user wants peers to wake them mid-task. Messages still surface on the next user prompt via the UserPromptSubmit hook even without the watcher, so nothing is lost; idle sessions just stop burning relaunch tokens.

The watch-loop protocol (exit codes, rename handling, mail handling) moves from the hook prose into the new skill body, where it only loads when actually needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-05-21 09:24:41 +02:00
parent 951fb4f021
commit 22824bd35f
5 changed files with 60 additions and 38 deletions

View File

@@ -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", () => {