@joshuafolkken/kit 1.1471.0 → 1.1514.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.
Files changed (267) hide show
  1. package/.claude/settings.json +6 -6
  2. package/.claude/skills/epic-commands/SKILL.md +2 -14
  3. package/.claude/skills/epic-commands/epic-bundle.md +9 -5
  4. package/.claude/skills/epic-commands/execution-waves.md +1 -2
  5. package/.claude/skills/verify-ui/SKILL.md +1 -2
  6. package/.claude/skills/workflow-commands/SKILL.md +7 -9
  7. package/.claude/skills/workflow-commands/background-commands.md +3 -7
  8. package/.claude/skills/workflow-commands/backlogrun-child.md +3 -10
  9. package/.claude/skills/workflow-commands/backlogrun-lanes.md +6 -15
  10. package/.claude/skills/workflow-commands/backlogrun-park.md +2 -8
  11. package/.claude/skills/workflow-commands/backlogrun-progress.md +10 -19
  12. package/.claude/skills/workflow-commands/backlogrun-recovery.md +8 -8
  13. package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -10
  14. package/.claude/skills/workflow-commands/backlogrun.md +31 -50
  15. package/.claude/skills/workflow-commands/chain-rule.md +7 -6
  16. package/.claude/skills/workflow-commands/delegation.md +12 -8
  17. package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
  18. package/.claude/skills/workflow-commands/followup.md +3 -6
  19. package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
  20. package/.claude/skills/workflow-commands/fullrun.md +4 -6
  21. package/.claude/skills/workflow-commands/halfrun.md +3 -7
  22. package/.claude/skills/workflow-commands/into-target.md +1 -2
  23. package/.claude/skills/workflow-commands/issue-comments.md +3 -3
  24. package/.claude/skills/workflow-commands/issue-scout.md +3 -2
  25. package/.claude/skills/workflow-commands/latest-gate.md +2 -6
  26. package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
  27. package/.claude/skills/workflow-commands/observation-filing.md +0 -14
  28. package/.claude/skills/workflow-commands/observation-ledger.md +4 -9
  29. package/.claude/skills/workflow-commands/pre-gate-cut.md +11 -16
  30. package/.claude/skills/workflow-commands/prerequisite.md +1 -2
  31. package/.claude/skills/workflow-commands/progress-watcher.md +2 -7
  32. package/.claude/skills/workflow-commands/prrun.md +3 -5
  33. package/.claude/skills/workflow-commands/retrospective.md +4 -4
  34. package/.claude/skills/workflow-commands/split-assessment.md +1 -3
  35. package/.claude/skills/workflow-commands/target-repository.md +1 -2
  36. package/.claude/skills/workflow-commands/working-tree-hold.md +2 -4
  37. package/.codex/hooks.json +6 -6
  38. package/.github/actions/setup-node/action.yml +1 -1
  39. package/.github/workflows/ci.yml +1 -1
  40. package/.github/workflows/publish.yml +2 -2
  41. package/CLAUDE.md +1 -1
  42. package/cspell/index.yaml +7 -0
  43. package/dist/CLAUDE.md +1 -1
  44. package/dist/commands/document-section-cli.js +67 -34
  45. package/dist/hooks/batch-guard.js +6 -6
  46. package/dist/hooks/chunk-4KKD2UTK.js +66 -0
  47. package/dist/hooks/{chunk-J47HU5H5.js → chunk-6TS2JD6D.js} +3 -3
  48. package/dist/hooks/chunk-7DSBJVFH.js +50 -0
  49. package/dist/hooks/{chunk-YHBZAJSV.js → chunk-A65Y4YOR.js} +3 -3
  50. package/dist/hooks/{chunk-E7OIKX5Z.js → chunk-APUO6U4J.js} +85 -7
  51. package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
  52. package/dist/hooks/chunk-GP4IEC4O.js +20 -0
  53. package/dist/hooks/{chunk-SHGPANFB.js → chunk-JP45SOCJ.js} +3 -3
  54. package/dist/hooks/{chunk-K4EOONYM.js → chunk-LAOV7NLM.js} +1 -1
  55. package/dist/hooks/chunk-M4HGZV7U.js +83 -0
  56. package/dist/hooks/{chunk-KC5AK2GF.js → chunk-O6GDRNJB.js} +3 -3
  57. package/dist/hooks/{chunk-YYPI7SJZ.js → chunk-PF2NZL2A.js} +5 -5
  58. package/dist/hooks/{chunk-H5HVWYXK.js → chunk-PTICX4OL.js} +3 -4
  59. package/dist/hooks/{chunk-IIGFYXKC.js → chunk-RT27QRJW.js} +581 -1438
  60. package/dist/hooks/{chunk-43ZHT22P.js → chunk-SYOTDYCI.js} +1127 -1089
  61. package/dist/hooks/{chunk-AS74OU3D.js → chunk-TNUMY6NL.js} +1507 -1336
  62. package/dist/hooks/chunk-UPUZF3AN.js +279 -0
  63. package/dist/hooks/{chunk-EUMACUK2.js → chunk-WK6SMBIC.js} +1948 -806
  64. package/dist/hooks/codex-hook-adapter.js +17 -14
  65. package/dist/hooks/duplicate-read-guard.js +7 -7
  66. package/dist/hooks/format-edited.js +5 -4
  67. package/dist/hooks/inputs.json +317 -264
  68. package/dist/hooks/investigation-guard.js +8 -11
  69. package/dist/hooks/pretool-guard.js +16 -13
  70. package/dist/hooks/session-lang.js +370 -25
  71. package/dist/hooks/stop-guard.js +2576 -176
  72. package/dist/josh.js +2124 -277
  73. package/lefthook/base.yml +5 -3
  74. package/package.json +12 -10
  75. package/prompts/collaboration-workflow/file-edits.md +1 -3
  76. package/prompts/collaboration-workflow/gh-rest.md +1 -2
  77. package/prompts/collaboration-workflow/issue-citation.md +2 -4
  78. package/prompts/collaboration-workflow/issue-template.md +10 -2
  79. package/prompts/collaboration-workflow/lane-instructions.md +35 -0
  80. package/prompts/collaboration-workflow/operating-rules.md +2 -6
  81. package/prompts/collaboration-workflow/output-bounds.md +3 -3
  82. package/prompts/collaboration-workflow/overview.md +1 -1
  83. package/prompts/collaboration-workflow/principles.md +14 -8
  84. package/prompts/collaboration-workflow/report-format.md +9 -0
  85. package/prompts/collaboration-workflow/residency.md +12 -8
  86. package/prompts/collaboration-workflow/rule-delivery.md +3 -11
  87. package/prompts/collaboration-workflow/shell-body.md +6 -6
  88. package/prompts/collaboration-workflow/turn-batching.md +5 -5
  89. package/prompts/collaboration-workflow/upstream-interrupt.md +2 -2
  90. package/prompts/collaboration-workflow/wip-cap.md +6 -6
  91. package/prompts/collaboration-workflow.md +1 -0
  92. package/prompts/review.md +4 -5
  93. package/prompts/testing-guide.md +2 -2
  94. package/scripts/agent/agent-argv.ts +20 -4
  95. package/scripts/agent/agent-role-profile.ts +101 -45
  96. package/scripts/agent/agent-safe-text.ts +60 -0
  97. package/scripts/agent/claude-agent-argv.ts +14 -0
  98. package/scripts/agent/codex-agent-argv.ts +2 -0
  99. package/scripts/agent/lane-instructions.ts +144 -0
  100. package/scripts/backlog/backlog-budget.ts +4 -4
  101. package/scripts/backlog/backlog-drive-named.ts +1 -1
  102. package/scripts/backlog/backlog-drive-restore.ts +9 -1
  103. package/scripts/backlog/backlog-occupied.ts +56 -0
  104. package/scripts/backlog/backlog-ready.ts +47 -10
  105. package/scripts/delegation/duplicate-read-guard.ts +2 -2
  106. package/scripts/delegation/investigation-guard.ts +15 -65
  107. package/scripts/delegation/investigation-reads.ts +0 -1
  108. package/scripts/document/document-byte-budget.ts +6 -5
  109. package/scripts/document/document-section-cli.ts +17 -5
  110. package/scripts/document/entry-read-budget.ts +2 -2
  111. package/scripts/document/read-set-cli.ts +4 -1
  112. package/scripts/epic/epic-audit-orphans.ts +5 -8
  113. package/scripts/epic/epic-audit.ts +52 -1
  114. package/scripts/epic/epic-bundle-evidence.ts +49 -26
  115. package/scripts/epic/epic-bundle-referenced.ts +14 -3
  116. package/scripts/epic/epic-bundle.ts +17 -9
  117. package/scripts/followup/git-followup-workflow.ts +19 -1
  118. package/scripts/gate/check-inputs.ts +129 -0
  119. package/scripts/gate/check-reuse.ts +162 -0
  120. package/scripts/gate/core-budget.ts +93 -44
  121. package/scripts/gate/core-wait.ts +130 -0
  122. package/scripts/gate/free-cores.ts +49 -0
  123. package/scripts/gate/gate-ledger.ts +17 -3
  124. package/scripts/gate/gate-plan.ts +23 -1
  125. package/scripts/gate/gate-report.ts +15 -2
  126. package/scripts/gate/gate-run-steps.ts +150 -0
  127. package/scripts/gate/held-mark.ts +55 -0
  128. package/scripts/gate/machine-capacity.ts +70 -11
  129. package/scripts/gate/pre-commit-type-check.ts +14 -1
  130. package/scripts/gate/verification-gate.ts +58 -125
  131. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  132. package/scripts/gh/git-gh-issue-write.ts +24 -2
  133. package/scripts/gh/git-pr.ts +13 -1
  134. package/scripts/git/main-merge-regenerate.ts +78 -0
  135. package/scripts/git/main-merge.ts +14 -5
  136. package/scripts/hooks/batch-guard.ts +5 -8
  137. package/scripts/hooks/pre-commit-lint.ts +91 -0
  138. package/scripts/hooks/pre-push-unit.ts +1 -1
  139. package/scripts/hooks/pretool-guard.ts +27 -52
  140. package/scripts/hooks/reserved-run.ts +1 -0
  141. package/scripts/hooks/run-hook.sh +10 -12
  142. package/scripts/hooks/stop-actions.ts +204 -0
  143. package/scripts/hooks/stop-guard.ts +62 -21
  144. package/scripts/init/hook-launch.ts +12 -7
  145. package/scripts/issue/expected-effect-lint.ts +59 -0
  146. package/scripts/issue/issue-cite-cli.ts +35 -15
  147. package/scripts/issue/issue-classification.ts +1 -1
  148. package/scripts/issue/issue-lint-cli.ts +5 -3
  149. package/scripts/issue/issue-scout-cli.ts +19 -14
  150. package/scripts/issue/markdown-section.ts +19 -5
  151. package/scripts/josh/command-usage.ts +110 -0
  152. package/scripts/josh/josh-command-map.ts +2 -0
  153. package/scripts/josh/josh-command-types.ts +55 -9
  154. package/scripts/josh/josh-commands-ai.ts +13 -0
  155. package/scripts/josh/josh-commands-development.ts +1 -1
  156. package/scripts/josh/josh-commands-hooks.ts +6 -0
  157. package/scripts/josh/josh-commands-lane.ts +10 -1
  158. package/scripts/josh/josh-commands-workflow.ts +5 -0
  159. package/scripts/josh/josh-logic.ts +6 -15
  160. package/scripts/josh/josh-usage-automation.ts +121 -0
  161. package/scripts/josh/josh-usage-backlog.ts +180 -0
  162. package/scripts/josh/josh-usage-developer.ts +230 -0
  163. package/scripts/josh/josh-usage-hooks.ts +38 -0
  164. package/scripts/josh/josh-usage-issue.ts +205 -0
  165. package/scripts/josh/josh-usage-review.ts +101 -0
  166. package/scripts/josh/josh-usage-run.ts +252 -0
  167. package/scripts/josh/josh-usage-session.ts +238 -0
  168. package/scripts/josh/josh-usage-workflow.ts +212 -0
  169. package/scripts/josh/josh.ts +14 -5
  170. package/scripts/josh/package-directory.ts +20 -0
  171. package/scripts/lane/lane-capacity.ts +8 -2
  172. package/scripts/lane/lane-check-stats.ts +56 -0
  173. package/scripts/lane/lane-child-invocation.ts +35 -4
  174. package/scripts/lane/lane-dispatch.ts +3 -3
  175. package/scripts/lane/lane-guard-policy.ts +2 -2
  176. package/scripts/lane/lane-handoff.ts +8 -1
  177. package/scripts/lane/lane-install.ts +12 -4
  178. package/scripts/lane/lane-ledger.ts +43 -1
  179. package/scripts/lane/lane-limit-cli.ts +8 -9
  180. package/scripts/lane/lane-measure-cli.ts +2 -0
  181. package/scripts/lane/lane-preserve.ts +29 -1
  182. package/scripts/lane/lane-registry.ts +9 -7
  183. package/scripts/lane/lane-restart.ts +5 -26
  184. package/scripts/lane/lane-session-action.ts +146 -0
  185. package/scripts/lane/lane-session-broker.ts +57 -138
  186. package/scripts/lane/lane-withdraw-cli.ts +71 -0
  187. package/scripts/lane/lane-withdraw.ts +182 -0
  188. package/scripts/lint/format.ts +4 -4
  189. package/scripts/lint/lint-parallel.ts +10 -3
  190. package/scripts/lint/lint-related-scope.ts +4 -1
  191. package/scripts/notify/telegram-test.ts +10 -5
  192. package/scripts/report/report-format-reference.ts +2 -0
  193. package/scripts/review/acceptance-map-check.ts +61 -0
  194. package/scripts/review/acceptance-map.ts +205 -0
  195. package/scripts/review/review-brief-cli.ts +1 -1
  196. package/scripts/rules/agent-model.ts +59 -0
  197. package/scripts/rules/delivered-rules.ts +11 -2
  198. package/scripts/rules/filing-cap.ts +1 -0
  199. package/scripts/rules/firing-point.ts +6 -5
  200. package/scripts/rules/guard-remedy.ts +117 -0
  201. package/scripts/rules/issue-comments.ts +67 -6
  202. package/scripts/rules/last-prompt.ts +50 -5
  203. package/scripts/rules/oracle-list-cli.ts +7 -0
  204. package/scripts/rules/piped-verification.ts +3 -1
  205. package/scripts/rules/rule-list.ts +4 -1
  206. package/scripts/rules/rule-registry-rows.ts +10 -0
  207. package/scripts/rules/rule-registry.ts +7 -4
  208. package/scripts/rules/rule-rewrite.ts +114 -0
  209. package/scripts/rules/rule-value.ts +7 -1
  210. package/scripts/rules/shell-segments.ts +15 -0
  211. package/scripts/rules/stop-decision.ts +77 -0
  212. package/scripts/rules/stop-rules.ts +147 -47
  213. package/scripts/rules/tail-commands.ts +15 -1
  214. package/scripts/run/board/run-board-cli.ts +2 -0
  215. package/scripts/run/board/run-board-closed.ts +8 -2
  216. package/scripts/run/board/run-board-github.ts +9 -1
  217. package/scripts/run/board/run-board-header.ts +4 -1
  218. package/scripts/run/board/run-board-labels.ts +2 -0
  219. package/scripts/run/board/run-board-layout.ts +1 -1
  220. package/scripts/run/board/run-board-read.ts +16 -0
  221. package/scripts/run/board/run-board-reason.ts +10 -1
  222. package/scripts/run/board/run-board-render-legend.ts +1 -0
  223. package/scripts/run/board/run-board-render-slot.ts +1 -1
  224. package/scripts/run/board/run-board-render-waits.ts +41 -0
  225. package/scripts/run/board/run-board-render.ts +6 -1
  226. package/scripts/run/board/run-board-row-yield.ts +1 -0
  227. package/scripts/run/board/run-board-state.ts +5 -0
  228. package/scripts/run/board/run-board-status.ts +72 -21
  229. package/scripts/run/board/run-board-tick.ts +5 -1
  230. package/scripts/run/cut/run-cut-cli.ts +3 -2
  231. package/scripts/run/cut/run-cut-report.ts +16 -0
  232. package/scripts/run/detached-launch.ts +23 -4
  233. package/scripts/run/entry/run-entry-cli.ts +8 -1
  234. package/scripts/run/event/run-event-render.ts +1 -0
  235. package/scripts/run/event/run-event-scope.ts +19 -1
  236. package/scripts/run/event/run-event-stream-emit.ts +13 -6
  237. package/scripts/run/event/run-event-stream.ts +4 -0
  238. package/scripts/run/hold/run-hold-session.ts +46 -0
  239. package/scripts/run/hold/run-hold.ts +7 -1
  240. package/scripts/run/launch-priority.ts +32 -0
  241. package/scripts/run/merge/run-merge-cli.ts +16 -65
  242. package/scripts/run/merge/run-merge-signals.ts +99 -0
  243. package/scripts/run/merge/run-merge-steps.ts +11 -1
  244. package/scripts/run/merge/run-merge.ts +21 -9
  245. package/scripts/run/run-liveness.ts +1 -1
  246. package/scripts/run/run-poll-hook.ts +157 -0
  247. package/scripts/run/run-step-cli.ts +22 -15
  248. package/scripts/run/run-step-section.ts +85 -0
  249. package/scripts/run/ship/run-ship-cli.ts +5 -12
  250. package/scripts/run/ship/run-ship-detach.ts +9 -0
  251. package/scripts/run/ship/run-ship-next.ts +10 -2
  252. package/scripts/run/ship/run-ship-overlap.ts +195 -0
  253. package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
  254. package/scripts/run/ship/run-ship-preflight.ts +28 -2
  255. package/scripts/run/ship/run-ship-return.ts +31 -7
  256. package/scripts/run/ship/run-ship-review-steps.ts +12 -10
  257. package/scripts/run/ship/run-ship-steps.ts +1 -1
  258. package/scripts/run/ship/run-ship-stop-context.ts +206 -0
  259. package/scripts/run/ship/run-ship-sync.ts +14 -6
  260. package/scripts/test/test-e2e-guard.ts +30 -5
  261. package/scripts/test/test-unit-guard.ts +16 -1
  262. package/scripts/test/unit-worker-share.ts +26 -4
  263. package/scripts/time-runtime/time-shell.ts +29 -4
  264. package/scripts/time-runtime/time-spans.ts +1 -0
  265. package/scripts/time-runtime/time-tool-call.ts +7 -0
  266. package/templates/gitignore +6 -2
  267. package/dist/hooks/chunk-K4LMBQC2.js +0 -68
