acstack 0.0.0-stage → 0.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +389 -2
  4. package/THIRD_PARTY_NOTICES +14 -0
  5. package/TROUBLESHOOTING.md +225 -0
  6. package/agents/codex/frontend-verifier.toml +352 -0
  7. package/agents/codex/task-checker.toml +345 -0
  8. package/agents/codex/task-executor.toml +157 -0
  9. package/agents/codex/test-writer.toml +149 -0
  10. package/agents/frontend-verifier.md +351 -0
  11. package/agents/task-checker.md +344 -0
  12. package/agents/task-executor.md +156 -0
  13. package/agents/test-writer.md +148 -0
  14. package/assets/playwriter-bridge/build-launcher.ps1 +32 -0
  15. package/assets/playwriter-bridge/doctor.ps1 +450 -0
  16. package/assets/playwriter-bridge/launcher-src.cs +131 -0
  17. package/assets/playwriter-bridge/opener.js +330 -0
  18. package/assets/playwriter-bridge/opener.ps1 +63 -0
  19. package/assets/playwriter-bridge/register-tasks.ps1 +328 -0
  20. package/assets/playwriter-bridge/relay.ps1 +50 -0
  21. package/assets/playwriter-bridge/stop.ps1 +125 -0
  22. package/assets/playwriter-bridge/sync.ps1 +271 -0
  23. package/assets/playwriter-bridge/tunnel.ps1 +325 -0
  24. package/assets/playwriter-bridge/vm-free-port.sh +166 -0
  25. package/assets/terminal-workspace/client-keybindings.md +87 -0
  26. package/assets/terminal-workspace/config.example.yaml +334 -0
  27. package/assets/terminal-workspace/setup.sh +4404 -0
  28. package/cli/README.md +47 -0
  29. package/cli/bin/acstack.js +3 -0
  30. package/cli/dist/index.js +25984 -0
  31. package/cli/dist/index.js.map +7 -0
  32. package/cli/package.json +59 -0
  33. package/docs/adr/0001-ste-writing-standard.md +16 -0
  34. package/docs/adr/0002-version-gate-not-auto-bump.md +35 -0
  35. package/docs/adr/0003-handoff-ack-command.md +19 -0
  36. package/docs/adr/0004-dev-setup-per-host.md +13 -0
  37. package/docs/adr/0005-two-orchestrators-per-host.md +25 -0
  38. package/docs/adr/0006-document-opener-service.md +89 -0
  39. package/docs/adr/0007-lead-check-reads-the-transcript.md +87 -0
  40. package/docs/adr/0008-cli-audits-skill-interviews.md +37 -0
  41. package/docs/adr/0009-verify-the-write-not-the-writer.md +51 -0
  42. package/docs/adr/0010-promote-dev-to-main-by-fast-forward.md +115 -0
  43. package/docs/adr/0011-node-header-is-the-edge-encoding.md +20 -0
  44. package/docs/adr/0012-node-kind-is-a-closed-set-of-five.md +31 -0
  45. package/docs/adr/0013-the-diagram-renderer-is-pure.md +23 -0
  46. package/docs/adr/0014-reading-a-node-hides-its-generated-diagram.md +29 -0
  47. package/docs/adr/0015-a-node-id-links-to-its-issue-through-an-anchor.md +37 -0
  48. package/docs/adr/0016-the-map-root-carries-no-status-of-its-own.md +21 -0
  49. package/docs/adr/0017-call-impeccable-copy-only-its-questions.md +13 -0
  50. package/docs/adr/index.md +17 -0
  51. package/docs/config-reference.md +572 -0
  52. package/docs/dev-setups.md +164 -0
  53. package/docs/migrate-0.8.md +28 -0
  54. package/docs/playwriter-bridge-windows.md +15 -0
  55. package/docs/terminal-workspace-keys.md +137 -0
  56. package/docs/terminal-workspace.md +1780 -0
  57. package/docs/workflows.md +309 -0
  58. package/hooks/ac-writing-style-reminder.sh +86 -0
  59. package/hooks/context-pressure.sh +583 -0
  60. package/hooks/hooks.json +50 -0
  61. package/hooks/session-start-handoff.sh +165 -0
  62. package/hooks/session-start.sh +214 -0
  63. package/orchestrator/AGENTS.md +532 -0
  64. package/output-styles/ac-writing-style.md +286 -0
  65. package/package.json +50 -4
  66. package/pi/agents/frontend-verifier.md +357 -0
  67. package/pi/agents/task-checker.md +348 -0
  68. package/pi/agents/task-executor.md +160 -0
  69. package/pi/agents/test-writer.md +154 -0
  70. package/pi/extension.ts +723 -0
  71. package/profiles/fullstack-python.yaml +29 -0
  72. package/profiles/node-typescript.yaml +23 -0
  73. package/profiles/python.yaml +24 -0
  74. package/skills/archive/SKILL.md +160 -0
  75. package/skills/bootstrap/SKILL.md +109 -0
  76. package/skills/bridge/HANDOFF.md +93 -0
  77. package/skills/bridge/SKILL-VM.md +292 -0
  78. package/skills/bridge/SKILL-WINDOWS.md +526 -0
  79. package/skills/bridge/SKILL.md +121 -0
  80. package/skills/browser-verify/SKILL.md +229 -0
  81. package/skills/code-review/SKILL.md +44 -0
  82. package/skills/commit/SKILL.md +106 -0
  83. package/skills/compact-and-continue/SKILL.md +241 -0
  84. package/skills/explore/SKILL.md +149 -0
  85. package/skills/grilling/SKILL.md +134 -0
  86. package/skills/handoff/SKILL.md +250 -0
  87. package/skills/handoff/launch-herdr.md +197 -0
  88. package/skills/handoff/launch-without-herdr.md +30 -0
  89. package/skills/handoff/lead-check.md +59 -0
  90. package/skills/handoff/outcomes.md +162 -0
  91. package/skills/handoff/running-agent.md +50 -0
  92. package/skills/handoff/watch.md +115 -0
  93. package/skills/host/SKILL.md +247 -0
  94. package/skills/host/detect-host.sh +297 -0
  95. package/skills/host/herdr.md +79 -0
  96. package/skills/host/install-herdr-block.sh +467 -0
  97. package/skills/host/install-orca.sh +512 -0
  98. package/skills/host/notes.md +97 -0
  99. package/skills/host/orca.md +160 -0
  100. package/skills/host/questions.md +163 -0
  101. package/skills/host/report.md +42 -0
  102. package/skills/implement/SKILL.md +230 -0
  103. package/skills/implement/decision-log.md +62 -0
  104. package/skills/implement/design-pass.md +32 -0
  105. package/skills/implement/relaxed-tdd.md +28 -0
  106. package/skills/map/SKILL.md +250 -0
  107. package/skills/map/frontend.md +227 -0
  108. package/skills/map/trackers/files.md +74 -0
  109. package/skills/map/trackers/github.md +180 -0
  110. package/skills/map/visuals.md +387 -0
  111. package/skills/new-worktree/SKILL.md +155 -0
  112. package/skills/new-worktree/detect-orchestrator.sh +328 -0
  113. package/skills/new-worktree/detect-orchestrator.test.sh +894 -0
  114. package/skills/new-worktree/herdr.md +84 -0
  115. package/skills/new-worktree/orca.md +101 -0
  116. package/skills/pre-commit-review/SKILL.md +242 -0
  117. package/skills/pre-commit-review/auto-fix.md +19 -0
  118. package/skills/pre-commit-review/interactive.md +35 -0
  119. package/skills/quick-fix/SKILL.md +125 -0
  120. package/skills/record-context/SKILL.md +106 -0
  121. package/skills/setup/SKILL.md +250 -0
  122. package/skills/setup/browser.md +123 -0
  123. package/skills/setup/design-review.md +97 -0
  124. package/skills/setup/first-run.md +114 -0
  125. package/skills/setup/report.md +68 -0
  126. package/skills/setup/row-fixes.md +196 -0
  127. package/skills/setup/terminal-workspace.md +153 -0
  128. package/skills/sync/SKILL.md +171 -0
  129. package/skills/teardown-worktree/SKILL.md +245 -0
  130. package/skills/teardown-worktree/detector.md +39 -0
  131. package/skills/teardown-worktree/herdr.md +113 -0
  132. package/skills/teardown-worktree/orca.md +61 -0
  133. package/skills/terminal-workspace/SKILL.md +249 -0
  134. package/skills/terminal-workspace/components.md +36 -0
  135. package/skills/terminal-workspace/config-choices.md +69 -0
  136. package/skills/terminal-workspace/emulator-keys.md +48 -0
  137. package/skills/terminal-workspace/headless-clipboard.md +175 -0
  138. package/skills/terminal-workspace/turn-off-and-update.md +104 -0
  139. package/skills/test-conventions/SKILL.md +59 -0
  140. package/skills/visual-docs/SKILL.md +246 -0
  141. package/skills/writing/SKILL.md +231 -0
  142. package/skills/writing/examples.md +153 -0
  143. package/skills/writing/vocabulary.md +96 -0
