@joshuafolkken/kit 1.1387.0 → 1.1453.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 (278) hide show
  1. package/.claude/skills/epic-commands/SKILL.md +1 -1
  2. package/.claude/skills/epic-commands/epic-bundle.md +5 -0
  3. package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
  4. package/.claude/skills/workflow-commands/backlogrun-park.md +2 -0
  5. package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
  6. package/.claude/skills/workflow-commands/backlogrun.md +3 -2
  7. package/.claude/skills/workflow-commands/chain-rule.md +4 -2
  8. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  9. package/.claude/skills/workflow-commands/halfrun.md +1 -1
  10. package/.claude/skills/workflow-commands/issue-comments.md +6 -0
  11. package/.claude/skills/workflow-commands/kickoff.md +12 -13
  12. package/.claude/skills/workflow-commands/observation-filing.md +1 -1
  13. package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
  14. package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
  15. package/.claude/skills/workflow-commands/prerequisite.md +6 -0
  16. package/.claude/skills/workflow-commands/retrospective.md +5 -5
  17. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  18. package/README.md +27 -4
  19. package/dist/hooks/audit-provision.js +2 -2
  20. package/dist/hooks/batch-guard.js +7 -7
  21. package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
  22. package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
  23. package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
  24. package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
  25. package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
  26. package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
  27. package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
  28. package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
  29. package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
  30. package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
  31. package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
  32. package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
  33. package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
  34. package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
  35. package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
  36. package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
  37. package/dist/hooks/codex-hook-adapter.js +15 -15
  38. package/dist/hooks/duplicate-read-guard.js +8 -8
  39. package/dist/hooks/format-edited.js +5 -5
  40. package/dist/hooks/inputs.json +261 -201
  41. package/dist/hooks/investigation-guard.js +8 -8
  42. package/dist/hooks/pretool-guard.js +14 -14
  43. package/dist/hooks/session-lang.js +69 -16
  44. package/dist/hooks/stop-guard.js +13 -38
  45. package/dist/josh.js +95 -19
  46. package/package.json +4 -3
  47. package/prompts/collaboration-workflow/issue-template.md +10 -8
  48. package/prompts/review.md +30 -4
  49. package/scripts/agent/claude-agent-argv.ts +13 -1
  50. package/scripts/auth/auth-setup-cli.ts +101 -0
  51. package/scripts/auth/claude-token-output.ts +78 -0
  52. package/scripts/auth/claude-token.ts +68 -0
  53. package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
  54. package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
  55. package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
  56. package/scripts/auto-ok/auto-ok-judge.ts +67 -0
  57. package/scripts/backlog/backlog-drive-cli.ts +14 -0
  58. package/scripts/backlog/backlog-drive-epic.ts +24 -8
  59. package/scripts/backlog/backlog-drive-launch.ts +3 -0
  60. package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
  61. package/scripts/backlog/backlog-drive-named.ts +4 -2
  62. package/scripts/backlog/backlog-drive-unread.ts +56 -0
  63. package/scripts/backlog/backlog-drive.ts +38 -11
  64. package/scripts/backlog/backlog-overlap.ts +22 -5
  65. package/scripts/backlog/backlog-ready.ts +1 -3
  66. package/scripts/backlog/backlogrun-args.ts +13 -2
  67. package/scripts/backlog/backlogrun-cli.ts +91 -19
  68. package/scripts/backlog/backlogrun-launch.ts +24 -0
  69. package/scripts/backlog/backlogrun-replace.ts +166 -0
  70. package/scripts/backlog/backlogrun-stop.ts +178 -0
  71. package/scripts/behavior/behavior-cli.ts +6 -1
  72. package/scripts/doctor/doctor-io.ts +60 -9
  73. package/scripts/doctor/doctor.ts +20 -1
  74. package/scripts/document/document-byte-budget.ts +3 -3
  75. package/scripts/epic/epic-bundle-cli.ts +8 -8
  76. package/scripts/epic/epic-bundle-referenced.ts +2 -5
  77. package/scripts/epic/epic-bundle.ts +39 -4
  78. package/scripts/exports/unused-members-cli.ts +2 -1
  79. package/scripts/followup/followup-checks-event.ts +82 -0
  80. package/scripts/followup/git-followup-finish.ts +16 -2
  81. package/scripts/followup/git-followup-workflow.ts +5 -2
  82. package/scripts/followup/git-pr-followup-checks.ts +18 -4
  83. package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
  84. package/scripts/followup/git-pr-followup.ts +4 -1
  85. package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
  86. package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
  87. package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
  88. package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
  89. package/scripts/fresh-machine/fresh-machine-patterns.ts +175 -0
  90. package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
  91. package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
  92. package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
  93. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
  94. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
  95. package/scripts/gate/gate-report.ts +70 -15
  96. package/scripts/gate/project-checks.ts +18 -4
  97. package/scripts/gate/scoped-green.ts +2 -1
  98. package/scripts/gate/verification-gate.ts +4 -3
  99. package/scripts/gh/gh-author-association.ts +14 -0
  100. package/scripts/gh/git-gh-conditional.ts +66 -0
  101. package/scripts/gh/git-gh-exec.ts +1 -1
  102. package/scripts/gh/git-gh-issue-read.ts +6 -3
  103. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  104. package/scripts/gh/git-gh-issue-write.ts +5 -1
  105. package/scripts/gh/git-gh-issue.ts +13 -1
  106. package/scripts/gh/git-pr-checks-eval.ts +13 -0
  107. package/scripts/gh/git-pr-checks-parse.ts +8 -1
  108. package/scripts/gh/git-pr-checks-watch.ts +11 -4
  109. package/scripts/gh/git-pr-checks.ts +36 -2
  110. package/scripts/git/git-command.ts +16 -4
  111. package/scripts/git/git-schemas.ts +1 -0
  112. package/scripts/git/git-spawn-sync.ts +1 -1
  113. package/scripts/hooks/pretool-guard.ts +3 -1
  114. package/scripts/init/hook-command-rewrite.ts +31 -0
  115. package/scripts/init/init-actions.ts +7 -4
  116. package/scripts/init/init-ai-copy.ts +34 -11
  117. package/scripts/init/init-basic.ts +9 -4
  118. package/scripts/init/init-bootstrap.ts +6 -2
  119. package/scripts/init/init-dry-run-report.ts +150 -0
  120. package/scripts/init/init-dry-run.ts +189 -0
  121. package/scripts/init/init-install.ts +8 -1
  122. package/scripts/init/init-logic-toolchain.ts +76 -0
  123. package/scripts/init/init-logic.ts +2 -0
  124. package/scripts/init/init.ts +62 -42
  125. package/scripts/init/kit-setup-state.ts +11 -17
  126. package/scripts/init/own-formatter.ts +21 -0
  127. package/scripts/init/project-profile.ts +60 -12
  128. package/scripts/init/start-dry-run.ts +70 -0
  129. package/scripts/init/start-exec.ts +27 -1
  130. package/scripts/init/start-plan.ts +23 -3
  131. package/scripts/init/start-setup-pr.ts +13 -24
  132. package/scripts/init/start-steps.ts +62 -1
  133. package/scripts/init/start.ts +38 -17
  134. package/scripts/init/workflow-mark.ts +35 -0
  135. package/scripts/init/working-tree.ts +116 -0
  136. package/scripts/issue/issue-ack-cli.ts +84 -0
  137. package/scripts/issue/issue-auto-ok.ts +68 -99
  138. package/scripts/issue/issue-file-cli.ts +17 -17
  139. package/scripts/issue/issue-file.ts +1 -6
  140. package/scripts/issue/issue-labels.ts +46 -10
  141. package/scripts/issue/issue-release-cli.ts +69 -23
  142. package/scripts/issue/issue-release.ts +3 -1
  143. package/scripts/issue/issue-state-cli.ts +14 -4
  144. package/scripts/issue/issue-watch-diff.ts +152 -0
  145. package/scripts/issue/issue-watch-gate.ts +81 -0
  146. package/scripts/issue/issue-watch-hook.ts +54 -0
  147. package/scripts/issue/issue-watch.ts +240 -0
  148. package/scripts/issue/measure-rerun-cli.ts +3 -4
  149. package/scripts/josh/josh-command-map.ts +4 -0
  150. package/scripts/josh/josh-commands-ai.ts +11 -0
  151. package/scripts/josh/josh-commands-backlog.ts +2 -2
  152. package/scripts/josh/josh-commands-development.ts +1 -1
  153. package/scripts/josh/josh-commands-issue.ts +10 -3
  154. package/scripts/josh/josh-commands-lane.ts +11 -2
  155. package/scripts/josh/josh-commands-project.ts +26 -2
  156. package/scripts/josh/josh-verdict.ts +33 -4
  157. package/scripts/josh/process-identity.ts +32 -6
  158. package/scripts/lane/claude-lane-broker.ts +142 -0
  159. package/scripts/lane/lane-await.ts +6 -2
  160. package/scripts/lane/lane-broker-server.ts +69 -0
  161. package/scripts/lane/lane-cache.ts +37 -7
  162. package/scripts/lane/lane-capacity.ts +8 -6
  163. package/scripts/lane/lane-child-invocation.ts +1 -0
  164. package/scripts/lane/lane-cli.ts +15 -1
  165. package/scripts/lane/lane-close.ts +15 -6
  166. package/scripts/lane/lane-dispatch.ts +36 -10
  167. package/scripts/lane/lane-environment.ts +1 -1
  168. package/scripts/lane/lane-launch-cli.ts +13 -4
  169. package/scripts/lane/lane-ledger.ts +21 -3
  170. package/scripts/lane/lane-measure-cli.ts +10 -2
  171. package/scripts/lane/lane-open.ts +9 -1
  172. package/scripts/lane/lane-preserve.ts +49 -0
  173. package/scripts/lane/lane-reap.ts +20 -5
  174. package/scripts/lane/lane-registry.ts +6 -0
  175. package/scripts/lane/lane-restart-cli.ts +51 -0
  176. package/scripts/lane/lane-restart.ts +175 -0
  177. package/scripts/lane/lane-session-broker.ts +363 -0
  178. package/scripts/lane/lane-spent.ts +195 -0
  179. package/scripts/lane/lane-stage-stats.ts +27 -3
  180. package/scripts/lane/lane-stop-stats.ts +96 -0
  181. package/scripts/lane/openai-lane-supervisor.ts +2 -2
  182. package/scripts/lane/process-tools.ts +31 -0
  183. package/scripts/lib/buffered-process.ts +36 -2
  184. package/scripts/lib/pnpm-reach.ts +27 -0
  185. package/scripts/lib/status-icons.ts +5 -1
  186. package/scripts/lint/lint-parallel.ts +19 -2
  187. package/scripts/observations/observation-ledger-home.ts +1 -0
  188. package/scripts/observations/observation-ledger-migrate.ts +3 -15
  189. package/scripts/release/release-issue-close.ts +133 -0
  190. package/scripts/release/release-publish.ts +12 -1
  191. package/scripts/repo/repository-labels.ts +11 -1
  192. package/scripts/review/comment-ranges.ts +199 -0
  193. package/scripts/review/review-attest-cli.ts +21 -6
  194. package/scripts/review/review-brief-cli.ts +24 -7
  195. package/scripts/review/review-code-tree.ts +119 -0
  196. package/scripts/review/review-comment-round.ts +114 -0
  197. package/scripts/review/review-record-cli.ts +39 -17
  198. package/scripts/review/review-round2-cli.ts +16 -5
  199. package/scripts/review/review-round2.ts +30 -10
  200. package/scripts/review/review-stamps.ts +22 -1
  201. package/scripts/rules/implementation-cut.ts +3 -3
  202. package/scripts/rules/pre-gate-cut.ts +1 -1
  203. package/scripts/run/board/run-board-checks.ts +42 -0
  204. package/scripts/run/board/run-board-cli.ts +2 -0
  205. package/scripts/run/board/run-board-every.ts +3 -1
  206. package/scripts/run/board/run-board-fit.ts +16 -5
  207. package/scripts/run/board/run-board-fresh.ts +64 -0
  208. package/scripts/run/board/run-board-header.ts +129 -53
  209. package/scripts/run/board/run-board-labels.ts +47 -4
  210. package/scripts/run/board/run-board-lane-event.ts +66 -0
  211. package/scripts/run/board/run-board-layout.ts +57 -9
  212. package/scripts/run/board/run-board-now.ts +234 -0
  213. package/scripts/run/board/run-board-operation-read.ts +75 -0
  214. package/scripts/run/board/run-board-operation.ts +150 -0
  215. package/scripts/run/board/run-board-phase.ts +26 -1
  216. package/scripts/run/board/run-board-plan.ts +4 -6
  217. package/scripts/run/board/run-board-read.ts +49 -3
  218. package/scripts/run/board/run-board-reason.ts +124 -0
  219. package/scripts/run/board/run-board-render-checks.ts +55 -0
  220. package/scripts/run/board/run-board-render-legend.ts +12 -3
  221. package/scripts/run/board/run-board-render-slot.ts +2 -2
  222. package/scripts/run/board/run-board-render-tail.ts +128 -0
  223. package/scripts/run/board/run-board-render-thanks.ts +50 -0
  224. package/scripts/run/board/run-board-render-title.ts +58 -0
  225. package/scripts/run/board/run-board-render.ts +96 -80
  226. package/scripts/run/board/run-board-row-yield.ts +32 -0
  227. package/scripts/run/board/run-board-setup.ts +61 -13
  228. package/scripts/run/board/run-board-state.ts +10 -0
  229. package/scripts/run/board/run-board-status.ts +66 -7
  230. package/scripts/run/board/run-board-thanks-csv.ts +71 -0
  231. package/scripts/run/board/run-board-thanks-read.ts +112 -0
  232. package/scripts/run/board/run-board-thanks.ts +53 -0
  233. package/scripts/run/board/run-board-tick.ts +50 -21
  234. package/scripts/run/board/run-board-usual.ts +44 -0
  235. package/scripts/run/carry/run-carry-args.ts +9 -1
  236. package/scripts/run/carry/run-carry-cli.ts +2 -45
  237. package/scripts/run/carry/run-carry-end.ts +73 -0
  238. package/scripts/run/carry/run-carry.ts +8 -1
  239. package/scripts/run/entry/run-entry-cli.ts +5 -0
  240. package/scripts/run/event/run-event-checks.ts +54 -0
  241. package/scripts/run/event/run-event-render.ts +2 -0
  242. package/scripts/run/event/run-event-scope.ts +1 -0
  243. package/scripts/run/event/run-event-setup.ts +55 -11
  244. package/scripts/run/event/run-event-stream.ts +27 -11
  245. package/scripts/run/merge/run-merge-cli.ts +7 -4
  246. package/scripts/run/merge/run-merge-ship.ts +61 -0
  247. package/scripts/run/merge/run-merge-steps.ts +7 -14
  248. package/scripts/run/merge/run-merge-token.ts +16 -2
  249. package/scripts/run/run-headless.ts +1 -0
  250. package/scripts/run/run-step.ts +2 -0
  251. package/scripts/run/ship/run-ship-cli.ts +22 -2
  252. package/scripts/run/ship/run-ship-return.ts +31 -13
  253. package/scripts/run/ship/run-ship-review-steps.ts +17 -4
  254. package/scripts/run/ship/run-ship-stage.ts +1 -0
  255. package/scripts/run/wake/run-wake-await.ts +130 -0
  256. package/scripts/run/wake/run-wake-cli.ts +60 -71
  257. package/scripts/run/wake/run-wake-describe.ts +34 -2
  258. package/scripts/run/wake/run-wake-driver.ts +11 -4
  259. package/scripts/run/wake/run-wake-session.ts +20 -3
  260. package/scripts/run/wake/run-wake-stop.ts +37 -0
  261. package/scripts/run/wake/run-wake-supervise.ts +44 -0
  262. package/scripts/sync/claude-settings-merge.ts +144 -0
  263. package/scripts/sync/sync-ai-files.ts +28 -3
  264. package/scripts/sync/sync.ts +15 -0
  265. package/scripts/test/project-test-script.ts +73 -0
  266. package/scripts/test/skip-marker.ts +14 -1
  267. package/scripts/test/test-related.ts +3 -2
  268. package/scripts/test/test-unit-guard.ts +78 -14
  269. package/scripts/time-runtime/time-reported-failure.ts +11 -3
  270. package/scripts/uninstall/distributed-files.ts +92 -0
  271. package/scripts/uninstall/uninstall-plan.ts +119 -0
  272. package/scripts/uninstall/uninstall-report.ts +61 -0
  273. package/scripts/uninstall/uninstall.ts +206 -0
  274. package/templates/AGENTS.basic.md +4 -0
  275. package/templates/GEMINI.basic.md +6 -0
  276. package/dist/hooks/chunk-6V5OPIUU.js +0 -13
  277. package/scripts/lane/openai-review-broker.ts +0 -262
  278. package/scripts/run/board/run-board-track.ts +0 -22
