Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions apps/hook/hooks/mod/inbox-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,22 +376,23 @@ export const INBOX_TOOL_BOARD_OFF_NOTE = "The Board (cards for work that outlast
* `inboxSteps`): off, no agent text carries any of it.
*/
export const INBOX_STEPS_LEAD =
'your plan for the work in this thread, as the person should see it: 3 to 8 short steps, the whole list each call (it replaces the last one). status: todo, doing, done, waiting (on the person), dropped (say why in note). Call it when a step starts or ends; it sends nothing and notifies no one. Skip it for work that ends in this reply.'
"The person's checklist for this thread, only for multi-step work you're both aligned on. High-level steps, a few plain words each; never your own to-do list. The whole list each call. status: todo, doing, done, waiting (on the person), dropped (say why in note). Update each step as it starts and ends; notifies no one."

/** The agent tool's line for the `steps` action (the owner, 2026-11-03): short, so the description stays under 2,048. */
export const INBOX_STEPS_ACTION_LINE = "keep the person's checklist for this thread current (details on the steps field)."
/** The agent tool's line for the `steps` action (the owner, 2026-11-07): short, so the description stays under 2,048. */
export const INBOX_STEPS_ACTION_LINE = "person's checklist for multi-step work you're aligned on (see the steps field)."

/** The `steps` field (the action's, and send_message's while steps are on): the approved text, then where a list goes before there is a thread. */
export const INBOX_STEPS_FIELD_DESCRIPTION = `steps: ${INBOX_STEPS_LEAD} No thread yet: send it with the send_message that starts one.`

/**
* The wake's one line on a reply in a thread where the session keeps steps
* (approval "inbox-thread-steps", approved text): "Your steps here: 3 of 6
* done; doing: X. Update them if that changed." Without a step in progress,
* (approval "inbox-thread-steps", amended 2026-11-07, approved text): "Your
* checklist here: 3 of 6 done; doing: X. If you're still working on this,
* keep it current." Without a step in progress,
* the `doing` part is left out.
*/
export function inboxStepsWakeLine(steps: { done: number; total: number; doing: string | null }): string {
return `Your steps here: ${steps.done} of ${steps.total} done${steps.doing ? `; doing: ${steps.doing}` : ''}. Update them if that changed.`
return `Your checklist here: ${steps.done} of ${steps.total} done${steps.doing ? `; doing: ${steps.doing}` : ''}. If you're still working on this, keep it current.`
}