@@ -0,0 +1,197 @@
1
+ # Launch through herdr
2
+
3
+ `SKILL.md` decides when to read this file: herdr hosts this session and the
4
+ work goes to a new worktree or a new tab.
5
+
6
+ ## Naming workspaces and tabs
7
+
8
+ herdr labels a new tab with a number, such as `1` or `2`. A user with four tabs
9
+ open cannot tell which agent works on what.
10
+
11
+ So every workspace and tab a handoff touches carries a descriptive label.
12
+ `label` is the only naming field herdr has. There is no `--name`, no `--title`,
13
+ and no `tab update`.
14
+
15
+ A tab label reads `<topic>-<thread>.<step>`.
16
+
17
+ - **topic** names the work in one or two words, lower-case and hyphenated, such
18
+ as `auth-bug` or `csv-export`. For a new worktree it is the workspace slug
19
+ already passed to `--label`, or a shortening of it. For a new tab it is the
20
+ subject of the handoff.
21
+
22
+ - **thread** numbers a line of work inside the workspace. The numbering restarts
23
+ in each workspace. So a handoff into a new worktree labels the receiving tab
24
+ `<topic>-1.0`, and a handoff inside this workspace labels it `<topic>-1.1`.
25
+
26
+ - **step** counts the handoffs along that line.
27
+
28
+ The session that started the line is step 0. The agent it hands to is
29
+ step 1. That agent's own handoff is step 2.
30
+
31
+ Keep the topic to two words at most, so the whole label fits the tab strip.
32
+
33
+ | Tab label | Who holds it |
34
+ | -------------- | ------------------------------------------------- |
35
+ | `auth-bug-1.0` | the session that started the line of work |
36
+ | `auth-bug-1.1` | the agent that session hands to |
37
+ | `auth-bug-1.2` | the agent the step 1 agent hands to |
38
+ | `auth-bug-2.0` | a fresh line of work started in the same workspace |
39
+
40
+ Read the labels already in use before you create a tab, so the new one continues
41
+ the numbering instead of colliding with it:
42
+
43
+ ```bash
44
+ herdr tab list --workspace "$HERDR_WORKSPACE_ID"
45
+ # {"result":{"tabs":[{"tab_id":"w1X:t1","label":"1","number":1}]}}
46
+ ```
47
+
48
+ Find this session's own tab in that list. `$HERDR_TAB_ID` names it. herdr sets
49
+ that variable in every managed pane, so an empty value means this session sits
50
+ in no herdr pane. Skip the rename of this session's tab then, and never fall
51
+ back to the focused tab. The focused tab belongs to whichever pane the user is
52
+ looking at, which is often another workspace.
53
+
54
+ A label of digits only is still the herdr default. Rename such a tab before you
55
+ create its successor, so the pair reads as a sequence:
56
+
57
+ ```bash
58
+ herdr tab rename <tab_id> <topic>-<thread>.0
59
+ ```
60
+
61
+ Rename another tab only when its label is digits only. Any other label may be
62
+ one the user or another agent set. Leave it alone.
63
+
64
+ A new worktree gets its workspace label from `herdr worktree create --label
65
+ <slug>`. Rename the workspace when that slug is long, or says little to a
66
+ reader – one or two words plus the branch intent:
67
+
68
+ ```bash
69
+ herdr workspace rename <workspace_id> <label>
70
+ ```
71
+
72
+ The first tab of a new workspace is the spare root pane's tab. Rename it to
73
+ `<topic>-1.0` after `worktree create`.
74
+
75
+ Read its tab id from the create JSON, or from
76
+ `herdr tab list --workspace <new_ws_id>`. Never hardcode a tab id.
77
+
78
+ Match the agent name in `herdr agent start <name>` to the tab label. The handle
79
+ regex is `[a-z][a-z0-9_-]{0,31}`, which allows no dot. So the agent whose tab is
80
+ `auth-bug-1.1` takes the name `auth-bug-1-1`.
81
+
82
+ ## Launch: a new worktree, for separate work
83
+
84
+ ```bash
85
+ cd <main-repo-root> && git fetch origin --quiet && \
86
+ herdr worktree create --cwd "<main-repo-root>" --branch <branch> --base <base> \
87
+ --label <slug> --no-focus
88
+ # read .result.root_pane.pane_id from the JSON – never hardcode
89
+ # .result.worktree.path confirms where the checkout landed, for the report at the end
90
+
91
+ # name the workspace's first tab – read its tab id from the JSON, never hardcode
92
+ herdr tab rename <root_tab_id> <topic>-1.0
93
+
94
+ # the quoted prompt is the FRESH AGENT opener in SKILL.md – use it verbatim
95
+ herdr agent start <handoff-name> --kind claude --pane <root_pane_id> \
96
+ -- "Before any other tool call, acknowledge this handoff by running ~/.claude/acstack/bin/acstack handoff ack accept --file <handoff-file> \"<one-line reason>\", or ack decline --file <handoff-file> \"<one-line reason>\". You may read <handoff-file> first to judge whether the work suits you, and nothing else until the command has run – the sender watches for the file it writes and cannot report this work as yours until it exists. Then continue that work."
97
+ ```
98
+
99
+ `--label <slug>` names the workspace, and the rename names its first tab –
100
+ *Naming workspaces and tabs*, above, gives the format.
101
+
102
+ `<base>` is `origin/dev` when that ref exists, otherwise `origin/main` – the same
103
+ rule the `new-worktree` skill follows. Local `dev` is often behind. So the fetch
104
+ comes first, and the command cuts the branch from the remote ref.
105
+
106
+ Stop after `worktree create`. It already leaves a spare root shell pane in the
107
+ new workspace, and that is the pane the agent goes into.
108
+
109
+ The `new-worktree` skill's extra pane-move steps exist only because that skill
110
+ wants the caller to end up there. A handoff does not.
111
+
112
+ The agent name is the handle – `[a-z][a-z0-9_-]{0,31}`, unique among live
113
+ agents. Every `herdr agent` subcommand accepts it in place of a pane ID, and it
114
+ survives a pane move. So always name the agent something short that describes
115
+ the work.
116
+
117
+ Pass `--no-focus` on every create, so the user's view never jumps.
118
+
119
+ ### When `agent start` times out
120
+
121
+ `agent start` can return `{"error":{"code":"timeout"}}` when the launch actually
122
+ worked. A timeout means the outcome is unknown, not failed, so check the pane
123
+ before concluding anything:
124
+
125
+ ```bash
126
+ herdr pane get <pane-id> # read .result.pane.agent_session.value
127
+ ```
128
+
129
+ If `agent_session.value` is present, the launch succeeded. herdr never
130
+ registered the named handle. So use the pane ID in place of the agent name for
131
+ every later `herdr agent ...` command – they all accept a pane ID.
132
+
133
+ Use that value as the session id for the acknowledgement watcher.
134
+ `agent wait <name>` and `agent get <name>` return `agent_not_found` here. That
135
+ is normal, not a second failure.
136
+
137
+ If it is absent, wait a few seconds. Then check once more. Only then treat the
138
+ launch as failed and report it.
139
+
140
+ Report upstream. If the timeout reproduces, file it against herdr – the launch
141
+ succeeded, but herdr never registered the handle.
142
+
143
+ ## Launch: a new tab, for a continuation
144
+
145
+ Same shape one level down – no new checkout, so no worktree:
146
+
147
+ ```bash
148
+ herdr tab list --workspace "$HERDR_WORKSPACE_ID" # the labels already in use
149
+ herdr tab create --workspace "$HERDR_WORKSPACE_ID" --label <topic>-<thread>.<step> --no-focus
150
+ # read the new tab's root pane ID from .result.root_pane.pane_id – same field the worktree's uses above
151
+ ```
152
+
153
+ `--label` sets the tab label at creation – *Naming workspaces and tabs*, above,
154
+ gives the format and the numbering.
155
+
156
+ Then `herdr agent start` into that pane, exactly as above – same fresh-agent
157
+ opener, same handoff file path.
158
+
159
+ ## The trust dialog
160
+
161
+ Handle this, or the launch hangs silently. A new worktree is a new directory, so
162
+ the launched session asks the user to trust the directory. It then sits at
163
+ `blocked`, while `agent start` has *already* returned success with
164
+ `interactive_ready: true`.
165
+
166
+ Readiness is not proof the prompt is running. So after starting, wait for the
167
+ blocked state instead of polling by hand:
168
+
169
+ ```bash
170
+ herdr agent wait <name> --until blocked --timeout <ms>
171
+ ```
172
+
173
+ On `blocked`, accept the dialog with `herdr agent send-keys <name> enter`. Then
174
+ re-check by waiting for the session to start working:
175
+
176
+ ```bash
177
+ herdr agent wait <name> --until working --timeout <ms>
178
+ ```
179
+
180
+ If either wait times out, read the agent's current state directly with
181
+ `herdr agent get <name>` rather than guessing.
182
+
183
+ Throughout this section `<name>` is the pane ID instead, whenever the launch
184
+ timed out – see *When `agent start` times out*, above.
185
+
186
+ Such a launch may already be past the trust dialog. The pane can report
187
+ `agent_status: working`, so `herdr agent wait <pane-id> --until blocked` may
188
+ simply time out. Read the real state with `herdr agent get <pane-id>`, and carry
189
+ on from there.
190
+
191
+ Never write `hasTrustDialogAccepted` into `~/.claude.json`, and never edit any
192
+ security settings file. ACStack does not touch those.
193
+
194
+ ## Report the labels
195
+
196
+ For a launched agent, also name the workspace label and the tab label. Those are
197
+ what the user reads off the tab strip to find the pane.
@@ -0,0 +1,30 @@
1
+ # Launch without herdr
2
+
3
+ `SKILL.md` decides when to read this file: herdr does not host this session.
4
+
5
+ ## Without herdr
6
+
7
+ Detect herdr with `test "${HERDR_ENV:-}" = 1`. When absent, fall back to a
8
+ background session using the command template from `workflow.handoff.agent`
9
+ (default `claude`):
10
+
11
+ ```bash
12
+ # again the FRESH AGENT opener in SKILL.md, verbatim
13
+ <agent-template> --bg --name "<name>" "Before any other tool call, acknowledge this handoff by running ~/.claude/acstack/bin/acstack handoff ack accept --file <handoff-file> \"<one-line reason>\", or ack decline --file <handoff-file> \"<one-line reason>\". You may read <handoff-file> first to judge whether the work suits you, and nothing else until the command has run – the sender watches for the file it writes and cannot report this work as yours until it exists. Then continue that work."
14
+ ```
15
+
16
+ There are no tabs and no workspaces here, so the destination rule has two cases,
17
+ not three.
18
+
19
+ A continuation – what would have been a new tab – is a plain `--bg` session. It
20
+ starts in the CURRENT directory, which is the checkout this session is already
21
+ in. That is exactly what a continuation wants.
22
+
23
+ Genuinely separate or parallel work still needs its own checkout, and `--bg`
24
+ will not make one – launched as-is it would quietly share this one. So create
25
+ the worktree first with the `new-worktree` skill, which falls back to a plain
26
+ git worktree when herdr is absent. Then launch the `--bg` session from inside
27
+ that worktree.
28
+
29
+ Everything else is identical, because both paths produce a real session with a
30
+ transcript.
@@ -0,0 +1,59 @@
1
+ # The lead check and the quiet marker
2
+
3
+ `SKILL.md` decides when to read this file: before you change the lead-check
4
+ command in *Only the lead session runs this*, or when its answer looks wrong.
5
+ That command is the one these facts describe.
6
+
7
+ The reason to stop is the quiet marker. This skill's last step writes that
8
+ marker, which silences the lead's context-pressure nudge. Inside a child
9
+ session `CLAUDE_CODE_SESSION_ID` names the lead, so its marker silences a nudge
10
+ the lead still needs.
11
+
12
+ Five facts sit behind that command, measured on Claude Code 2.1.241. Each one
13
+ names a way the command has already been got wrong:
14
+
15
+ - **No environment variable answers this.** `CLAUDE_CODE_CHILD_SESSION` holds
16
+ `1` in every Bash subprocess, lead or child. It marks "spawned by Claude
17
+ Code", nothing more. An in-process child session shares the lead's
18
+ `CLAUDE_CODE_SESSION_ID`, `CLAUDE_PID` and `CLAUDE_CODE_ENTRYPOINT`.
19
+
20
+ - **The command hunts for its own record, so it waits.** The host writes that
21
+ record while the command runs, roughly four seconds in. One grep with no loop
22
+ runs too early, finds nothing and answers `lead` for everyone. Never flatten
23
+ the loop.
24
+
25
+ - **The nonce must be yours alone.** Reuse one, or take one from a prompt, and
26
+ another agent's transcript answers for you. The angle brackets make an
27
+ unsubstituted copy a bash syntax error. That beats a silent match on whatever
28
+ agent read this file before you, so never soften `<nonce>` to a bare word.
29
+
30
+ - **`<session>.jsonl` can only ever say `lead`.** Your own mark lands there
31
+ whether you are the lead or not. So the loop reads the agent transcripts
32
+ first, and treats that file as an early exit rather than evidence of a child.
33
+
34
+ - **No evidence means lead.** The old gate read a variable that is always set.
35
+ It answered `child` in every session, and neither skill could run (#146).
36
+
37
+ Ten seconds with no matching transcript answers `lead` instead. A teammate
38
+ running as its own top-level session lands here, and rightly: it owns its
39
+ own marker.
40
+
41
+ - **pi answers from the session file path.** pi exports the session file as
42
+ `PI_SESSION_FILE`, and a `pi-subagents` child keeps it in one of two layouts:
43
+
44
+ - `<parent session>/<run id>/run-<n>/session.jsonl` for a fresh child
45
+ - `<parent session>/forks/<file>.jsonl` for a child with forked context
46
+
47
+ A matching path means `child` at once. Any other path, with
48
+ `CLAUDE_CODE_SESSION_ID` unset, means `lead` at once, with no wait.
49
+
50
+ A child's `PI_SESSION_ID` is its own id, never the parent's, so the id
51
+ alone cannot tell the two apart. The path layout comes from `pi-subagents`,
52
+ checked by hand on 2026-10-11. Recheck it when `pi-subagents` changes how it
53
+ stores child sessions.
54
+
55
+ ## When the quiet marker clears
56
+
57
+ The hook clears the marker when the session compacts, because it resets its
58
+ state on `SessionStart` with `source: compact`. So the nudge can return once the
59
+ context grows again.
@@ -0,0 +1,162 @@
1
+ # Outcomes of the watch
2
+
3
+ `SKILL.md` decides when to read this file: the watcher has exited and printed
4
+ its result.
5
+
6
+ The watcher prints one JSON object and exits with one of four outcomes. The
7
+ same object carries `anchored`, and you cannot read one of those outcomes
8
+ honestly without it:
9
+
10
+ | Field | Means |
11
+ | ----------------------- | ------------------------------------------------------------------------------ |
12
+ | `outcome: acknowledged` | the target acknowledged – `ack.decision` is `accept`, `decline`, or `null` if neither |
13
+ | `outcome: engaged` | no acknowledgement after N turns, but the target read the handoff file or started using work tools |
14
+ | `outcome: silence` | delivered, or launched, then N turns passed with no acknowledgement |
15
+ | `outcome: timeout` | the watcher's own deadline elapsed first |
16
+ | `ack.via` | `cli` when the sidecar recorded the decision, `text` when only the transcript did |
17
+ | `ack.reason` | the one-line reason the target gave, or `null` when it gave none |
18
+ | `nudged` | whether this watch ran with `--nudged`, so the one nudge is already spent |
19
+ | `engaged` | whether the target is working on the handoff, whatever the outcome |
20
+ | `staleAck` | a sidecar that exists but predates the cut-off – read it before reporting no answer |
21
+ | `ackAfter` | the sidecar cut-off this watch applied, in epoch milliseconds; `null` means the target's transcript had not begun |
22
+ | `startedAt` | epoch milliseconds at which this watch began, and the token route's sidecar cut-off |
23
+ | `anchored` | whether the watcher ever saw the thing it counts from – our message arriving in the target's transcript, or a fresh agent's transcript beginning. `false` means delivery was never observed at all |
24
+
25
+ ## Nudge once, then watch again
26
+
27
+ An unacknowledged target gets exactly one nudge. The watcher returns `silence`
28
+ or `engaged`, and `nudged` is `false`.
29
+
30
+ Send one message to the target saying it has not acknowledged, and that it must
31
+ run the ack command now. Quote the command with the handoff file path in it.
32
+
33
+ | The target is | Nudge it with |
34
+ | -------------------- | -------------------------------------------------------------- |
35
+ | in a fresh pane | `herdr agent prompt <name> "<nudge text>"` |
36
+ | already running | the host's agent-messaging tool (`SendMessage` on Claude Code) |
37
+
38
+ Then restart the watcher. Keep every argument, add `--nudged`, and re-anchor.
39
+
40
+ The second watch must not count from where the first one started. It would
41
+ report an instant silence off turns that elapsed before the nudge.
42
+
43
+ Each route re-anchors differently:
44
+
45
+ | The first watch used | Re-anchor the second one by |
46
+ | -------------------- | ------------------------------------------------------------------------ |
47
+ | `--token` | putting a fresh token in the nudge message, and passing that new token |
48
+ | `--fresh` | passing `--turns` at double `workflow.handoff.ack_turns` |
49
+
50
+ The token route gets a new anchor for free, because the nudge is a fresh
51
+ delivery. The nudge counts as an attempt under *Generate the token fresh for
52
+ every attempt*, in `running-agent.md`, so never send it carrying the first token.
53
+
54
+ The fresh route has no such anchor – `--fresh` counts from record 0 again, over
55
+ a transcript that already holds the spent turns. Double the budget covers the
56
+ spent turns and a fresh budget on top.
57
+
58
+ On the token route, pass `--ack-after <startedAt>` too, carrying the
59
+ `startedAt` the first watch reported. Without it the restart would throw away
60
+ an ack the target wrote in the gap between the first watch ending and this one
61
+ starting. The fresh route needs no flag: its cut-off is the launch, which is
62
+ already behind both watches.
63
+
64
+ ```bash
65
+ # token route – <new-token> is the one in the nudge message
66
+ ~/.claude/acstack/bin/acstack handoff watch --file <handoff-file> --nudged \
67
+ --session-id <id> --cwd <dir> --token <new-token> --turns <n> \
68
+ --ack-after <startedAt of the first watch>
69
+
70
+ # fresh route – <n> doubled, because --fresh re-counts the spent turns
71
+ ~/.claude/acstack/bin/acstack handoff watch --file <handoff-file> --nudged \
72
+ --session-id <id> --cwd <dir> --fresh --turns <2n>
73
+ ```
74
+
75
+ The `--nudged` flag tells the second watch that the target has had its nudge,
76
+ so never omit it. Never nudge twice. A watcher that returns `nudged: true` has
77
+ had its one nudge – act on the outcome and report.
78
+
79
+ ## What each outcome means
80
+
81
+ **ACCEPT** – the target owns the work now. Report that, and stop.
82
+
83
+ **DECLINE – read the reason before doing anything.** Two very different refusals
84
+ wear the same word:
85
+
86
+ | The reason is about | Do |
87
+ | --------------------------------------------------------------- | ------------------------------------------------------------ |
88
+ | **fit** – busy, wrong scope, owns files this would conflict with | launch a fresh agent, as the handoff intended in the first place |
89
+ | **the merits** – the work looks wrong or unsafe | stop and report to the user; do not shop the work around |
90
+
91
+ Treating the two identically throws away the useful half of the answer.
92
+
93
+ A relaunch after a decline on fit reuses the same handoff file, and needs no
94
+ cleanup. The new watch ignores the decline sidecar the first target wrote,
95
+ because that sidecar predates the new agent's launch.
96
+
97
+ Never pass `--ack-after` on such a relaunch. It aims at a new agent, so the
98
+ launch cut-off is the one that keeps the old decline out.
99
+
100
+ **Acknowledged, but `ack.decision` is null** – the target replied with something
101
+ that is neither ACCEPT nor DECLINE, such as a clarifying question. This is not
102
+ acceptance.
103
+
104
+ Report the reply verbatim and stop. Never treat it as ownership.
105
+
106
+ This outcome only appears when the watch ran without `--file`. With `--file` the
107
+ watcher never reports a null decision. The sidecar records one word or the
108
+ other, and a keyword-free message is not an acknowledgement.
109
+
110
+ **`ack.via: text`** – report it as an acknowledgement, and say the target typed
111
+ the decision rather than recording it. The sidecar does not exist, so nothing on
112
+ disk holds the decision.
113
+
114
+ A target that cannot write the sidecar gets that instruction from the failing
115
+ ack command itself. A read-only temp directory is one such case. The command
116
+ tells the target to reply with a message beginning ACCEPT or DECLINE.
117
+
118
+ Everything else about the outcome reads the same.
119
+
120
+ **Engaged** – the target is doing the work without acknowledging it. On
121
+ `nudged: false`, nudge once and watch again. On `nudged: true`, report
122
+ "proceeding, unacknowledged", name the target, and stop.
123
+
124
+ **Never relaunch the work elsewhere.** Two agents on the same files is the
125
+ failure file ownership exists to prevent. A second launch causes exactly it.
126
+
127
+ **Silence** – a first-class outcome, not an error. The watcher saw the message
128
+ land, or saw the agent launch, and N turns then passed with nothing back.
129
+
130
+ Nudge once, then watch again. After the nudge, report exactly that.
131
+
132
+ Name the target so the user can go and look. Then stop.
133
+
134
+ **Timeout – read `anchored` before saying a word about the target.** One outcome
135
+ covers two situations. Reporting the second as the first is the one report that
136
+ must never be wrong:
137
+
138
+ | `anchored` | What actually happened | Report it as |
139
+ | ---------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
140
+ | `true` | the message landed, or the fresh agent's transcript began, and the watcher's own deadline then ran out | delivered (or launched), no answer yet |
141
+ | `false` | the watcher never saw it land: still queued, wrong session id or cwd, wrong or reused token, unreadable transcript | **never observed delivered** – a fact about our watcher, not about the target |
142
+
143
+ On `anchored: false`, never call the target silent, busy or unresponsive: there
144
+ is no evidence about the target at all. Have the user check the target's session
145
+ id, its cwd, and the token before concluding anything about it.
146
+
147
+ On `engaged: true`, report "proceeding, unacknowledged", whatever the outcome.
148
+ The field rides on every result, so a timeout can carry it too. Never relaunch
149
+ the handoff elsewhere while the target works on it.
150
+
151
+ Nothing in any of these reports may imply anyone owns the work.
152
+
153
+ ## Reporting an unacknowledged handoff
154
+
155
+ For an unacknowledged handoff, say which kind – delivered (or launched) with no
156
+ answer, versus never observed delivered at all. Then point the user at where to
157
+ look.
158
+
159
+ **Read `staleAck` before calling any handoff unacknowledged.** It means a
160
+ sidecar sits on disk that the cut-off ruled out. Report its decision and its
161
+ `at`, name the cut-off from `ackAfter`, and never spend the nudge on a target
162
+ that already answered.
@@ -0,0 +1,50 @@
1
+ # Route to an agent already running
2
+
3
+ `SKILL.md` decides when to read this file: the work goes to a session that is
4
+ already running, by message, with no new pane.
5
+
6
+ Listed is not the same as reachable. Each row carries a `state` field. You
7
+ cannot message a row that is not in a live or working state, even though the
8
+ listing shows it.
9
+
10
+ If a session is already working in this worktree or repository, weigh it and
11
+ propose it. Ask the user to confirm a target whose working directory sits
12
+ outside this repository, unless the user named that target. *Routing to an agent
13
+ already running*, below, gives the way to propose the work to such a session.
14
+
15
+ **Agent already running** – one reached by cross-session message. It runs the
16
+ same command.
17
+
18
+ A reply through the agent-messaging tool is no longer required. A reply that begins ACCEPT or
19
+ DECLINE is the recognised fallback:
20
+
21
+ > Before any other tool call, acknowledge this handoff by running ~/.claude/acstack/bin/acstack handoff ack accept --file <handoff-file> "<one-line reason>", or ack decline --file <handoff-file> "<one-line reason>". You may read <handoff-file> first to judge whether the work suits you, and nothing else until the command has run – the sender watches for the file it writes and cannot report this work as yours until it exists. A reply through the agent-messaging tool beginning ACCEPT or DECLINE is a recognised fallback, but the command is what this handoff expects. Then continue that work.
22
+
23
+ ## Routing to an agent already running
24
+
25
+ Message that session by name. *Propose* the work, and point at the handoff file.
26
+ Do not assign it, and do not assume the target accepted it.
27
+
28
+ Open the message with the **agent already running** variant of the
29
+ acknowledgement instruction above. That variant asks for the ack command, and
30
+ names a reply beginning ACCEPT or DECLINE as the fallback.
31
+
32
+ Generate a short token – a random string that appears nowhere else, say
33
+ `ack-7f3a91c4` – and include it in the message text. The token still belongs in
34
+ the message, even though the acknowledgement no longer travels back over this
35
+ channel.
36
+
37
+ The watcher anchors on it, so counting starts when the target **receives** the
38
+ message, not when this session sends it. The target's queue absorbs however long
39
+ the target was busy. That is why a single number of turns serves both a running
40
+ agent and a fresh launch.
41
+
42
+ Generate the token fresh for every attempt – at least 8 random characters, and
43
+ never reused on a retry. The watcher matches it by substring, and anchors on the
44
+ FIRST match.
45
+
46
+ So a reused token anchors on the ORIGINAL delivery, where the turns have already
47
+ elapsed. It then reports an instant silence that never happened.
48
+
49
+ The nudge in *Nudge once, then watch again*, in `outcomes.md`, is one of those attempts,
50
+ and carries a fresh token of its own.
@@ -0,0 +1,115 @@
1
+ # Watch for the acknowledgement – detail
2
+
3
+ Read this file before any launch, and before you run the watcher. It holds the full flag reference and where each flag's value comes from.
4
+
5
+ ## Why a command, not a word
6
+
7
+ *The command records the decision. The typed word stays a convention.*
8
+
9
+ Cross-session messaging has no accept-or-decline mechanism. A peer can read a
10
+ message and simply ignore it, and nobody tells the sender.
11
+
12
+ So the receiver acknowledges by running a command.
13
+ `acstack handoff ack accept|decline` writes a sidecar file at
14
+ `<handoff-file>.ack`, beside the handoff file in the temp directory. The watcher
15
+ polls for that path.
16
+
17
+ The sidecar makes the decision a recorded fact, rather than a phrase the sender
18
+ must recognise.
19
+
20
+ A typed ACCEPT or DECLINE in the transcript still counts. The watcher reports
21
+ that fallback as `ack.via: 'text'`. It stays a convention an agent can drift
22
+ from.
23
+
24
+ The handoff file's first line, set out in `SKILL.md`, repeats the
25
+ acknowledgement requirement.
26
+
27
+ Both routes acknowledge with the same command, so this line reads the same on
28
+ both. The receiving agent may read this file before it decides, so the file is
29
+ the second place the requirement lands. The launch prompt is the first.
30
+
31
+ ## The flags
32
+
33
+ ```bash
34
+ # --file <handoff-file> names the handoff file. The watcher polls its sidecar,
35
+ # at <handoff-file>.ack. Always pass it, as an absolute path.
36
+ # --session-id with --cwd locates the target's transcript; --transcript <path>
37
+ # does it directly when the path is already known.
38
+ # --token anchors on delivery of our message. --turns: workflow.handoff.ack_turns,
39
+ # default 5. --poll-interval <ms> (default 1000) is how often the transcript is
40
+ # re-read. --timeout <ms> (default 1800000) is the watcher's own deadline –
41
+ # its expiry is the source of the `timeout` outcome.
42
+ # --nudged marks a restart after the one nudge – see outcomes.md.
43
+ # --ack-after <epoch-ms> moves the sidecar cut-off back. Only a token-route
44
+ # nudge restart passes it – see outcomes.md. The default is right
45
+ # everywhere else.
46
+ ~/.claude/acstack/bin/acstack handoff watch --file <handoff-file> \
47
+ --session-id <id> --cwd <dir> --token <token> --turns <n>
48
+ ```
49
+
50
+ The watcher reads two sources. The sidecar `<handoff-file>.ack` holds the
51
+ acknowledgement the ack command writes, and the watcher reports it as
52
+ `ack.via: 'cli'`. The transcript is the fallback, reported as `ack.via: 'text'`
53
+ – a message through the agent-messaging tool (`SendMessage` on Claude Code)
54
+ beginning ACCEPT or DECLINE, or, for a fresh agent, a reply beginning with it.
55
+
56
+ The word has to lead there. The watcher does not match a decision buried
57
+ mid-sentence.
58
+
59
+ `--file` must be an absolute path here, the same rule `--cwd` follows, and the
60
+ watcher exits 1 on a relative one. The sender types this path against another
61
+ session's world, where "relative to here" names a different file. The
62
+ receiver's `handoff ack --file` is the lenient half of the pair – it takes a
63
+ relative path and resolves it against its own working directory.
64
+
65
+ The two sources differ in when they count. The transcript stops counting once
66
+ the target spends the turn budget, because the watch has resolved by then.
67
+
68
+ The sidecar needs no anchor at all. The watcher reads it first on every tick, so
69
+ a target whose delivery record has not landed yet still acknowledges.
70
+
71
+ The transcript also shows whether the target is working, which separates
72
+ `engaged` from `silence`.
73
+
74
+ Every watch ignores any sidecar written before a cut-off. So a sidecar from an
75
+ earlier round never closes this watch, and nothing has to delete
76
+ `<handoff-file>.ack` between attempts. The watcher reports the cut-off it used
77
+ as `ackAfter`, and each route picks its own:
78
+
79
+ | Route | Cut-off |
80
+ | --------- | --------------------------------------------------- |
81
+ | `--token` | the moment this watch starts, reported as `startedAt` |
82
+ | `--fresh` | the launch, read off the first timestamped line of the target's transcript |
83
+
84
+ `--fresh` reaches back to the launch because the ack usually lands before the
85
+ watch. The sender has to recover the new session id before it can watch, while
86
+ the launch prompt tells the target to acknowledge before anything else.
87
+
88
+ So a well-behaved target answers inside that gap, roughly half a minute wide.
89
+
90
+ The launch cut-off still keeps an earlier round out. A relaunch after a decline
91
+ reuses the handoff file, and the first target's decline sidecar with it. The
92
+ relaunched agent's transcript begins after that decline.
93
+
94
+ `--ack-after <epoch-ms>` overrides both, for a restart that has to reach behind
95
+ its own start. Only the token-route nudge restart does – *Nudge once, then
96
+ watch again*, in `outcomes.md`.
97
+
98
+ An agent launched for this handoff leaves no delivery record. So pass `--fresh`
99
+ instead of `--token`, and counting starts at the first line of its transcript.
100
+ `claude agents --json` supplies `sessionId` and `cwd` for every live session,
101
+ and needs no terminal attached.
102
+
103
+ For a new worktree, `cwd` identifies the freshly launched session – it is the
104
+ new worktree's path.
105
+
106
+ For a new tab, `cwd` cannot, because this session shares that same cwd. So
107
+ snapshot `claude agents --json` before launching. The target is whichever
108
+ session id appears afterward and was absent from the snapshot.
109
+
110
+ The no-herdr `--bg` fallback needs no snapshot at all. This session launched it
111
+ with `--name`, so find the row carrying that name in `claude agents --json`, and
112
+ take its session id.
113
+
114
+ When a herdr `agent start` timed out, there is no agent name to look up. The
115
+ session id is the `agent_session.value` read from `herdr pane get <pane-id>`.