@@ -0,0 +1,54 @@
1
+ import { issue_cite } from '#scripts/issue/issue-cite'
2
+
3
+ // The words of a `pr-checks` event (joshuafolkken/kit#3770): `josh followup` writes them and `run:board`
4
+ // reads them back, so the one spelling lives here. A line reads
5
+ // `#3754 PR 3765 pass 6 pending 2 fail 0 coderabbit running` — the issue first, as every event names
6
+ // it, so the stream's readers key it to the child as they key any other.
7
+
8
+ const CODERABBIT_STATES = ['running', 'done', 'rate-limited', 'failed', 'none'] as const
9
+
10
+ type CodeRabbitState = (typeof CODERABBIT_STATES)[number]
11
+
12
+ interface PrChecks {
13
+ pr: number
14
+ pass: number
15
+ pending: number
16
+ fail: number
17
+ coderabbit: CodeRabbitState
18
+ }
19
+
20
+ const CHECKS_PATTERN =
21
+ /^#\d+ PR (?<pr>\d+) pass (?<pass>\d+) pending (?<pending>\d+) fail (?<fail>\d+) coderabbit (?<coderabbit>\S+)$/u
22
+
23
+ function text_of(issue: string, checks: PrChecks): string {
24
+ const { pr, pass, pending, fail, coderabbit } = checks
25
+
26
+ return `${issue_cite.plain(issue)} PR ${String(pr)} pass ${String(pass)} pending ${String(pending)} fail ${String(fail)} coderabbit ${coderabbit}`
27
+ }
28
+
29
+ const STATE_NAMES: ReadonlySet<string> = new Set(CODERABBIT_STATES)
30
+
31
+ function is_coderabbit_state(value = ''): value is CodeRabbitState {
32
+ return STATE_NAMES.has(value)
33
+ }
34
+
35
+ // The checks a line carries, `undefined` for a line written in some other shape.
36
+ function checks_of(text: string): PrChecks | undefined {
37
+ const groups = CHECKS_PATTERN.exec(text)?.groups
38
+ const coderabbit = groups?.['coderabbit']
39
+
40
+ if (groups === undefined || !is_coderabbit_state(coderabbit)) return undefined
41
+
42
+ return {
43
+ pr: Number(groups['pr']),
44
+ pass: Number(groups['pass']),
45
+ pending: Number(groups['pending']),
46
+ fail: Number(groups['fail']),
47
+ coderabbit,
48
+ }
49
+ }
50
+
51
+ const run_event_checks = { checks_of, text_of }
52
+
53
+ export { run_event_checks }
54
+ export type { CodeRabbitState, PrChecks }
@@ -28,6 +28,7 @@ const KIND_LABELS: Readonly<Record<EventKind, KindLabel>> = {
28
28
  outage: { ja: '障害', en: 'outage' },
29
29
  cut: { ja: 'セッション交代', en: 'session cut' },
30
30
  resume: { ja: '実装を再開', en: 'implementation resumed' },
31
+ restart: { ja: '止めて出し直し', en: 'restarted' },
31
32
  drain: { ja: 'バックログが空', en: 'backlog drained' },
32
33
  stop: { ja: '停止', en: 'stopped' },
33
34
  'pr-opened': { ja: 'PR 作成', en: 'PR opened' },
@@ -45,6 +46,7 @@ const KIND_LABELS: Readonly<Record<EventKind, KindLabel>> = {
45
46
  add: { ja: '割り込み', en: 'added' },
46
47
  'lane-limit': { ja: 'レーン上限', en: 'lane limit' },
47
48
  setup: { ja: '準備', en: 'setup' },
49
+ 'pr-checks': { ja: 'PR のチェック', en: 'PR checks' },
48
50
  }
49
51
 
50
52
  const LABELS_BY_KIND: ReadonlyMap<string, KindLabel> = new Map(Object.entries(KIND_LABELS))
@@ -93,6 +93,7 @@ const KIND = run_event_stream.EVENT_KIND
93
93
  // so a lane child never reads one as its own.
94
94
  const BATCH_RECORD_KINDS: ReadonlySet<string> = new Set([
95
95
  KIND.CHILD_LAUNCH,
96
+ KIND.RESTART,
96
97
  KIND.PARK,
97
98
  KIND.SPLIT,
98
99
  KIND.RETROSPECTIVE,
@@ -3,10 +3,12 @@ import { error_text } from '#scripts/lib/error-message'
3
3
  import { run_event_stream } from './run-event-stream'
4
4
  import { run_event_stream_emit } from './run-event-stream-emit'
5
5
 
6
- // The stages a run passes between `josh backlogrun` and its first child, each written to the stream as
7
- // one `setup` event by the step that enters it. **The text is the stage's own words**: `run:board` draws
8
- // it as written, so the message a person reads and the `💓` beside it come from one event and cannot
9
- // disagree. Only a lane opening names an issue, which is how a reader tells it from the stages before it.
6
+ // The stages a run passes between `josh backlogrun` and its first child, and those of a later hand-back
7
+ // to a session, each written to the stream as one `setup` event by the step that enters it. **The text
8
+ // is the stage's own words**: `run:board` draws it as written, so the message a person reads and the
9
+ // `💓` beside it come from one event and cannot disagree. Only the stages of a lane's launch — opening
10
+ // it, installing into it, dispatching its child — name an issue, which is how a reader tells them from
11
+ // the stages before them.
10
12
 
11
13
  const STAGE = {
12
14
  // `josh backlogrun` started the parent session, which is reading its procedure.
@@ -15,33 +17,75 @@ const STAGE = {
15
17
  PLANNING: 'planning',
16
18
  // The supervisor took the run over from the parent and is asking for the first offer.
17
19
  HANDING_OFF: 'handing off',
20
+ // The driver answered something only a session can act on — an untriaged child, an epic left to a
21
+ // person — and the supervisor is waking one. The next hand-off replaces it.
22
+ JUDGING: 'waiting on judgment',
23
+ // The supervisor started that session, which is now acting on the driver's answer. It is detached, so
24
+ // its end is the supervisor's next hand-off, which replaces this.
25
+ SESSION: 'judgment session running',
26
+ // The driver's launch of a lane returned without a child: no lane was free to open, or a step after
27
+ // the open failed. It names no issue, so the lane's own stage ends here whatever the driver does next.
28
+ NO_CHILD: 'no child launched',
18
29
  } as const
19
30
 
20
31
  const OPENING = 'opening lane'
32
+ const INSTALLING = 'installing dependencies'
33
+ const DISPATCHING = 'dispatching'
34
+ // The stages of one lane's launch, in the order a lane passes them.
35
+ const LANE_STAGE = { OPENING, INSTALLING, DISPATCHING } as const
21
36
 
22
37
  // The driver is about to open a lane for `issue` — for every child, not only the first.
23
38
  function opening_lane(issue: string): string {
24
39
  return `${OPENING} ${issue_cite.plain(issue)}`
25
40
  }
26
41
 
42
+ // The lane's work tree exists and its dependencies are about to be installed into it.
43
+ function installing(issue: string): string {
44
+ return `${INSTALLING} ${issue_cite.plain(issue)}`
45
+ }
46
+
47
+ // The lane is ready and its child is about to be dispatched; the child's own `child-launch` follows.
48
+ function dispatching(issue: string): string {
49
+ return `${DISPATCHING} ${issue_cite.plain(issue)}`
50
+ }
51
+
27
52
  // Best-effort, as every append to this stream is: a stage that could not be written is a board that
28
53
  // says less, never a run that failed.
29
54
  async function emit(text: string): Promise<void> {
30
55
  await run_event_stream_emit.emit(run_event_stream.EVENT_KIND.SETUP, text)
31
56
  }
32
57
 
33
- // The same append for a caller that already holds the repository's common git directory and the moment
34
- // it wants the stage dated — the launcher, whose record and first stage carry one time.
35
- function emit_in(repository: string, text: string, at: Date): void {
58
+ // The same append to a stream the caller resolves, dated the moment it hands over.
59
+ function append_stage(resolve_target: () => string, text: string, at: Date): void {
36
60
  try {
37
- const target = run_event_stream.target_of(repository)
61
+ const { SETUP } = run_event_stream.EVENT_KIND
38
62
 
39
- run_event_stream.append(target, run_event_stream.EVENT_KIND.SETUP, text, at.toISOString())
63
+ run_event_stream.append(resolve_target(), SETUP, text, at.toISOString())
40
64
  } catch (error) {
41
- error_text.trace_swallowed('run_event_setup.emit_in', error)
65
+ error_text.trace_swallowed('run_event_setup.append_stage', error)
42
66
  }
43
67
  }
44
68
 
45
- const run_event_setup = { STAGE, emit, emit_in, opening_lane }
69
+ // For a caller that already holds the repository's common git directory and the moment it wants the
70
+ // stage dated — the launcher, whose record and first stage carry one time.
71
+ function emit_in(repository: string, text: string, at: Date): void {
72
+ append_stage(() => run_event_stream.target_of(repository), text, at)
73
+ }
74
+
75
+ // For a caller that already holds the stream's own path — the supervisor, which resolved it once.
76
+ function emit_to(target: string, text: string, at: Date): void {
77
+ append_stage(() => target, text, at)
78
+ }
79
+
80
+ const run_event_setup = {
81
+ LANE_STAGE,
82
+ STAGE,
83
+ dispatching,
84
+ emit,
85
+ emit_in,
86
+ emit_to,
87
+ installing,
88
+ opening_lane,
89
+ }
46
90
 
47
91
  export { run_event_setup }
@@ -60,6 +60,10 @@ const EVENT_KIND = {
60
60
  // resume clears the cut record, so without this the `cut` would stay the newest position and
61
61
  // `run:step` would keep answering the resume command it had just run.
62
62
  RESUME: 'resume',
63
+ // A person stopped a running lane child and re-dispatched its issue in a fresh session
64
+ // (`lane:restart`). It follows the fresh dispatch's `CHILD_LAUNCH`, so it is a launch to the parent and
65
+ // the board, and — like that launch — a batch record a lane child never reads as its own position.
66
+ RESTART: 'restart',
63
67
  // The backlog emptied while nothing of the run's own was in flight — the drain. It marks the position
64
68
  // at which the end-of-run retrospective is owed, *before* the idle watch opens, so `run:step` fires
65
69
  // the retrospective at the drain rather than after the watch runs its course. It is not itself a
@@ -126,10 +130,15 @@ const EVENT_KIND = {
126
130
  // already holds. A trace: it says the run gained room, not where the run is.
127
131
  LANE_LIMIT: 'lane-limit',
128
132
  // One stage of the run's start — the session starting, the plan, the hand-off to the supervisor, a
129
- // lane opening (`run-event-setup.ts`). `run:board` draws it until the first child launches, so the
133
+ // lane opening (`run-event-setup.ts`). `run:board` draws it while the run stands at it, so the
130
134
  // minutes before one read as work rather than as a run that has stopped. A trace: it says the run is
131
135
  // getting ready, not where the run is.
132
136
  SETUP: 'setup',
137
+ // A child's pull request checks as `josh followup` last polled them — the CI counts and CodeRabbit's
138
+ // state (`run-event-checks.ts`), written only when they change, so `run:board` draws a child waiting
139
+ // in followup without asking GitHub (joshuafolkken/kit#3770). A trace: it says how the checks stand,
140
+ // not where the run is.
141
+ PR_CHECKS: 'pr-checks',
133
142
  } as const
134
143
 
135
144
  type EventKind = (typeof EVENT_KIND)[keyof typeof EVENT_KIND]
@@ -148,17 +157,19 @@ const TRACE_KINDS: ReadonlySet<string> = new Set([
148
157
  EVENT_KIND.ADD,
149
158
  EVENT_KIND.LANE_LIMIT,
150
159
  EVENT_KIND.SETUP,
160
+ EVENT_KIND.PR_CHECKS,
151
161
  ])
152
162
 
153
163
  // The trace kinds that say only that the run was alive, so the bound rolls them off first. A `filed` or
154
164
  // `note` line is a trace to the position readers but is the board's record of what the run found, so
155
165
  // it rolls off only with the positions. A ship-stage `start` is disposable but goes last of them:
156
- // `run:board` draws from that line. A `setup` line is read only until the child it prepares launches,
157
- // so every one but the newest goes.
166
+ // `run:board` draws from that line. Only the newest `setup` line is the stage the run stands at,
167
+ // so every one but the newest goes; likewise only each child's newest `pr-checks` is drawn.
158
168
  const DISPOSABLE_KINDS: ReadonlySet<string> = new Set([
159
169
  EVENT_KIND.SHIP_STAGE,
160
170
  EVENT_KIND.HEARTBEAT,
161
171
  EVENT_KIND.SETUP,
172
+ EVENT_KIND.PR_CHECKS,
162
173
  ])
163
174
 
164
175
  const event_schema = z.object({
@@ -229,17 +240,18 @@ function last_position(events: ReadonlyArray<RunEvent>): number {
229
240
  return events[events.length - POSITION_INCREMENT]?.pos ?? FIRST_POSITION
230
241
  }
231
242
 
232
- // The issue a ship-stage names — `#<N> <stage> <phase>` carries it first.
243
+ // The issue a ship-stage or a `pr-checks` line names — `#<N> …` carries it first.
233
244
  function issue_of(event: RunEvent): string {
234
245
  return event.text.split(WORD_SEPARATOR)[FIRST_POSITION] ?? ''
235
246
  }
236
247
 
237
- // Each issue's newest ship-stage, the one the board reads its furthest phase from.
238
- function newest_stages(events: ReadonlyArray<RunEvent>): ReadonlySet<RunEvent> {
248
+ // Each issue's newest line of one kind: the ship-stage the board reads its furthest phase from, and the
249
+ // `pr-checks` it draws a followup row's end from.
250
+ function newest_by_issue(events: ReadonlyArray<RunEvent>, kind: string): ReadonlySet<RunEvent> {
239
251
  const newest = new Map<string, RunEvent>()
240
252
 
241
253
  for (const event of events) {
242
- if (event.kind === EVENT_KIND.SHIP_STAGE) newest.set(issue_of(event), event)
254
+ if (event.kind === kind) newest.set(issue_of(event), event)
243
255
  }
244
256
 
245
257
  return new Set(newest.values())
@@ -253,12 +265,16 @@ function is_stage_start(event: RunEvent): boolean {
253
265
  return event.text.split(WORD_SEPARATOR)[STAGE_PHASE_INDEX] === run_ship_stage.PHASE.START
254
266
  }
255
267
 
256
- // The disposable lines the bound never drops: each issue's newest ship-stage and the run's newest
257
- // `setup` stage, the ones `run:board` draws from now.
268
+ // The disposable lines the bound never drops: each issue's newest ship-stage and `pr-checks` and the
269
+ // run's newest `setup` stage, the ones `run:board` draws from now.
258
270
  function drawn_lines(events: ReadonlyArray<RunEvent>): ReadonlySet<RunEvent> {
259
271
  const setup = events.findLast((event) => event.kind === EVENT_KIND.SETUP)
260
272
 
261
- return new Set([...newest_stages(events), ...(setup === undefined ? [] : [setup])])
273
+ return new Set([
274
+ ...newest_by_issue(events, EVENT_KIND.SHIP_STAGE),
275
+ ...newest_by_issue(events, EVENT_KIND.PR_CHECKS),
276
+ ...(setup === undefined ? [] : [setup]),
277
+ ])
262
278
  }
263
279
 
264
280
  // The lines the bound may drop, in the order it drops them: the ones that draw nothing first, then the
@@ -282,7 +298,7 @@ function droppable(events: ReadonlyArray<RunEvent>): ReadonlyArray<RunEvent> {
282
298
  // **Each issue's newest ship-stage is not disposable**: it is the only record `run:board` draws a lane's
283
299
  // review-and-later phase from, and on a stream full of positions it would otherwise roll off in its own
284
300
  // append. **The newest `setup` stage is kept for the same reason**: it is the stage `run:board` draws
285
- // until the first child launches, and a run that starts on a full stream would otherwise show none.
301
+ // while the run stands at it, and a run that starts on a full stream would otherwise show none.
286
302
  //
287
303
  // **A ship-stage `start` outlasts the other disposable lines**: each one is a phase on the lane's track,
288
304
  // so it is kept past the `done` lines and the heartbeats.
@@ -25,7 +25,7 @@ import { run_merge_token, type MergeToken } from './run-merge-token'
25
25
  // The contract is `run:carry`'s: **standard output carries one token** (or the newline-separated
26
26
  // offer `epic:next --lanes` prints), and everything else is a captured subprocess or a quiet
27
27
  // in-process call. The verdicts this command adds to the offer are `over` (hand off), `human-review`
28
- // (stop — the child's own ending), `stop` (the consecutive-failure guard tripped) and `retry` (the
28
+ // (stop — the child's own ending), `stop` (the consecutive-failure guard tripped, or a failed child could not be parked) and `retry` (the
29
29
  // child's state could not be read; re-read before deciding).
30
30
 
31
31
  const ARGV_OFFSET = 2
@@ -163,13 +163,16 @@ function parse(argv: ReadonlyArray<string>): MergeContext | undefined {
163
163
  interface ControlVerdict {
164
164
  token: MergeToken
165
165
  code: number
166
+ is_offer: false
166
167
  }
167
168
 
168
169
  // The next child offered in the collected one's place: the text `backlog:next` or `epic:next --lanes`
169
- // printed, passed through as it was read, so no narrower type holds it.
170
+ // printed, passed through as it was read, so no narrower type holds it. `is_offer` keeps its `stop`
171
+ // apart from the control one (`run_merge_token.is_control_stop`).
170
172
  interface OfferVerdict {
171
173
  token: string
172
174
  code: number
175
+ is_offer: true
173
176
  }
174
177
 
175
178
  type MergeVerdict = ControlVerdict | OfferVerdict
@@ -179,11 +182,11 @@ type MergeVerdict = ControlVerdict | OfferVerdict
179
182
  type MergeResult = MergeVerdict & { outcome: ChildOutcome }
180
183
 
181
184
  function emit(token: MergeToken, code: number): ControlVerdict {
182
- return { token, code }
185
+ return { token, code, is_offer: false }
183
186
  }
184
187
 
185
188
  async function offered(ctx: MergeContext): Promise<OfferVerdict> {
186
- return { token: await run_merge_steps.ask_next(ctx), code: SUCCESS_EXIT_CODE }
189
+ return { token: await run_merge_steps.ask_next(ctx), code: SUCCESS_EXIT_CODE, is_offer: true }
187
190
  }
188
191
 
189
192
  // Whether the child's exit record shows it could not reach the API — read only when `--output` named
@@ -0,0 +1,61 @@
1
+ import { git_common_directory } from '#scripts/git/git-common-directory'
2
+ import { run_event_scope } from '#scripts/run/event/run-event-scope'
3
+ import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
4
+ import { run_ship_detach } from '#scripts/run/ship/run-ship-detach'
5
+
6
+ // Why `run:merge` parks a child whose detached ship has already ended (joshuafolkken/kit#3808).
7
+ //
8
+ // **A child that handed its region to `josh ship --detach` is parked for what its ship did.** A running
9
+ // ship is never parked (`merge_child` answers `shipping` first), so a park after a hand-off means the
10
+ // ship ended unmerged — and the session the ship relaunched to fix it ended too. The generic "session
11
+ // ended with its issue open" then hides the one fact a person needs: the stage the ship stopped at. The
12
+ // cause is that stage's `ship-stop` line, written by the supervisor since its own launch; a ship that
13
+ // ended without one (killed, or failed before any stage) is named by its result and its log instead.
14
+
15
+ type ShipEnding = ReturnType<typeof run_ship_detach.read_result>
16
+
17
+ const ENDED_RESULTS: ReadonlySet<string> = new Set(['failed', 'abnormal'])
18
+
19
+ type Events = ReadonlyArray<RunEvent>
20
+
21
+ // `child`'s events after its newest `ship-launch`, so an earlier launch's stop the relaunched session
22
+ // already fixed is never blamed for this ending. The record and the stream outlive a dispatch, so only a
23
+ // ship launched since the child's own newest dispatch is this attempt's: an earlier attempt's ended ship
24
+ // answers `undefined`, never this park.
25
+ function since_ship_launch(events: Events, child: string): Events | undefined {
26
+ const attempt = run_event_scope.since_own_launch(events, child)
27
+ const own = attempt.filter((event) => run_event_scope.issue_named(event) === child)
28
+ const launched_at = own.findLastIndex(
29
+ (event) => event.kind === run_event_stream.EVENT_KIND.SHIP_LAUNCH,
30
+ )
31
+
32
+ return launched_at === -1 ? undefined : own.slice(launched_at + 1)
33
+ }
34
+
35
+ function cause_of(ending: ShipEnding, events: Events, child: string): string | undefined {
36
+ if (ending === undefined || !ENDED_RESULTS.has(ending.result)) return undefined
37
+
38
+ const attempt = since_ship_launch(events, child)
39
+ if (attempt === undefined) return undefined
40
+
41
+ const stop = attempt.findLast((event) => event.kind === run_event_stream.EVENT_KIND.SHIP_STOP)
42
+
43
+ if (stop !== undefined) return `its detached ship stopped and was not fixed: ${stop.text}.`
44
+
45
+ return `its detached ship ended ${ending.result} before recording a stopped stage — pnpm josh ship --log ${child}.`
46
+ }
47
+
48
+ // The ship's cause for `child`'s park, read from the record and the stream this checkout's repository
49
+ // keys on, or `undefined` when the child handed nothing to a ship or its ship succeeded.
50
+ function cause(child: string, directory: string = process.cwd()): string | undefined {
51
+ const repository = git_common_directory.repository(directory)
52
+ if (repository === undefined) return undefined
53
+
54
+ const events = run_event_stream.read_events(run_event_stream.target_of(repository))
55
+
56
+ return cause_of(run_ship_detach.read_result(repository, child), events, child)
57
+ }
58
+
59
+ const run_merge_ship = { cause, cause_of }
60
+
61
+ export { run_merge_ship }
@@ -9,6 +9,7 @@ import { NEEDS_DECISION_LABEL } from '#scripts/issue/issue-labels'
9
9
  import { session_cite } from '#scripts/issue/session-cite'
10
10
  import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
11
11
  import { lane_close } from '#scripts/lane/lane-close'
12
+ import { lane_preserve } from '#scripts/lane/lane-preserve'
12
13
  import { lane_reap } from '#scripts/lane/lane-reap'
13
14
  import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
14
15
  import { lane_relaunch } from '#scripts/lane/lane-relaunch'
@@ -24,6 +25,7 @@ import { run_carry_conversation } from '#scripts/run/carry/run-carry-conversatio
24
25
  import { run_cut, type RunCut } from '#scripts/run/cut/run-cut'
25
26
  import { run_label } from '#scripts/run/run-label'
26
27
  import { run_merge } from './run-merge'
28
+ import { run_merge_ship } from './run-merge-ship'
27
29
 
28
30
  // The result of attempting to apply a carry change. Distinguishing `refused` from `applied` lets
29
31
  // callers surface the refusal as a hard failure rather than silently continuing on a carry that was
@@ -167,18 +169,8 @@ async function close_lane(child: string): Promise<void> {
167
169
  await josh(['lane:close', child])
168
170
  }
169
171
 
170
- function preserved_comment(message: string): string {
171
- return [
172
- 'The lane held uncommitted work when `run:merge` closed it, so it was stashed before the close:',
173
- `- recover with \`pnpm josh stash:pop "${message}"\``,
174
- ].join('\n')
175
- }
176
-
177
172
  async function stash_work(child: string, directory: string): Promise<void> {
178
- const message = git_stash.work_message(child, LANE_CLOSE_OCCASION)
179
-
180
- await git_stash.push(message, directory)
181
- await git_gh_issue_write.issue_try_comment(child, preserved_comment(message))
173
+ await lane_preserve.stash_work(child, directory, LANE_CLOSE_OCCASION, '`run:merge` closed it')
182
174
  }
183
175
 
184
176
  // Stash whatever the lane still holds uncommitted before its tree is removed: `lane:close` deletes by
@@ -344,13 +336,14 @@ async function park_failed(ctx: MergeContext, cause: string): Promise<FailedResu
344
336
  }
345
337
 
346
338
  // A child that ended unfinished: waiting when it records an open blocker (`do_waiting`), parked
347
- // otherwise (`park_failed`) — an order already recorded is never a decision for a person.
348
- async function do_failed(ctx: MergeContext, cause: string = FAILED_CAUSE): Promise<FailedResult> {
339
+ // otherwise (`park_failed`) — an order already recorded is never a decision for a person. Without a
340
+ // cause of the caller's, a child whose detached ship ended is parked for the ship's stop.
341
+ async function do_failed(ctx: MergeContext, cause?: string): Promise<FailedResult> {
349
342
  const blockers = await open_blockers(ctx.child)
350
343
 
351
344
  if (blockers.length > 0) return await do_waiting(ctx, blockers)
352
345
 
353
- return await park_failed(ctx, cause)
346
+ return await park_failed(ctx, cause ?? run_merge_ship.cause(ctx.child) ?? FAILED_CAUSE)
354
347
  }
355
348
 
356
349
  // The outcome of counting an outage: the record to read the streak against, and whether the count was
@@ -24,7 +24,21 @@ const MERGE_TOKEN = {
24
24
 
25
25
  type MergeToken = (typeof MERGE_TOKEN)[keyof typeof MERGE_TOKEN]
26
26
 
27
- const run_merge_token = { MERGE_TOKEN }
27
+ // Where the token came from: `run:merge`'s own control verdict, or the next offer (`backlog:next` /
28
+ // `epic:next`) passed through as it was read. The offer has a `stop` of its own — nothing ready to start
29
+ // is left — spelled like the control `stop`, so the source travels beside the token.
30
+ interface MergeTokenRead {
31
+ token: string
32
+ is_offer: boolean
33
+ }
28
34
 
29
- export type { MergeToken }
35
+ // The single reading of "this merge stopped the run": the control `stop` alone. An offer's `stop` is a
36
+ // parked backlog, which `backlog:budget` decides — a watch while the idle budget lasts — never the merge.
37
+ function is_control_stop(read: MergeTokenRead): boolean {
38
+ return !read.is_offer && read.token === MERGE_TOKEN.STOP
39
+ }
40
+
41
+ const run_merge_token = { MERGE_TOKEN, is_control_stop }
42
+
43
+ export type { MergeToken, MergeTokenRead }
30
44
  export { run_merge_token }
@@ -129,6 +129,7 @@ const run_headless = {
129
129
  is_backlog_parent,
130
130
  is_cut_capped,
131
131
  is_headless,
132
+ is_headless_parent,
132
133
  is_owned_here,
133
134
  must_keep_waiting,
134
135
  }
@@ -222,6 +222,8 @@ const FULL_INPUT_ACTIONS: Record<string, (input: StepInput) => StepAction> = {
222
222
  [KIND.STOP]: stop_action,
223
223
  [KIND.DRAIN]: drain_action,
224
224
  [KIND.CHILD_LAUNCH]: child_launch_action,
225
+ // A person's restart re-dispatched the child, so the parent's position is a launch's.
226
+ [KIND.RESTART]: child_launch_action,
225
227
  }
226
228
 
227
229
  // An event the table does not name leaves the position unknown rather than guessing a next step. A
@@ -274,6 +274,18 @@ async function open_context(args: ShipArguments): Promise<ShipContext> {
274
274
  return { target, done, state, started }
275
275
  }
276
276
 
277
+ // Every stop goes to the lane ledger — supervised, in an agent's own turn, or a lane child's preflight
278
+ // before the hand-off — so `lane:stats` can say how long a ship stayed stopped for each reason.
279
+ async function record_stop(
280
+ args: ShipArguments,
281
+ stage: Stage,
282
+ conflicts?: ReadonlyArray<string>,
283
+ ): Promise<void> {
284
+ const reason = run_ship_stage.stop_reason(stage, conflicts)
285
+
286
+ await lane_ledger.record_stop({ issue: Number(args.number), reason })
287
+ }
288
+
277
289
  // A detached supervisor hands the stopped stage back (`run-ship-return.ts`); a
278
290
  // ship in an agent's own turn has its report in front of that agent already. The `ship-stop` ends the
279
291
  // attempt, so the record's started stages go with it.
@@ -283,8 +295,11 @@ async function stopped(
283
295
  step: Step,
284
296
  context: ShipContext,
285
297
  ): Promise<ReadonlyArray<ShipSection>> {
298
+ const conflicts = sections.at(-1)?.conflicts
299
+
300
+ await record_stop(args, step.stage, conflicts)
301
+
286
302
  if (run_ship_detach.is_supervised()) {
287
- const conflicts = sections.at(-1)?.conflicts
288
303
  const resume = run_ship_next.resume_of(args)
289
304
 
290
305
  record(context.target, run_ship_stage.end_attempt)
@@ -371,7 +386,12 @@ function should_detach(args: ShipArguments): boolean {
371
386
  // supervisor's gate runs them again rather than passing over them.
372
387
  async function pre_detach_sections(args: ShipArguments): Promise<ReadonlyArray<ShipSection>> {
373
388
  const preflight = await run_stage(run_ship_steps.PREFLIGHT_STEP, args, await open_context(args))
374
- if (preflight.code !== SUCCESS_EXIT_CODE) return [preflight]
389
+
390
+ if (preflight.code !== SUCCESS_EXIT_CODE) {
391
+ await record_stop(args, run_ship_steps.PREFLIGHT_STEP.stage)
392
+
393
+ return [preflight]
394
+ }
375
395
 
376
396
  const result = await run_ship_pre_detach.checks()
377
397
 
@@ -1,8 +1,9 @@
1
1
  import { agent_role_profile } from '#scripts/agent/agent-role-profile'
2
- import { lane_child_invocation } from '#scripts/lane/lane-child-invocation'
2
+ import { lane_child_invocation, type ShipStop } from '#scripts/lane/lane-child-invocation'
3
3
  import { lane_child_marker } from '#scripts/lane/lane-child-marker'
4
4
  import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
5
5
  import { lane_relaunch } from '#scripts/lane/lane-relaunch'
6
+ import { lane_session_broker } from '#scripts/lane/lane-session-broker'
6
7
  import { run_event_stream } from '#scripts/run/event/run-event-stream'
7
8
  import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
8
9
  import type { ShipResume } from './run-ship-next'
@@ -49,6 +50,31 @@ function note(text: string): void {
49
50
  process.stderr.write(`${text}\n`)
50
51
  }
51
52
 
53
+ function relaunched(lane: LaneInfo, stop: ShipStop): ReturnOutcome {
54
+ const invocation = lane_child_invocation.ship_stop_invocation(lane.issue, stop)
55
+ const phase = agent_role_profile.IMPLEMENTATION_PHASE
56
+ const result = lane_relaunch.relaunch(lane, invocation, phase, note)
57
+
58
+ if (result.kind === 'launched') return 'relaunched'
59
+
60
+ note(`the lane child could not be relaunched: ${result.note}`)
61
+
62
+ return 'recorded'
63
+ }
64
+
65
+ // **A served lane's child is relaunched by its broker, not from here**: this supervisor descends from the
66
+ // child's Bash tool, which Claude Code starts without the token, so a child started here could not sign
67
+ // in where the token is the only credential (joshuafolkken/kit#3817).
68
+ async function brokered(lane: LaneInfo, stop: ShipStop): Promise<ReturnOutcome> {
69
+ if (await lane_session_broker.request(lane.directory, lane.issue, { kind: 'relaunch', stop })) {
70
+ return 'relaunched'
71
+ }
72
+
73
+ note('the lane child could not be relaunched: the lane broker did not start it')
74
+
75
+ return 'recorded'
76
+ }
77
+
52
78
  async function return_control(
53
79
  issue: string,
54
80
  stage: Stage,
@@ -63,19 +89,11 @@ async function return_control(
63
89
 
64
90
  if (lane === undefined) return 'recorded'
65
91
 
66
- const invocation = lane_child_invocation.ship_stop_invocation(issue, { stage, ...resume })
67
- const result = lane_relaunch.relaunch(
68
- lane,
69
- invocation,
70
- agent_role_profile.IMPLEMENTATION_PHASE,
71
- note,
72
- )
92
+ const stop = { stage, ...resume }
73
93
 
74
- if (result.kind === 'launched') return 'relaunched'
75
-
76
- note(`the lane child could not be relaunched: ${result.note}`)
77
-
78
- return 'recorded'
94
+ return lane_session_broker.is_serving(lane.directory, issue)
95
+ ? await brokered(lane, stop)
96
+ : relaunched(lane, stop)
79
97
  }
80
98
 
81
99
  const run_ship_return = { return_control }
@@ -4,7 +4,7 @@ import { PROJECT_ROOT } from '#scripts/init/init-paths'
4
4
  import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
5
5
  import { stamp_file } from '#scripts/josh/stamp-file'
6
6
  import { lane_child_marker } from '#scripts/lane/lane-child-marker'
7
- import { openai_review_broker } from '#scripts/lane/openai-review-broker'
7
+ import { lane_session_broker } from '#scripts/lane/lane-session-broker'
8
8
  import { review_record } from '#scripts/review/review-record'
9
9
  import { detached_launch } from '#scripts/run/detached-launch'
10
10
  import { run_ship_review, type RoundOutcome, type ScoredVerdict } from './run-ship-review'
@@ -66,7 +66,7 @@ function failure(out: string): JoshResult {
66
66
  }
67
67
 
68
68
  function paths(): { brief: string; findings: string; log: string } {
69
- return openai_review_broker.review_paths(PROJECT_ROOT)
69
+ return lane_session_broker.review_paths(PROJECT_ROOT)
70
70
  }
71
71
 
72
72
  async function josh(argv: ReadonlyArray<string>): Promise<JoshResult> {
@@ -94,6 +94,16 @@ async function run_direct_session(
94
94
  return { code: SUCCESS_EXIT_CODE, out: '' }
95
95
  }
96
96
 
97
+ // A lane child's reviewer is started by the lane's broker: an OpenAI lane's always, since a nested Codex
98
+ // cannot write outside its sandbox, and a Claude lane's whenever the `run:wake` supervisor serves it, since this
99
+ // ship was started from the child's Bash tool and so never received a token-only credential
100
+ // (joshuafolkken/kit#3817). A Claude lane nobody serves starts the reviewer itself, as before.
101
+ function is_brokered(profile: AgentProfile, issue: string): boolean {
102
+ if (!lane_child_marker.is_child_of(PROJECT_ROOT)) return false
103
+
104
+ return profile.provider === 'openai' || lane_session_broker.is_serving(PROJECT_ROOT, issue)
105
+ }
106
+
97
107
  async function run_session(
98
108
  prompt: string,
99
109
  log_path: string,
@@ -103,11 +113,14 @@ async function run_session(
103
113
  const built = agent_argv.resolve_in(prompt, agent_role_profile.REVIEWER, PROJECT_ROOT)
104
114
  if (built.kind === 'rejected') return failure(`reviewer profile rejected: ${built.note}`)
105
115
 
106
- if (built.profile.provider !== 'openai' || !lane_child_marker.is_child_of(PROJECT_ROOT)) {
116
+ if (!is_brokered(built.profile, issue)) {
107
117
  return await run_direct_session(built.argv, built.profile, log_path)
108
118
  }
109
119
 
110
- const is_success = await openai_review_broker.request(PROJECT_ROOT, issue, round)
120
+ const is_success = await lane_session_broker.request(PROJECT_ROOT, issue, {
121
+ kind: 'review',
122
+ round,
123
+ })
111
124
 
112
125
  return is_success ? { code: SUCCESS_EXIT_CODE, out: '' } : failure('isolated reviewer failed')
113
126
  }