acstack 0.0.0-stage → 0.47.0
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.
- package/.claude-plugin/plugin.json +19 -0
- package/LICENSE +21 -0
- package/README.md +389 -2
- package/THIRD_PARTY_NOTICES +14 -0
- package/TROUBLESHOOTING.md +225 -0
- package/agents/codex/frontend-verifier.toml +352 -0
- package/agents/codex/task-checker.toml +345 -0
- package/agents/codex/task-executor.toml +157 -0
- package/agents/codex/test-writer.toml +149 -0
- package/agents/frontend-verifier.md +351 -0
- package/agents/task-checker.md +344 -0
- package/agents/task-executor.md +156 -0
- package/agents/test-writer.md +148 -0
- package/assets/playwriter-bridge/build-launcher.ps1 +32 -0
- package/assets/playwriter-bridge/doctor.ps1 +450 -0
- package/assets/playwriter-bridge/launcher-src.cs +131 -0
- package/assets/playwriter-bridge/opener.js +330 -0
- package/assets/playwriter-bridge/opener.ps1 +63 -0
- package/assets/playwriter-bridge/register-tasks.ps1 +328 -0
- package/assets/playwriter-bridge/relay.ps1 +50 -0
- package/assets/playwriter-bridge/stop.ps1 +125 -0
- package/assets/playwriter-bridge/sync.ps1 +271 -0
- package/assets/playwriter-bridge/tunnel.ps1 +325 -0
- package/assets/playwriter-bridge/vm-free-port.sh +166 -0
- package/assets/terminal-workspace/client-keybindings.md +87 -0
- package/assets/terminal-workspace/config.example.yaml +329 -0
- package/assets/terminal-workspace/setup.sh +4317 -0
- package/cli/README.md +47 -0
- package/cli/bin/acstack.js +3 -0
- package/cli/dist/index.js +25984 -0
- package/cli/dist/index.js.map +7 -0
- package/cli/package.json +59 -0
- package/docs/adr/0001-ste-writing-standard.md +16 -0
- package/docs/adr/0002-version-gate-not-auto-bump.md +35 -0
- package/docs/adr/0003-handoff-ack-command.md +19 -0
- package/docs/adr/0004-dev-setup-per-host.md +13 -0
- package/docs/adr/0005-two-orchestrators-per-host.md +25 -0
- package/docs/adr/0006-document-opener-service.md +89 -0
- package/docs/adr/0007-lead-check-reads-the-transcript.md +87 -0
- package/docs/adr/0008-cli-audits-skill-interviews.md +37 -0
- package/docs/adr/0009-verify-the-write-not-the-writer.md +51 -0
- package/docs/adr/0010-promote-dev-to-main-by-fast-forward.md +115 -0
- package/docs/adr/0011-node-header-is-the-edge-encoding.md +20 -0
- package/docs/adr/0012-node-kind-is-a-closed-set-of-five.md +31 -0
- package/docs/adr/0013-the-diagram-renderer-is-pure.md +23 -0
- package/docs/adr/0014-reading-a-node-hides-its-generated-diagram.md +29 -0
- package/docs/adr/0015-a-node-id-links-to-its-issue-through-an-anchor.md +37 -0
- package/docs/adr/0016-the-map-root-carries-no-status-of-its-own.md +21 -0
- package/docs/adr/0017-call-impeccable-copy-only-its-questions.md +13 -0
- package/docs/adr/index.md +17 -0
- package/docs/config-reference.md +572 -0
- package/docs/dev-setups.md +164 -0
- package/docs/migrate-0.8.md +28 -0
- package/docs/playwriter-bridge-windows.md +15 -0
- package/docs/terminal-workspace-keys.md +137 -0
- package/docs/terminal-workspace.md +1774 -0
- package/docs/workflows.md +309 -0
- package/hooks/ac-writing-style-reminder.sh +86 -0
- package/hooks/context-pressure.sh +583 -0
- package/hooks/hooks.json +50 -0
- package/hooks/session-start-handoff.sh +165 -0
- package/hooks/session-start.sh +214 -0
- package/orchestrator/AGENTS.md +532 -0
- package/output-styles/ac-writing-style.md +286 -0
- package/package.json +50 -4
- package/pi/agents/frontend-verifier.md +357 -0
- package/pi/agents/task-checker.md +348 -0
- package/pi/agents/task-executor.md +160 -0
- package/pi/agents/test-writer.md +154 -0
- package/pi/extension.ts +723 -0
- package/profiles/fullstack-python.yaml +29 -0
- package/profiles/node-typescript.yaml +23 -0
- package/profiles/python.yaml +24 -0
- package/skills/archive/SKILL.md +160 -0
- package/skills/bootstrap/SKILL.md +109 -0
- package/skills/bridge/HANDOFF.md +93 -0
- package/skills/bridge/SKILL-VM.md +292 -0
- package/skills/bridge/SKILL-WINDOWS.md +526 -0
- package/skills/bridge/SKILL.md +121 -0
- package/skills/browser-verify/SKILL.md +229 -0
- package/skills/code-review/SKILL.md +44 -0
- package/skills/commit/SKILL.md +106 -0
- package/skills/compact-and-continue/SKILL.md +241 -0
- package/skills/explore/SKILL.md +149 -0
- package/skills/grilling/SKILL.md +134 -0
- package/skills/handoff/SKILL.md +250 -0
- package/skills/handoff/launch-herdr.md +197 -0
- package/skills/handoff/launch-without-herdr.md +30 -0
- package/skills/handoff/lead-check.md +59 -0
- package/skills/handoff/outcomes.md +162 -0
- package/skills/handoff/running-agent.md +50 -0
- package/skills/handoff/watch.md +115 -0
- package/skills/host/SKILL.md +247 -0
- package/skills/host/detect-host.sh +297 -0
- package/skills/host/herdr.md +79 -0
- package/skills/host/install-herdr-block.sh +467 -0
- package/skills/host/install-orca.sh +512 -0
- package/skills/host/notes.md +97 -0
- package/skills/host/orca.md +160 -0
- package/skills/host/questions.md +163 -0
- package/skills/host/report.md +42 -0
- package/skills/implement/SKILL.md +230 -0
- package/skills/implement/decision-log.md +62 -0
- package/skills/implement/design-pass.md +32 -0
- package/skills/implement/relaxed-tdd.md +28 -0
- package/skills/map/SKILL.md +250 -0
- package/skills/map/frontend.md +227 -0
- package/skills/map/trackers/files.md +74 -0
- package/skills/map/trackers/github.md +180 -0
- package/skills/map/visuals.md +387 -0
- package/skills/new-worktree/SKILL.md +155 -0
- package/skills/new-worktree/detect-orchestrator.sh +328 -0
- package/skills/new-worktree/detect-orchestrator.test.sh +894 -0
- package/skills/new-worktree/herdr.md +84 -0
- package/skills/new-worktree/orca.md +101 -0
- package/skills/pre-commit-review/SKILL.md +242 -0
- package/skills/pre-commit-review/auto-fix.md +19 -0
- package/skills/pre-commit-review/interactive.md +35 -0
- package/skills/quick-fix/SKILL.md +125 -0
- package/skills/record-context/SKILL.md +106 -0
- package/skills/setup/SKILL.md +250 -0
- package/skills/setup/browser.md +123 -0
- package/skills/setup/design-review.md +97 -0
- package/skills/setup/first-run.md +114 -0
- package/skills/setup/report.md +68 -0
- package/skills/setup/row-fixes.md +196 -0
- package/skills/setup/terminal-workspace.md +153 -0
- package/skills/sync/SKILL.md +171 -0
- package/skills/teardown-worktree/SKILL.md +245 -0
- package/skills/teardown-worktree/detector.md +39 -0
- package/skills/teardown-worktree/herdr.md +113 -0
- package/skills/teardown-worktree/orca.md +61 -0
- package/skills/terminal-workspace/SKILL.md +249 -0
- package/skills/terminal-workspace/components.md +36 -0
- package/skills/terminal-workspace/config-choices.md +69 -0
- package/skills/terminal-workspace/emulator-keys.md +48 -0
- package/skills/terminal-workspace/headless-clipboard.md +175 -0
- package/skills/terminal-workspace/turn-off-and-update.md +104 -0
- package/skills/test-conventions/SKILL.md +59 -0
- package/skills/visual-docs/SKILL.md +246 -0
- package/skills/writing/SKILL.md +231 -0
- package/skills/writing/examples.md +153 -0
- 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>`.
|