/** A command's `steps` as the wake may print it, or null (absent, or not the Inbox's shape). */
Expand Down
2 changes: 1 addition & 1 deletion apps/hook/server/inbox-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export const TOOL_GUIDE: Record<InboxMcpToolName, InboxToolGuide> = {
`steps: ${INBOX_STEPS_LEAD}`,
"- `thread_id`: the thread (or a message id in it), from send_message's result. `steps`: the whole list, `[{ \"step\": \"Replay the failures in staging\", \"status\": \"doing\", \"note\": \"38 of 40 recovered\" }, ...]`; `[]` clears it. At most 12 steps, 80 characters per step and per note, one line each; past that nothing is written (`validation_error` naming the step).",
"- The list is your session's own in that thread; another session's list there is its own. send_message takes the same `steps`, so a message and its list go together.",
"- A reply in that thread wakes you with one line, \"Your steps here: 3 of 6 done; doing: X. Update them if that changed.\" Read_thread shows the lists. From a shell: `plannotator inbox steps <thread> --json '<list>'`.",
"- A reply in that thread wakes you with one line, \"Your checklist here: 3 of 6 done; doing: X. If you're still working on this, keep it current.\" Read_thread shows the lists. From a shell: `plannotator inbox steps <thread> --json '<list>'`.",
"- The person turns steps on and off (Settings → Capabilities in the Inbox). Off, the Inbox lists no `steps`, and a call answers `steps_off` and writes nothing.",
],
},
Expand Down
4 changes: 2 additions & 2 deletions docs/agent-guides/inbox-notifications-and-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,10 @@
- Live (`App.tsx`): while the Board is in front and the switch is on, a steps line for a thread a card links, a reconnect or catch-up, presence moving while a card wears a list, and a minute passing while one does read the cards again (250 ms debounce); the switch turning reads them once.
- Tidy (approval: "Tidy asks about a step still in `doing` when the session has been idle over 2 h or has ended"; only while the switch is on, approval item 6's "no Tidy rule" when off). `inboxStepsStale` (`packages/server/inbox-steps.ts`): a list with a step `doing` whose presence is `ended`, or `idle` with the session quiet for 2 h or more (`INBOX_STEPS_IDLE_MS`): quiet since its last idle transition when its connection says, else since it was last heard, never before the list's own last write. `inboxTidyPlan` (`stepsStale`) lists the thread for that session (reason "step in progress, 3 h"), and its wake adds one line under the thread: "Your steps here: 1 of 3 done; doing: X, unchanged for 3 h. Update them if that changed (the steps action, thread_id …)." The owner approved this line word for word (amendment `.product/approved/inbox-thread-steps-amendments-2026-11-03-2/approval.md`, not in this repo): the approved wake line, plus how long the step has been unchanged and where to update. A session Tidy cannot reach is not offered, as for every Tidy item. The note's body is written when the run starts, so delivery checks the switch again: with steps turned off since, every hand-off (`replyCommand` for a poll and the Codex, Qwen Code and T3 wakes; `withTidy` for an MCP agent's next call) gives the body through `inboxTidyBodyStepsOff` (`packages/shared/inbox/tidy.ts`), which drops each steps line and every thread left with nothing listed, and renumbers the rest. A wake that listed only steps (`inboxTidyIsStepsOnly`) is not handed over at all: it is withdrawn (`store.recordTidyWithdrawn`, under the Inbox's own host `INBOX_TIDY_WITHDRAWN_HOST`), so no turn goes out. A withdrawal settles nothing else: unlike `recordDelivery` it marks nothing read in the thread, so the person's earlier replies to that session are still handed over. The run reads that session "Nothing to change" whatever it writes later; the wake starts no cool-down; `read_thread` leaves that run's notes out for that session. The switch turning off withdraws every such wake (`inboxTidyStepsOnlyWakes`); a poll, an MCP next call and the Codex, Qwen Code and T3 wakes (their `skip`) withdraw the one they were about to hand (a switch turned off in config.json by hand). A wake already handed when the switch turns off (a poll not yet confirmed, a Codex item queued) cannot be taken back: it arrives whole and only its record reads withdrawn. Tests: `inbox-tidy.test.ts` ("a checklist step left in progress", among them "only steps listed" and "keeps the person's earlier reply owed"), `inbox-steps.test.ts` ("stale for Tidy").
- Agents (`packages/server/inbox-mcp-steps.ts`), only while the switch is on. Off, agents get **no added words**: no `steps` in tools/list (`inboxServedTools`), no `steps` field on send_message, no lines in read_thread, no wake line, no compaction count, nothing in `plannotator inbox --help`. On:
- The MCP tool `steps` (`{ thread_id, steps }`, the session's whole list; `[]` clears it), whose description is the approved text verbatim (`INBOX_STEPS_LEAD` in the contract). In the agent tool the approved text rides the `steps` field's description (`INBOX_STEPS_FIELD_DESCRIPTION`, then "No thread yet: send it with the send_message that starts one."), and the action's line is short (`INBOX_STEPS_ACTION_LINE`, the owner's words: "steps: keep the person's checklist for this thread current (details on the steps field)."), so the description stays under Claude Code's 2,048 characters with steps on (2,029; 2,031 with the Board on; `inbox-steps-agent.test.ts` holds both). It writes nothing that sends: its result says "Nothing was sent to the person."
- The MCP tool `steps` (`{ thread_id, steps }`, the session's whole list; `[]` clears it), whose description is the approved text verbatim (`INBOX_STEPS_LEAD` in the contract; amended 2026-11-07, `.product/approved/inbox-thread-steps-amendments-2026-11-07/approval.md`, not in this repo: "The person's checklist for this thread, only for multi-step work you're both aligned on. High-level steps, a few plain words each; never your own to-do list. The whole list each call. status: todo, doing, done, waiting (on the person), dropped (say why in note). Update each step as it starts and ends; notifies no one."). In the agent tool the approved text rides the `steps` field's description (`INBOX_STEPS_FIELD_DESCRIPTION`, then "No thread yet: send it with the send_message that starts one."), and the action's line is short (`INBOX_STEPS_ACTION_LINE`, the owner's words: "steps: person's checklist for multi-step work you're aligned on (see the steps field)."), so the description stays under Claude Code's 2,048 characters with steps on (2,027; 2,029 with the Board on; `inbox-steps-agent.test.ts` holds both). It writes nothing that sends: its result says "Nothing was sent to the person."
- send_message's optional `steps`, the same list for the thread the message lands in. The list and the agent session are checked before the send (`checkStepsInput`), so a refused list never leaves a message sent without it. A replay (the same `idempotency_key`) answers the first send and writes no list, so it never puts back an older one.
- read_thread on one thread lists every session's steps there, the reader's first (`Your steps (2 of 5 done; doing: …; updated …):`, then `- <status>: <step> · <note>`), and structured `steps`. A list row carries `steps: { done, total }` for the reader's own list, so the compaction list (`inboxSessionThreadsText`) says `steps 2/5`.
- A reply's wake (`replyCommand`'s `steps`, never on a Tidy, a 👍 or a hand-off notice) carries one line before the closing line, `inboxStepsWakeLine`: "Your steps here: 2 of 5 done; doing: X. Update them if that changed."
- A reply's wake (`replyCommand`'s `steps`, never on a Tidy, a 👍 or a hand-off notice) carries one line before the closing line, `inboxStepsWakeLine`: "Your checklist here: 2 of 5 done; doing: X. If you're still working on this, keep it current." (no step doing: the "; doing: X" part is left out)
- From a shell: `plannotator inbox steps [--session <id>] <thread> --json '<list>'` (`apps/hook/server/inbox-steps-command.ts`, sugar over `inbox call steps`); the stdio shim fills its session and author but no `project_path`.
- What the entry grows by: `INBOX_TOOL_COST_STEPS`.
- No shipped skill names steps (the owner: an optional feature's instructions appear only while it is on); `plannotator-skill-reference.test.ts` fails on one that does.
Expand Down
10 changes: 5 additions & 5 deletions packages/server/inbox-steps-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ import { INBOX_DEFAULT_TOOLS } from "../../apps/hook/hooks/mod/inbox-default-too
import { startInboxServer, type InboxServer } from "./inbox";
import { INBOX_MCP_INSTRUCTIONS } from "./inbox-mcp";

/** The approved agent text, exact (approval "inbox-thread-steps", item 7). Pinned on purpose. */
/** The approved agent text, exact (approval "inbox-thread-steps", item 7, amended 2026-11-07). Pinned on purpose. */
const APPROVED =
"steps: your plan for the work in this thread, as the person should see it: 3 to 8 short steps, the whole list each call (it replaces the last one). status: todo, doing, done, waiting (on the person), dropped (say why in note). Call it when a step starts or ends; it sends nothing and notifies no one. Skip it for work that ends in this reply.";
/** The action's line in the agent tool (the owner, 2026-11-03), so the description stays under Claude Code's 2,048. */
const ACTION_LINE = "steps: keep the person's checklist for this thread current (details on the steps field).";
const APPROVED_WAKE = "Your steps here: 2 of 5 done; doing: Replay the failures in staging. Update them if that changed.";
"steps: The person's checklist for this thread, only for multi-step work you're both aligned on. High-level steps, a few plain words each; never your own to-do list. The whole list each call. status: todo, doing, done, waiting (on the person), dropped (say why in note). Update each step as it starts and ends; notifies no one.";
/** The action's line in the agent tool (the owner, 2026-11-07), so the description stays under Claude Code's 2,048. */
const ACTION_LINE = "steps: person's checklist for multi-step work you're aligned on (see the steps field).";
const APPROVED_WAKE = "Your checklist here: 2 of 5 done; doing: Replay the failures in staging. If you're still working on this, keep it current.";

/** The word as an agent would read it: `steps` alone, not inside `diff_steps`. */
const STEPS_WORD = /(?<![A-Za-z_])steps(?![A-Za-z_])/i;
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/inbox/connection-wake-closing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ test("a Tidy closes with the line and names no thread; a 👍 keeps its own shor

test("the steps line stays its own paragraph before the closing line", () => {
const text = wake({ steps: { done: 2, total: 5, doing: "Replay" }, context: context() });
expect(text.endsWith(`\n\nYour steps here: 2 of 5 done; doing: Replay. Update them if that changed.\n\n${INBOX_WAKE_CLOSING_LINE}`)).toBe(true);
expect(text.endsWith(`\n\nYour checklist here: 2 of 5 done; doing: Replay. If you're still working on this, keep it current.\n\n${INBOX_WAKE_CLOSING_LINE}`)).toBe(true);
});

const STILL_OPEN = "You have 1 other question open in this thread. It stays open; the person will answer it in the Inbox. Don't re-ask it.";
Expand Down
13 changes: 7 additions & 6 deletions packages/shared/inbox/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,22 +421,23 @@ export const INBOX_TOOL_BOARD_OFF_NOTE = "The Board (cards for work that outlast
* `inboxSteps`): off, no agent text carries any of it.
*/
export const INBOX_STEPS_LEAD =
'your plan for the work in this thread, as the person should see it: 3 to 8 short steps, the whole list each call (it replaces the last one). status: todo, doing, done, waiting (on the person), dropped (say why in note). Call it when a step starts or ends; it sends nothing and notifies no one. Skip it for work that ends in this reply.'
"The person's checklist for this thread, only for multi-step work you're both aligned on. High-level steps, a few plain words each; never your own to-do list. The whole list each call. status: todo, doing, done, waiting (on the person), dropped (say why in note). Update each step as it starts and ends; notifies no one."

/** The agent tool's line for the `steps` action (the owner, 2026-11-03): short, so the description stays under 2,048. */
export const INBOX_STEPS_ACTION_LINE = "keep the person's checklist for this thread current (details on the steps field)."
/** The agent tool's line for the `steps` action (the owner, 2026-11-07): short, so the description stays under 2,048. */
export const INBOX_STEPS_ACTION_LINE = "person's checklist for multi-step work you're aligned on (see the steps field)."

/** The `steps` field (the action's, and send_message's while steps are on): the approved text, then where a list goes before there is a thread. */
export const INBOX_STEPS_FIELD_DESCRIPTION = `steps: ${INBOX_STEPS_LEAD} No thread yet: send it with the send_message that starts one.`

/**
* The wake's one line on a reply in a thread where the session keeps steps
* (approval "inbox-thread-steps", approved text): "Your steps here: 3 of 6
* done; doing: X. Update them if that changed." Without a step in progress,
* (approval "inbox-thread-steps", amended 2026-11-07, approved text): "Your
* checklist here: 3 of 6 done; doing: X. If you're still working on this,
* keep it current." Without a step in progress,
* the `doing` part is left out.
*/
export function inboxStepsWakeLine(steps: { done: number; total: number; doing: string | null }): string {
return `Your steps here: ${steps.done} of ${steps.total} done${steps.doing ? `; doing: ${steps.doing}` : ''}. Update them if that changed.`
return `Your checklist here: ${steps.done} of ${steps.total} done${steps.doing ? `; doing: ${steps.doing}` : ''}. If you're still working on this, keep it current.`
}

/** A command's `steps` as the wake may print it, or null (absent, or not the Inbox's shape). */
Expand Down