package/prompts/review.md CHANGED
@@ -48,8 +48,7 @@ review, its severity tests and the round cap are unchanged.
48
48
  Run `pnpm josh review:brief --level-only` and use what it prints — `low` or `medium`, from the changed
49
49
  paths only, never "this one is small". `--staged` classifies the staged diff, `--json` adds the reason;
50
50
  inside a workflow `pnpm josh review:brief` prints the level on its first line. `low` gets 1 round,
51
- `medium` up to 2. Derivation (inert paths, documentation at `medium`):
52
- `docs/maintainers/review-history.md` → "The level is decided from the changed paths".
51
+ `medium` up to 2. A diff that touches documentation is not inert: it stays at `medium`.
53
52
 
54
53
  ```bash
55
54
  pnpm josh review:brief --level-only # the branch diff
@@ -94,8 +93,7 @@ deferred. Without the flag, and on any uncertainty (a missing round-1 snapshot i
94
93
  as code).
95
94
 
96
95
  One path outside that set makes it `required` — `.claude/**`, `CLAUDE.md`, a test, configuration, a
97
- code line, or a source file whose comment-free text cannot be read. Why: `docs/maintainers/review-history.md`
98
- → "The two skip arms".
96
+ code line, or a source file whose comment-free text cannot be read.
99
97
 
100
98
  Record a skipped round on the Issue:
101
99
 
@@ -129,7 +127,8 @@ Findings that reduce to one root judgement are filed as one Issue with a section
129
127
 
130
128
  Branch 2 only:
131
129
 
132
- - File a follow-up Issue referencing the current one; the current Issue completes.
130
+ - File a follow-up Issue whose `parent: #N` line names the current one — a mention in its background
131
+ is not read by `epic:bundle`; the current Issue completes.
133
132
  - An Issue in no epic is never offered by `epic:next`, so filing continues to the epic. Run these steps
134
133
  inside the CI wait: in `fullrun` / `backlogrun` after `pnpm josh git -y` and before
135
134
  `pnpm josh followup`; in `halfrun` or a standalone pre-commit self-review as soon as the disposition
@@ -50,7 +50,7 @@ Req N: ...
50
50
  | Component / browser | `*.svelte.test.ts` | browser/component vitest project |
51
51
  | E2E | `src/routes/**/*.e2e.ts` | Playwright |
52
52
 
53
- - **Never `*.spec.ts`** — lint-enforced by `eslint/rules/test-filename.js` (wired into `eslint/base.js`) for every JS/TS extension; rationale: `docs/maintainers/testing-guide-history.md` → "Why the `*.spec.*` ban is a lint rule".
53
+ - **Never `*.spec.ts`** — lint-enforced by `eslint/rules/test-filename.js` (wired into `eslint/base.js`) for every JS/TS extension.
54
54
  - **Keep the `.svelte.` infix** on component/browser tests: `*.svelte.test.ts` matches the browser project (`include: src/**/*.svelte.{test,spec}.{js,ts}`), plain `*.test.ts` the node project (`include: src/**/*.{test,spec}.{js,ts}`); dropping it silently moves the file to the wrong project.
55
55
  - **Colocate** each test beside its code (`foo.test.ts` next to `foo.ts`); no top-level `tests/` (lint-enforced by the same rule). E2E specs go under `src/routes/**` — enforced by review only, since `playwright.config.ts` `testMatch: '**/*.e2e.{ts,js}'` runs them anywhere.
56
56
 
@@ -148,7 +148,7 @@ Reset module state at the start of each test (`reset_praise_audio_index()` in `s
148
148
 
149
149
  ## 6. Closing the E2E gate without a human run
150
150
 
151
- No human E2E run closes the gate, and nothing a red local run stopped is let through (`docs/maintainers/testing-guide-history.md`).
151
+ No human E2E run closes the gate, and nothing a red local run stopped is let through.
152
152
 
153
153
  ### Which result closes the gate
154
154
 
@@ -4,6 +4,7 @@ import {
4
4
  type AgentEnvironment,
5
5
  type AgentProfile,
6
6
  type AgentRole,
7
+ type ProfileResult,
7
8
  } from './agent-role-profile'
8
9
  import { claude_agent_argv } from './claude-agent-argv'
9
10
  import { codex_agent_argv } from './codex-agent-argv'
@@ -11,6 +12,8 @@ import { codex_agent_argv } from './codex-agent-argv'
11
12
  interface AgentArgv {
12
13
  command: string
13
14
  args: ReadonlyArray<string>
15
+ // The invocation is the last element, so `detached-launch.ts` lets that one break lines.
16
+ ends_with_prompt: true
14
17
  }
15
18
 
16
19
  type AgentArgvResult =
@@ -112,22 +115,35 @@ function resume_argv(
112
115
  : with_profile_in(invocation, resolved.profile, cwd)
113
116
  }
114
117
 
118
+ function profiled_in(invocation: string, resolved: ProfileResult, cwd: string): AgentArgvResult {
119
+ return resolved.kind === 'rejected'
120
+ ? resolved
121
+ : with_profile_in(invocation, resolved.profile, cwd)
122
+ }
123
+
115
124
  function resolve_in(
116
125
  invocation: string,
117
126
  role: AgentRole,
118
127
  cwd: string,
119
128
  environment: AgentEnvironment = process.env,
120
129
  ): AgentArgvResult {
121
- const resolved = agent_role_profile.resolve(role, environment)
130
+ return profiled_in(invocation, agent_role_profile.resolve(role, environment), cwd)
131
+ }
122
132
 
123
- return resolved.kind === 'rejected'
124
- ? resolved
125
- : with_profile_in(invocation, resolved.profile, cwd)
133
+ // The reviewer for one review round, so a verification pass runs on its own profile.
134
+ function review_in(
135
+ invocation: string,
136
+ round: number,
137
+ cwd: string,
138
+ environment: AgentEnvironment = process.env,
139
+ ): AgentArgvResult {
140
+ return profiled_in(invocation, agent_role_profile.resolve_review(round, environment), cwd)
126
141
  }
127
142
 
128
143
  const agent_argv = {
129
144
  resolve,
130
145
  resolve_in,
146
+ review_in,
131
147
  resume_argv,
132
148
  with_profile,
133
149
  with_profile_in,
@@ -1,11 +1,8 @@
1
1
  import { agent_session_environment } from '#scripts/josh/agent-session-environment'
2
2
  import { z } from 'zod'
3
+ import { agent_safe_text } from './agent-safe-text'
3
4
 
4
- const FIRST_PRINTABLE_CODE = 0x20
5
- const DELETE_CODE = 0x7f
6
- const FIRST_CODE_POINT = 0
7
5
  const EMPTY_LENGTH = 0
8
- const MAX_VALUE_LENGTH = 4096
9
6
 
10
7
  const ROLE_SCHEMA = z.enum(['scheduler', 'worker', 'reviewer'])
11
8
  const PROVIDER_SCHEMA = z.enum(['anthropic', 'openai'])
@@ -32,6 +29,18 @@ type ProfileResult = { kind: 'profile'; profile: AgentProfile } | { kind: 'rejec
32
29
  type Rejected = Extract<ProfileResult, { kind: 'rejected' }>
33
30
  type EffortResult = Rejected | { kind: 'effort'; effort: AgentEffort }
34
31
  type ProviderResult = Rejected | { kind: 'provider'; provider: AgentProvider; is_default?: true }
32
+ type OverrideField = 'model' | 'effort'
33
+
34
+ interface Override {
35
+ key: string
36
+ value: string | undefined
37
+ }
38
+
39
+ // What a resolution is keyed on beyond the role: the run phase (effort) and the review round (profile).
40
+ interface AgentStage {
41
+ phase?: string | undefined
42
+ round?: number
43
+ }
35
44
 
36
45
  const SCHEDULER: AgentRole = 'scheduler'
37
46
  const WORKER: AgentRole = 'worker'
@@ -72,6 +81,22 @@ const OPENAI_PROFILES: Readonly<Record<AgentRole, AgentProfile>> = {
72
81
 
73
82
  const PROVIDER_PROFILES = { anthropic: DEFAULT_PROFILES, openai: OPENAI_PROFILES }
74
83
 
84
+ // **The verification pass resolves a profile of its own** (joshuafolkken/kit#3926). A round after the
85
+ // first only checks that the first round's findings were fixed, so its model and effort are set apart:
86
+ // its own keys win, then the reviewer's, then its own default.
87
+ const FIRST_REVIEW_ROUND = 1
88
+ // Sonnet holds the verification pass by the eval criterion: on every `verification-fix-*` scenario its
89
+ // pass/fail equals Opus's (100% agreement), and Opus passes all 4.
90
+ const VERIFICATION_MODEL = 'claude-sonnet-5-5'
91
+ const VERIFICATION_PROFILES: Readonly<Record<AgentProvider, AgentProfile>> = {
92
+ anthropic: { ...DEFAULT_PROFILES.reviewer, model: VERIFICATION_MODEL },
93
+ openai: OPENAI_PROFILES.reviewer,
94
+ }
95
+ const VERIFICATION_KEYS = {
96
+ model: 'JOSH_REVIEWER_VERIFICATION_MODEL',
97
+ effort: 'JOSH_REVIEWER_VERIFICATION_EFFORT',
98
+ }
99
+
75
100
  // **Effort as a function of the run phase, not the role alone**. For the first
76
101
  // merge only the mechanical ship/bookkeeping region is lowered: the pre-gate resume drives the gate,
77
102
  // commit, PR and merge, applying fixes the gate has already named — work delegation.md calls the opposite
@@ -93,19 +118,7 @@ const ENV_KEYS: Readonly<Record<AgentRole, { model: string; effort: string }>> =
93
118
 
94
119
  const LEGACY_WORKER_KEYS = { model: 'JOSH_LANE_MODEL', effort: 'JOSH_LANE_EFFORT' }
95
120
 
96
- function is_control_character(character: string): boolean {
97
- const code = character.codePointAt(FIRST_CODE_POINT) ?? FIRST_PRINTABLE_CODE
98
-
99
- return code < FIRST_PRINTABLE_CODE || code === DELETE_CODE
100
- }
101
-
102
- function is_safe_value(value: string): boolean {
103
- if (value.length === EMPTY_LENGTH || value.length > MAX_VALUE_LENGTH) return false
104
-
105
- for (const character of value) if (is_control_character(character)) return false
106
-
107
- return true
108
- }
121
+ const { MAX_VALUE_LENGTH, is_safe_prompt, is_safe_value, printable } = agent_safe_text
109
122
 
110
123
  function trimmed(value: string | undefined): string | undefined {
111
124
  const result = value?.trim()
@@ -113,44 +126,53 @@ function trimmed(value: string | undefined): string | undefined {
113
126
  return result === undefined || result.length === EMPTY_LENGTH ? undefined : result
114
127
  }
115
128
 
116
- function override_value(
129
+ function is_verification(role: AgentRole, round: number | undefined): boolean {
130
+ return role === REVIEWER && round !== undefined && round > FIRST_REVIEW_ROUND
131
+ }
132
+
133
+ // The keys an override is read from, first match wins: a worker falls back to its legacy lane keys, and
134
+ // a verification round reads its own keys ahead of the reviewer's.
135
+ function override_keys(
117
136
  role: AgentRole,
118
- field: 'model' | 'effort',
119
- environment: AgentEnvironment,
120
- ): { key: string; value: string | undefined } {
137
+ field: OverrideField,
138
+ round: number | undefined,
139
+ ): readonly [string, ...Array<string>] {
121
140
  const key = ENV_KEYS[role][field]
122
- const value = trimmed(environment[key])
123
- if (value !== undefined) return { key, value }
124
- if (role !== WORKER) return { key, value: undefined }
141
+ if (role === WORKER) return [key, LEGACY_WORKER_KEYS[field]]
125
142
 
126
- const legacy = LEGACY_WORKER_KEYS[field]
127
- const legacy_value = trimmed(environment[legacy])
143
+ return is_verification(role, round) ? [VERIFICATION_KEYS[field], key] : [key]
144
+ }
128
145
 
129
- return legacy_value === undefined
130
- ? { key, value: undefined }
131
- : { key: legacy, value: legacy_value }
146
+ function override_value(
147
+ role: AgentRole,
148
+ field: OverrideField,
149
+ environment: AgentEnvironment,
150
+ round?: number,
151
+ ): Override {
152
+ const keys = override_keys(role, field, round)
153
+ const key = keys.find((candidate) => trimmed(environment[candidate]) !== undefined)
154
+
155
+ return key === undefined
156
+ ? { key: keys[0], value: undefined }
157
+ : { key, value: trimmed(environment[key]) }
132
158
  }
133
159
 
134
160
  function model_override(
135
161
  role: AgentRole,
136
162
  environment: AgentEnvironment,
137
163
  provider: AgentProvider,
138
- ): { key: string; value: string | undefined } {
139
- const key = ENV_KEYS[role].model
140
-
164
+ round: number | undefined,
165
+ ): Override {
141
166
  return provider === OPENAI_PROVIDER
142
- ? { key, value: undefined }
143
- : override_value(role, 'model', environment)
167
+ ? { key: ENV_KEYS[role].model, value: undefined }
168
+ : override_value(role, 'model', environment, round)
144
169
  }
145
170
 
146
171
  function rejection(key: string, value: string, expected: string): Rejected {
147
172
  return { kind: 'rejected', note: `${key}=${value} ${expected}, so the agent was not started` }
148
173
  }
149
174
 
150
- function resolved_effort(
151
- override: { key: string; value: string | undefined },
152
- fallback: AgentEffort,
153
- ): EffortResult {
175
+ function resolved_effort(override: Override, fallback: AgentEffort): EffortResult {
154
176
  const parsed = EFFORT_SCHEMA.safeParse(override.value ?? fallback)
155
177
 
156
178
  return parsed.success
@@ -290,17 +312,27 @@ function with_phase_effort(
290
312
  return { ...profile, effort: overridden_effort(profile, phase, environment) }
291
313
  }
292
314
 
293
- function resolve(
315
+ function stage_defaults(
316
+ provider: AgentProvider,
294
317
  role: AgentRole,
295
- environment: AgentEnvironment = process.env,
296
- phase?: string,
318
+ round: number | undefined,
319
+ ): AgentProfile {
320
+ return is_verification(role, round)
321
+ ? VERIFICATION_PROFILES[provider]
322
+ : PROVIDER_PROFILES[provider][role]
323
+ }
324
+
325
+ function resolve_stage(
326
+ role: AgentRole,
327
+ environment: AgentEnvironment,
328
+ stage: AgentStage,
297
329
  ): ProfileResult {
298
330
  const selected = resolve_provider(environment)
299
331
  if (selected.kind === 'rejected') return selected
300
- const defaults = PROVIDER_PROFILES[selected.provider][role]
301
- const model = model_override(role, environment, selected.provider)
302
- const effort = override_value(role, 'effort', environment)
303
- const resolved = resolved_effort(effort, phase_effort(role, phase, defaults.effort))
332
+ const defaults = stage_defaults(selected.provider, role, stage.round)
333
+ const model = model_override(role, environment, selected.provider, stage.round)
334
+ const effort = override_value(role, 'effort', environment, stage.round)
335
+ const resolved = resolved_effort(effort, phase_effort(role, stage.phase, defaults.effort))
304
336
 
305
337
  if (resolved.kind === 'rejected') return resolved
306
338
 
@@ -314,10 +346,29 @@ function resolve(
314
346
  return validate(profile, model.key)
315
347
  }
316
348
 
349
+ function resolve(
350
+ role: AgentRole,
351
+ environment: AgentEnvironment = process.env,
352
+ phase?: string,
353
+ ): ProfileResult {
354
+ return resolve_stage(role, environment, { phase })
355
+ }
356
+
357
+ // The reviewer for one review round: round 1 as `resolve(REVIEWER)` does, any later round the
358
+ // verification pass's own profile.
359
+ function resolve_review(round: number, environment: AgentEnvironment = process.env): ProfileResult {
360
+ return resolve_stage(REVIEWER, environment, { round })
361
+ }
362
+
317
363
  function describe(profile: AgentProfile): string {
318
364
  return `provider=${profile.provider} role=${profile.role} model=${profile.model} effort=${profile.effort}`
319
365
  }
320
366
 
367
+ // The run-log line a review launch writes, so each round's model is on record beside its round.
368
+ function describe_review(round: number, profile: AgentProfile): string {
369
+ return `review round ${String(round)} · ${describe(profile)}`
370
+ }
371
+
321
372
  function parse(value: unknown): AgentProfile | undefined {
322
373
  const parsed = PROFILE_SCHEMA.safeParse(value)
323
374
 
@@ -335,15 +386,20 @@ const agent_role_profile = {
335
386
  PROFILE_SCHEMA,
336
387
  REVIEWER,
337
388
  SCHEDULER,
389
+ VERIFICATION_PROFILES,
338
390
  WORKER,
339
391
  default_notice,
340
392
  describe,
393
+ describe_review,
341
394
  handoff_environment,
342
395
  has_completion_callback,
396
+ is_safe_prompt,
343
397
  is_safe_value,
344
398
  parse,
399
+ printable,
345
400
  resolve,
346
401
  resolve_provider,
402
+ resolve_review,
347
403
  warn_of_default,
348
404
  with_phase_effort,
349
405
  }
@@ -0,0 +1,60 @@
1
+ // The text an agent launch may carry: a model id, an effort and a prompt, refused when empty, over
2
+ // length or carrying a control character, so nothing a value holds can inject a further argument.
3
+
4
+ const FIRST_PRINTABLE_CODE = 0x20
5
+ const DELETE_CODE = 0x7f
6
+ const FIRST_CODE_POINT = 0
7
+ const EMPTY_LENGTH = 0
8
+ const MAX_VALUE_LENGTH = 4096
9
+ const LINE_FEED = '\n'
10
+ const SPACE = ' '
11
+
12
+ function is_control_character(character: string): boolean {
13
+ const code = character.codePointAt(FIRST_CODE_POINT) ?? FIRST_PRINTABLE_CODE
14
+
15
+ return code < FIRST_PRINTABLE_CODE || code === DELETE_CODE
16
+ }
17
+
18
+ function is_line_feed(character: string): boolean {
19
+ return character === LINE_FEED
20
+ }
21
+
22
+ function is_never_allowed(): boolean {
23
+ return false
24
+ }
25
+
26
+ function is_refused(character: string, is_allowed: (character: string) => boolean): boolean {
27
+ return is_control_character(character) && !is_allowed(character)
28
+ }
29
+
30
+ function is_safe_text(value: string, is_allowed: (character: string) => boolean): boolean {
31
+ if (value.length === EMPTY_LENGTH || value.length > MAX_VALUE_LENGTH) return false
32
+
33
+ for (const character of value) if (is_refused(character, is_allowed)) return false
34
+
35
+ return true
36
+ }
37
+
38
+ function is_safe_value(value: string): boolean {
39
+ return is_safe_text(value, is_never_allowed)
40
+ }
41
+
42
+ // **An agent's prompt is the one value a line break belongs in.** A ship-stop prompt carries the failed
43
+ // output's lines (joshuafolkken/kit#3964); every other control character is refused as in any value.
44
+ function is_safe_prompt(value: string): boolean {
45
+ return is_safe_text(value, is_line_feed)
46
+ }
47
+
48
+ function printable_character(character: string): string {
49
+ return is_refused(character, is_line_feed) ? SPACE : character
50
+ }
51
+
52
+ // Command output made fit for a prompt: every control character but a line break becomes a space, so
53
+ // the text passes `is_safe_prompt` whatever the command printed.
54
+ function printable(text: string): string {
55
+ return Array.from(text, printable_character).join('')
56
+ }
57
+
58
+ const agent_safe_text = { MAX_VALUE_LENGTH, is_safe_prompt, is_safe_value, printable }
59
+
60
+ export { agent_safe_text }
@@ -6,6 +6,7 @@ import {
6
6
  type AgentProfile,
7
7
  type AgentRole,
8
8
  } from './agent-role-profile'
9
+ import { lane_instructions } from './lane-instructions'
9
10
 
10
11
  const AGENT_COMMAND = 'claude'
11
12
  // **The permission mode is passed, not left to the checkout's `.claude/settings.json`.** Claude Code
@@ -63,6 +64,7 @@ const PROJECT_MCP_FILE = '.mcp.json'
63
64
  interface ClaudeArgv {
64
65
  command: string
65
66
  args: ReadonlyArray<string>
67
+ ends_with_prompt: true
66
68
  }
67
69
 
68
70
  type ClaudeArgvResult =
@@ -87,6 +89,14 @@ function tool_flags(cwd: string = process.cwd()): ReadonlyArray<string> {
87
89
  return [TOOLS_FLAG, LAUNCHED_TOOLS.join(','), ...mcp_flags]
88
90
  }
89
91
 
92
+ // **A lane child and a review session start from the lane instruction set** (`lane-instructions.ts`);
93
+ // the scheduler keeps `CLAUDE.md`, because it is the one that stops and asks a person.
94
+ function instruction_flags(profile: AgentProfile, cwd?: string): ReadonlyArray<string> {
95
+ if (profile.role === agent_role_profile.SCHEDULER) return []
96
+
97
+ return lane_instructions.flags(cwd)
98
+ }
99
+
90
100
  function build(
91
101
  invocation: string,
92
102
  profile: AgentProfile,
@@ -98,6 +108,7 @@ function build(
98
108
  args: [
99
109
  ...AGENT_FLAGS,
100
110
  ...tool_flags(cwd),
111
+ ...instruction_flags(profile, cwd),
101
112
  ...session_id_flags(session_id),
102
113
  MODEL_FLAG,
103
114
  profile.model,
@@ -105,6 +116,7 @@ function build(
105
116
  profile.effort,
106
117
  invocation,
107
118
  ],
119
+ ends_with_prompt: true,
108
120
  }
109
121
  }
110
122
 
@@ -121,6 +133,7 @@ function build_resume(
121
133
  args: [
122
134
  ...AGENT_FLAGS,
123
135
  ...tool_flags(cwd),
136
+ ...instruction_flags(profile, cwd),
124
137
  RESUME_FLAG,
125
138
  session_id,
126
139
  MODEL_FLAG,
@@ -129,6 +142,7 @@ function build_resume(
129
142
  profile.effort,
130
143
  invocation,
131
144
  ],
145
+ ends_with_prompt: true,
132
146
  }
133
147
  }
134
148
 
@@ -19,6 +19,7 @@ const LEDGER_DIRECTORY = path.dirname(OBSERVATION_LEDGER_DIRECTORY)
19
19
  interface CodexArgv {
20
20
  command: string
21
21
  args: ReadonlyArray<string>
22
+ ends_with_prompt: true
22
23
  }
23
24
 
24
25
  function effort_config(profile: AgentProfile): string {
@@ -91,6 +92,7 @@ function build(invocation: string, profile: AgentProfile, cwd?: string): CodexAr
91
92
  JSON_FLAG,
92
93
  invocation,
93
94
  ],
95
+ ends_with_prompt: true,
94
96
  }
95
97
  }
96
98
 
@@ -0,0 +1,144 @@
1
+ import path from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { cost_tokens } from '#scripts/cost-runtime/cost-tokens'
4
+ import { document_section } from '#scripts/document/document-section'
5
+ import { distributed_paths } from '#scripts/init/distributed-paths'
6
+ import { find_package_directory } from '#scripts/josh/package-directory'
7
+ import { stamp_file } from '#scripts/josh/stamp-file'
8
+
9
+ interface LaneSet {
10
+ text: string
11
+ rules_path: string
12
+ }
13
+
14
+ // **A headless lane child or review session starts from the lane instruction set, not `CLAUDE.md`.**
15
+ // `CLAUDE.md` carries rules only an interactive session can act on — consultation versus execution,
16
+ // explicit workflow invocation, stop notifications — and every request re-reads it. The lane document
17
+ // names the rules a lane does need by `CLAUDE.md` heading or bold label, and each `include` line is
18
+ // expanded from the launch checkout's own `CLAUDE.md` here, so the rule text keeps one source (#3929).
19
+ const LANE_DOCUMENT = 'prompts/collaboration-workflow/lane-instructions.md'
20
+ const RULES_FILE = 'CLAUDE.md'
21
+ // `AGENTS.md` only points at `CLAUDE.md`, and Claude Code loads it once `CLAUDE.md` is excluded.
22
+ const POINTER_FILE = 'AGENTS.md'
23
+ const INCLUDE_PATTERN = /^<!-- include: CLAUDE\.md → "(?<wanted>.+)" -->$/u
24
+ const PROMPT_FILE_FLAG = '--append-system-prompt-file'
25
+ const SETTINGS_FLAG = '--settings'
26
+ const PROMPT_FILE_PREFIX = 'josh-lane-instructions-'
27
+ const PROMPT_FILE_SUFFIX = '.md'
28
+ const PACKAGE_DIRECTORY = find_package_directory(path.dirname(fileURLToPath(import.meta.url)))
29
+
30
+ // An include that resolves to nothing is `undefined`, never an empty string, so a renamed heading
31
+ // fails the whole composition rather than silently dropping a rule.
32
+ function expand_line(line: string, rules: string): string | undefined {
33
+ const wanted = INCLUDE_PATTERN.exec(line)?.groups?.['wanted']
34
+
35
+ if (wanted === undefined) return line
36
+
37
+ return document_section.section(rules, wanted)?.text
38
+ }
39
+
40
+ function compose(lane_markdown: string, rules: string): string | undefined {
41
+ const expanded = lane_markdown.split('\n').map((line) => expand_line(line, rules))
42
+
43
+ if (expanded.includes(undefined)) return undefined
44
+
45
+ return expanded.join('\n')
46
+ }
47
+
48
+ // **Where the rules are: the checkout's `CLAUDE.md`, else the package copy it imports.** kit and a
49
+ // consumer holding a full copy keep them in `CLAUDE.md`; a consumer written by `josh init` imports
50
+ // them from the package, and its own `CLAUDE.md` — the bootstrap note and the project's additions —
51
+ // stays loaded, because only the file the set was composed from is excluded.
52
+ function rules_sources(cwd: string): ReadonlyArray<string> {
53
+ const imported = distributed_paths.CLAUDE_MD_IMPORT_LINE.replace(/^@/u, '')
54
+
55
+ return [path.join(cwd, RULES_FILE), path.join(cwd, imported)]
56
+ }
57
+
58
+ function composed_from(lane_markdown: string, rules_path: string): string | undefined {
59
+ const rules = document_section.read_optional(rules_path)
60
+
61
+ return rules === undefined ? undefined : compose(lane_markdown, rules)
62
+ }
63
+
64
+ function composed_set(cwd: string, package_directory: string): LaneSet | undefined {
65
+ const lane_markdown = document_section.read_optional(path.join(package_directory, LANE_DOCUMENT))
66
+
67
+ if (lane_markdown === undefined) return undefined
68
+
69
+ for (const rules_path of rules_sources(cwd)) {
70
+ const text = composed_from(lane_markdown, rules_path)
71
+
72
+ if (text !== undefined) return { text, rules_path }
73
+ }
74
+
75
+ return undefined
76
+ }
77
+
78
+ function composed_in(
79
+ cwd: string,
80
+ package_directory: string = PACKAGE_DIRECTORY,
81
+ ): string | undefined {
82
+ return composed_set(cwd, package_directory)?.text
83
+ }
84
+
85
+ // Named by its content and this account, so concurrent launches of the same text share one file and a
86
+ // changed `CLAUDE.md` never reuses a stale one. The name is predictable in a temp directory other
87
+ // accounts may write to, so an existing file is reused only when it is this account's own regular file
88
+ // holding exactly this text; anything else is replaced through `stamp_file`, which refuses another
89
+ // account's file and renames a new one into place whole. A file rather than the text itself, because
90
+ // the launcher refuses a line break in any argv element but the prompt.
91
+ function written(text: string): string | undefined {
92
+ const file_path = stamp_file.stamp_path(PROMPT_FILE_PREFIX, text, PROMPT_FILE_SUFFIX)
93
+
94
+ if (stamp_file.read_stamp_text(file_path) === text) return file_path
95
+
96
+ try {
97
+ return stamp_file.replace_text_stamp(file_path, text)
98
+ } catch {
99
+ return undefined
100
+ }
101
+ }
102
+
103
+ function excluded_settings(rules_path: string, cwd: string): string {
104
+ return JSON.stringify({ claudeMdExcludes: [rules_path, path.join(cwd, POINTER_FILE)] })
105
+ }
106
+
107
+ // No flags when the set cannot be composed or safely written: the child then loads `CLAUDE.md` as
108
+ // before, which costs tokens but never loses a rule.
109
+ function flags(cwd: string = process.cwd()): ReadonlyArray<string> {
110
+ const set = composed_set(cwd, PACKAGE_DIRECTORY)
111
+ const prompt_file = set === undefined ? undefined : written(set.text)
112
+
113
+ if (set === undefined || prompt_file === undefined) return []
114
+
115
+ return [PROMPT_FILE_FLAG, prompt_file, SETTINGS_FLAG, excluded_settings(set.rules_path, cwd)]
116
+ }
117
+
118
+ function tokens_of(text: string): string {
119
+ return cost_tokens.estimate(text).toLocaleString('en-US')
120
+ }
121
+
122
+ // `read:set lane-child`'s resident line: what each lane request carries in place of the rules file.
123
+ function rules_tokens(rules_path: string): string {
124
+ return tokens_of(document_section.read_optional(rules_path) ?? '')
125
+ }
126
+
127
+ function resident_line(cwd: string = process.cwd()): string {
128
+ const set = composed_set(cwd, PACKAGE_DIRECTORY)
129
+ const rules = rules_tokens(set?.rules_path ?? path.join(cwd, RULES_FILE))
130
+ const lane_tokens = set === undefined ? 'not composed' : `${tokens_of(set.text)} tok`
131
+
132
+ return ` resident: lane instructions ${lane_tokens} in place of ${RULES_FILE} ${rules} tok`
133
+ }
134
+
135
+ const lane_instructions = {
136
+ PROMPT_FILE_FLAG,
137
+ SETTINGS_FLAG,
138
+ compose,
139
+ composed_in,
140
+ flags,
141
+ resident_line,
142
+ }
143
+
144
+ export { lane_instructions }
@@ -42,7 +42,7 @@ const WATCH_VERDICT = 'watch'
42
42
  const STOP_VERDICT = 'stop'
43
43
  const TRIAGE_VERDICT = 'triage'
44
44
 
45
- // `run` — start what `backlog:next` offered. `watch` — sleep the polling interval and ask again,
45
+ // `run` — start what `backlog:next` offered. `watch` — sleep the re-ask floor and ask again,
46
46
  // except while something of the run's own is in flight, where the wake is the progress watcher's exit
47
47
  // and the interval is only a floor (`docs/maintainers/backlogrun-driver.md` → "Waiting while
48
48
  // something is in flight").
@@ -87,7 +87,7 @@ interface BudgetDecision {
87
87
  }
88
88
 
89
89
  const BLOCKED_REASON =
90
- 'Everything opted in is blocked or already running, so waiting can still change the answer. Poll again at the polling interval.'
90
+ 'Everything opted in is blocked or already running, so waiting can still change the answer. Ask again at the re-ask floor (60 s).'
91
91
 
92
92
  const TRIAGE_REASON =
93
93
  'An opted-in candidate carries neither `run:solo` nor `run:lane`, so nothing starts until it is judged. Read each issue named above, record its order and one of the two labels with a comment, then ask again.'
@@ -121,7 +121,7 @@ function max_reached_reason(merged: number, max_issues: number): string {
121
121
  }
122
122
 
123
123
  function draining_reason(running: number, reason: string): string {
124
- return `Start nothing more and poll until the running children merge (running: ${String(running)}). Then report and finish — ${reason}`
124
+ return `Start nothing more and wait on \`pnpm josh lane:await\` until the running children merge (running: ${String(running)}). Then report and finish — ${reason}`
125
125
  }
126
126
 
127
127
  function idle_expired_reason(answer: BacklogAnswer, idle_budget_ms: number): string {
@@ -140,7 +140,7 @@ function idle_watch_reason(answer: BacklogAnswer, left_ms: number): string {
140
140
  // **The watch runs down while they drain, and restarts at each one collected.** A collected child is
141
141
  // work, so it moves `active_at`; an ask that offered nothing never does.
142
142
  function running_watch_reason(answer: BacklogAnswer, running: number, left_ms: number): string {
143
- return `${waiting_on(answer)} and the idle watch has about ${to_minutes(left_ms)} minutes left, with ${String(running)} still running. Keep the working tree until they merge and poll at the polling interval — the watch is running down meanwhile.`
143
+ return `${waiting_on(answer)} and the idle watch has about ${to_minutes(left_ms)} minutes left, with ${String(running)} still running. Keep the working tree and wait on \`pnpm josh lane:await\` until they merge — the watch is running down meanwhile.`
144
144
  }
145
145
 
146
146
  function watching_reason(input: BudgetInput, left_ms: number): string {
@@ -12,7 +12,7 @@ const { MERGE_TOKEN } = run_merge_token
12
12
 
13
13
  const NO_RETRIES = 0
14
14
  const FIRST = 0
15
- const TERMINAL_OUTCOMES: ReadonlySet<string> = new Set(['merged', 'parked', 'split'])
15
+ const TERMINAL_OUTCOMES: ReadonlySet<string> = new Set(['merged', 'parked', 'split', 'withdrawn'])
16
16
 
17
17
  // The skip follows the declared order alone: an issue `run:add` put in was
18
18
  // asked for on its own, so its failure skips nothing and a declared failure does not skip it.