@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
@@ -8,7 +8,6 @@ import { run_event_stream } from '#scripts/run/event/run-event-stream'
8
8
  import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
9
9
  import { run_ship, type ShipSection } from './run-ship'
10
10
  import { run_ship_detach } from './run-ship-detach'
11
- import { run_ship_next } from './run-ship-next'
12
11
  import { run_ship_pre_detach } from './run-ship-pre-detach'
13
12
  import { run_ship_probe } from './run-ship-probe'
14
13
  import { run_ship_return } from './run-ship-return'
@@ -295,19 +294,13 @@ async function stopped(
295
294
  step: Step,
296
295
  context: ShipContext,
297
296
  ): Promise<ReadonlyArray<ShipSection>> {
298
- const conflicts = sections.at(-1)?.conflicts
297
+ const failed = sections.at(-1)
299
298
 
300
- await record_stop(args, step.stage, conflicts)
299
+ await record_stop(args, step.stage, failed?.conflicts)
301
300
 
302
301
  if (run_ship_detach.is_supervised()) {
303
- const resume = run_ship_next.resume_of(args)
304
-
305
302
  record(context.target, run_ship_stage.end_attempt)
306
- await run_ship_return.return_control(
307
- args.number,
308
- step.stage,
309
- conflicts === undefined ? resume : { ...resume, conflicts },
310
- )
303
+ await run_ship_return.hand_back(args, step.stage, failed)
311
304
  }
312
305
 
313
306
  return sections
@@ -381,8 +374,8 @@ function should_detach(args: ShipArguments): boolean {
381
374
 
382
375
  // The preflight is deterministic and fast, so it runs here, in the agent's own turn, before the
383
376
  // hand-off — a stop it finds is fixed by the same session instead of relaunching one from the
384
- // supervisor. Only the slow stages go to the supervisor, which re-asks the preflight itself. The type
385
- // check and the document tests follow it here for the same reason; they are not a stage, so the
377
+ // supervisor. Only the slow stages go to the supervisor, which re-asks the preflight itself. The fast
378
+ // checks (`run-ship-pre-detach.ts`) follow it here for the same reason; they are not a stage, so the
386
379
  // supervisor's gate runs them again rather than passing over them.
387
380
  async function pre_detach_sections(args: ShipArguments): Promise<ReadonlyArray<ShipSection>> {
388
381
  const preflight = await run_stage(run_ship_steps.PREFLIGHT_STEP, args, await open_context(args))
@@ -183,6 +183,14 @@ function current_record(repository: string, number: string): ShipRecord | undefi
183
183
  return record?.launch_id === process.env[LAUNCH_ID_KEY] ? record : undefined
184
184
  }
185
185
 
186
+ // When the ship for `number` that is still running was launched — `undefined` where none is running or
187
+ // its record carries no launch time. `run-ship-overlap.ts` orders two running ships by it.
188
+ function running_since(repository: string, number: string): number | undefined {
189
+ const record = read_record(repository, number)
190
+
191
+ return record !== undefined && result_of(record) === 'running' ? record.launched_at : undefined
192
+ }
193
+
186
194
  function is_pending(record: ShipRecord | undefined): boolean {
187
195
  return record !== undefined && record.pid === undefined
188
196
  }
@@ -362,6 +370,7 @@ const run_ship_detach = {
362
370
  last_launch,
363
371
  log_path,
364
372
  read_log,
373
+ running_since,
365
374
  supervisor_argv,
366
375
  }
367
376
 
@@ -104,13 +104,21 @@ function conflict_step(conflicts: ReadonlyArray<string>): string {
104
104
  return `Resolve the conflict with origin/main in ${conflicts.join(', ')} (the merge is left in progress: remove the conflict markers in those files), then`
105
105
  }
106
106
 
107
+ // A prompt that already carries the failed output points at it; only one without has the
108
+ // session read the stopped report first.
109
+ function fix_step(issue: string, stage: Stage, is_excerpted: boolean): string {
110
+ if (is_excerpted) return `Fix the ${stage} failure shown in the output below`
111
+
112
+ return `Read the stopped report with \`${COMMAND.LOG} ${issue}\`, fix the ${stage} failure`
113
+ }
114
+
107
115
  // Two lines' worth: read the stopped report, then the one command that resumes after the fix.
108
- function next_step(issue: string, stage: Stage, resume: ShipResume): string {
116
+ function next_step(issue: string, stage: Stage, resume: ShipResume, is_excerpted = false): string {
109
117
  const conflicts = resume.conflicts ?? []
110
118
 
111
119
  if (conflicts.length > 0) return `${conflict_step(conflicts)} ${AFTER_FIX[stage](resume)}`
112
120
 
113
- return `Read the stopped report with \`${COMMAND.LOG} ${issue}\`, fix the ${stage} failure, then ${AFTER_FIX[stage](resume)}`
121
+ return `${fix_step(issue, stage, is_excerpted)}, then ${AFTER_FIX[stage](resume)}`
114
122
  }
115
123
 
116
124
  const run_ship_next = { COMMAND, next_step, quoted, resume_of }
@@ -0,0 +1,195 @@
1
+ import { changed_paths } from '#scripts/git/changed-paths'
2
+ import { git_diff_reads } from '#scripts/git/git-diff-reads'
3
+ import { git_spawn } from '#scripts/git/git-spawn'
4
+ import { session_cite } from '#scripts/issue/session-cite'
5
+ import { lane_handoff } from '#scripts/lane/lane-handoff'
6
+ import { lane_ledger, type OverlapRecord } from '#scripts/lane/lane-ledger'
7
+ import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
8
+ import { poll } from '#scripts/lib/poll'
9
+
10
+ // The wait in front of `josh ship`'s sync (joshuafolkken/kit#3901). `backlog-overlap.ts` keeps lanes
11
+ // apart only on the paths their issue bodies declare, so two lanes that both edit an undeclared file
12
+ // were found out only when the later pull request turned `DIRTY` — after its gate and its CI wait,
13
+ // often as a `conflict` stop its child no longer had the context for.
14
+ //
15
+ // **The later ship waits for the earlier one rather than anything being pushed into a lane.** A lane
16
+ // whose actual change overlaps this one's and whose pull request is already open (its branch pushed,
17
+ // its detached ship still running) is waited on until that ship ends, merged or stopped; the sync that
18
+ // follows then merges a default branch that already holds it, so a conflict surfaces before this
19
+ // lane's gate rather than after its CI. Merging into the other lane's tree instead could not work: a
20
+ // lane mid-gate would have its record moved under it, and a lane still implementing holds the
21
+ // overlapping paths uncommitted, which `main:merge`'s guard refuses.
22
+ //
23
+ // **Only a ship launched before this one is waited on**, so two ships never wait on each other — a
24
+ // re-ship after a stop already has a pushed branch, so "pushed" alone could not order two re-ships in
25
+ // their sync. A ship run in an agent's own turn has no launch record and counts as the latest; equal
26
+ // launch times fall to the lower issue number. The wait is bounded — a ship that hangs never holds
27
+ // another past `MAX_WAIT_ATTEMPTS`, after which the sync goes ahead as it always did.
28
+
29
+ const WAIT_INTERVAL_MS = 30_000
30
+ const MAX_WAIT_ATTEMPTS = 60
31
+ // Unquoted like `read_changed_paths`, so a non-ASCII path compares equal on both sides.
32
+ const NAME_ONLY_ARGUMENTS = [
33
+ '-c',
34
+ 'core.quotePath=false',
35
+ 'diff',
36
+ '--name-only',
37
+ '--no-relative',
38
+ '--no-renames',
39
+ ]
40
+
41
+ interface OverlapOptions {
42
+ sleeper?: (duration_ms: number) => Promise<void>
43
+ ledger?: string
44
+ }
45
+
46
+ interface Overlap {
47
+ lanes: ReadonlyArray<LaneInfo>
48
+ paths: ReadonlyArray<string>
49
+ }
50
+
51
+ function to_paths(raw: string): Array<string> {
52
+ return changed_paths.to_paths(raw)
53
+ }
54
+
55
+ async function has_upstream(directory: string): Promise<boolean> {
56
+ try {
57
+ await git_spawn.read(['-C', directory, 'rev-parse', '--verify', '--quiet', '@{upstream}'])
58
+
59
+ return true
60
+ } catch {
61
+ return false
62
+ }
63
+ }
64
+
65
+ // A lane's change as its pushed branch carries it: everything its commits changed since it left the
66
+ // default branch. Read in its own tree, so the other lane's uncommitted edits — not yet on its pull
67
+ // request — are not counted.
68
+ async function lane_paths(directory: string): Promise<ReadonlyArray<string>> {
69
+ const base = await git_diff_reads.default_branch_reference()
70
+
71
+ try {
72
+ return to_paths(
73
+ await git_spawn.read(['-C', directory, ...NAME_ONLY_ARGUMENTS, `${base}...HEAD`, '--']),
74
+ )
75
+ } catch {
76
+ return []
77
+ }
78
+ }
79
+
80
+ interface Launch {
81
+ issue: string
82
+ since: number
83
+ }
84
+
85
+ function own_launch(issue: string): Launch {
86
+ const since = lane_handoff.ship_running_since(issue, process.cwd())
87
+
88
+ return { issue, since: since ?? Infinity }
89
+ }
90
+
91
+ function is_launched_before(lane: LaneInfo, own: Launch): boolean {
92
+ const since = lane_handoff.ship_running_since(lane.issue, lane.directory)
93
+
94
+ if (since === undefined) return false
95
+
96
+ return since < own.since || (since === own.since && Number(lane.issue) < Number(own.issue))
97
+ }
98
+
99
+ async function is_open_ship(lane: LaneInfo, own: Launch): Promise<boolean> {
100
+ if (!is_launched_before(lane, own)) return false
101
+
102
+ return await has_upstream(lane.directory)
103
+ }
104
+
105
+ async function shared_paths(
106
+ lane: LaneInfo,
107
+ launch: Launch,
108
+ own: ReadonlySet<string>,
109
+ ): Promise<Array<string>> {
110
+ if (!(await is_open_ship(lane, launch))) return []
111
+
112
+ const paths = await lane_paths(lane.directory)
113
+
114
+ return paths.filter((file_path) => own.has(file_path))
115
+ }
116
+
117
+ async function other_lanes(issue: string): Promise<ReadonlyArray<LaneInfo>> {
118
+ const lanes = await lane_registry.list_lanes()
119
+
120
+ return lanes.filter((lane) => lane.issue !== issue && !lane.is_stranded)
121
+ }
122
+
123
+ // Every other lane whose open pull request, shipped before this one, touches a path this lane changed,
124
+ // with the shared paths.
125
+ async function find_overlap(issue: string): Promise<Overlap> {
126
+ const own = new Set(await changed_paths.read_changed_paths(false))
127
+ const launch = own_launch(issue)
128
+ const others = await other_lanes(issue)
129
+ const shared = await Promise.all(
130
+ others.map(async (lane) => await shared_paths(lane, launch, own)),
131
+ )
132
+
133
+ return {
134
+ lanes: others.filter((_lane, index) => (shared[index] ?? []).length > 0),
135
+ paths: [...new Set(shared.flat())].toSorted((left, right) => left.localeCompare(right)),
136
+ }
137
+ }
138
+
139
+ async function read_overlap(issue: string): Promise<Overlap> {
140
+ try {
141
+ return await find_overlap(issue)
142
+ } catch {
143
+ return { lanes: [], paths: [] }
144
+ }
145
+ }
146
+
147
+ function is_settled(lanes: ReadonlyArray<LaneInfo>): boolean {
148
+ return lanes.every((lane) => !lane_handoff.is_ship_running(lane.issue, lane.directory))
149
+ }
150
+
151
+ async function wait_for(lanes: ReadonlyArray<LaneInfo>, options: OverlapOptions): Promise<boolean> {
152
+ return await poll.poll_until(async () => is_settled(lanes), {
153
+ attempts: MAX_WAIT_ATTEMPTS,
154
+ interval_ms: WAIT_INTERVAL_MS,
155
+ sleeper: options.sleeper ?? poll.sleep,
156
+ })
157
+ }
158
+
159
+ // Waits out every overlapping open ship and records the decision, returning what was recorded; a lane
160
+ // that overlaps nothing returns `undefined` at once and writes nothing. Best-effort: an overlap that
161
+ // could not be read is no overlap, so the sync is never blocked by the check that only exists to make
162
+ // it land cleaner.
163
+ async function await_overlap(
164
+ issue: string,
165
+ options: OverlapOptions = {},
166
+ ): Promise<OverlapRecord | undefined> {
167
+ const overlap = await read_overlap(issue)
168
+
169
+ if (overlap.lanes.length === 0) return undefined
170
+
171
+ const started = Date.now()
172
+ const is_cleared = await wait_for(overlap.lanes, options)
173
+ const lanes = overlap.lanes.map((lane) => Number(lane.issue))
174
+ const waited_ms = Date.now() - started
175
+ const record = { issue: Number(issue), lanes, paths: [...overlap.paths], waited_ms, is_cleared }
176
+
177
+ await lane_ledger.record_overlap(record, options.ledger)
178
+
179
+ return record
180
+ }
181
+
182
+ // The line the sync stage prints for a recorded wait, or `''` where nothing overlapped.
183
+ function note(record: OverlapRecord | undefined): string {
184
+ if (record === undefined) return ''
185
+
186
+ const lanes = record.lanes.map((lane) => session_cite.issue(lane)).join(', ')
187
+ const outcome = record.is_cleared ? 'waited for' : 'gave up waiting for'
188
+
189
+ return `${outcome} ${lanes}, whose open pull requests also change: ${record.paths.join(', ')}`
190
+ }
191
+
192
+ const run_ship_overlap = { MAX_WAIT_ATTEMPTS, await_overlap, note }
193
+
194
+ export type { OverlapOptions }
195
+ export { run_ship_overlap }
@@ -6,14 +6,19 @@ import type { JoshResult } from '#scripts/josh/josh-run'
6
6
  import { buffered_process, type BufferedProcessResult } from '#scripts/lib/buffered-process'
7
7
 
8
8
  // The fast checks a detached `josh ship` runs in the agent's own turn, after the preflight and before
9
- // the hand-off: a type error in a file the related-only checks never touched, or a document test (a
10
- // SKILL.md pointer, a byte budget, the command list) that only the full suite runs, would otherwise
11
- // first surface after the detach and cost a relaunched session. These two answer in seconds, so the
12
- // same session fixes what they find and ships again. **Nothing moves out of the gate**: the
13
- // supervised gate stage still runs every check, these included.
9
+ // the hand-off: a type error in a file the related-only checks never touched, an export nothing reads,
10
+ // an unknown word, or a guard test (a SKILL.md pointer, a byte budget, the command list, a source with
11
+ // no colocated test, a bare issue citation) that only the full suite runs, would otherwise first
12
+ // surface after the detach and cost a relaunched session. They answer in seconds, so the same session
13
+ // fixes what they find and ships again. **Nothing moves out of the gate**: the supervised gate stage
14
+ // still runs every check, these included.
14
15
  //
15
- // The test targets are the directories such failures land in. A consumer has none of them, so only
16
- // the ones present run, and a consumer meets the type check alone.
16
+ // joshuafolkken/kit#3924: nine of ten ship gate stops were `exports:unused`, `cspell:dot` or a
17
+ // repository-wide guard test failing on the child's own code — none a check `test:related` selects,
18
+ // since a guard test imports no changed file.
19
+ //
20
+ // The test targets are where such failures land. A consumer has none of them, so only the ones
21
+ // present run, and a consumer meets the static checks alone.
17
22
  //
18
23
  // In kit the metrics ratchet's totals join them: a feature grows a total almost every time, and here
19
24
  // the session that grew the total writes the reason itself rather than a relaunched one. The
@@ -25,24 +30,31 @@ const FAILURE_EXIT_CODE = 1
25
30
  const SECTION_SEPARATOR = '\n\n'
26
31
  const PASSED = 'pre-detach checks passed'
27
32
  const METRICS_TOTALS_COMMAND: ReadonlyArray<string> = ['josh', 'metrics', '--totals-only']
28
- const DOCUMENT_TEST_TARGETS: ReadonlyArray<string> = [
33
+ const STATIC_CHECKS: ReadonlyArray<ReadonlyArray<string>> = [
34
+ ['josh', 'exports:unused'],
35
+ ['josh', 'cspell:dot'],
36
+ ]
37
+ const GUARD_TEST_TARGETS: ReadonlyArray<string> = [
29
38
  'scripts/document',
30
39
  'scripts/claude',
31
40
  'scripts/rules',
32
41
  'scripts/josh/josh-command-map.test.ts',
42
+ 'scripts/issue/session-cite-scan.test.ts',
43
+ 'scripts/test/untested-sources.test.ts',
44
+ 'scripts/test/pure-files.test.ts',
33
45
  ]
34
46
  // A present directory need not hold a test the project's Vitest includes — a consumer's own
35
47
  // `scripts/claude` of shell helpers, say — and a filter that matches nothing would stop every ship.
36
- const DOCUMENT_TEST_COMMAND: ReadonlyArray<string> = ['josh', 'test:unit', '--passWithNoTests']
48
+ const GUARD_TEST_COMMAND: ReadonlyArray<string> = ['josh', 'test:unit', '--passWithNoTests']
37
49
 
38
50
  function present_targets(directory: string): ReadonlyArray<string> {
39
- return DOCUMENT_TEST_TARGETS.filter((target) => existsSync(path.join(directory, target)))
51
+ return GUARD_TEST_TARGETS.filter((target) => existsSync(path.join(directory, target)))
40
52
  }
41
53
 
42
- function document_tests(directory: string): Array<ReadonlyArray<string>> {
54
+ function guard_tests(directory: string): Array<ReadonlyArray<string>> {
43
55
  const targets = present_targets(directory)
44
56
 
45
- return targets.length === 0 ? [] : [[...DOCUMENT_TEST_COMMAND, ...targets]]
57
+ return targets.length === 0 ? [] : [[...GUARD_TEST_COMMAND, ...targets]]
46
58
  }
47
59
 
48
60
  // The same kit-only rule as the gate's `is_kit_only` step: a consumer's kit has no `josh metrics`.
@@ -53,7 +65,7 @@ function kit_only(directory: string): Array<ReadonlyArray<string>> {
53
65
  async function command_lists(directory: string): Promise<Array<ReadonlyArray<string>>> {
54
66
  const type_check = await type_check_step.resolve_type_check_args(directory)
55
67
 
56
- return [type_check, ...document_tests(directory), ...kit_only(directory)]
68
+ return [type_check, ...STATIC_CHECKS, ...guard_tests(directory), ...kit_only(directory)]
57
69
  }
58
70
 
59
71
  async function run_in(
@@ -63,7 +75,7 @@ async function run_in(
63
75
  return await buffered_process.run_buffered_process(list, { cwd: directory })
64
76
  }
65
77
 
66
- // Both run at once, buffered, and every failure is reported, so one ship shows all there is to fix.
78
+ // All run at once, buffered, and every failure is reported, so one ship shows all there is to fix.
67
79
  async function checks(directory: string = process.cwd()): Promise<JoshResult> {
68
80
  const lists = await command_lists(directory)
69
81
  const results = await Promise.all(lists.map(async (list) => await run_in(directory, list)))
@@ -77,6 +89,12 @@ async function checks(directory: string = process.cwd()): Promise<JoshResult> {
77
89
  }
78
90
  }
79
91
 
80
- const run_ship_pre_detach = { DOCUMENT_TEST_TARGETS, METRICS_TOTALS_COMMAND, PASSED, checks }
92
+ const run_ship_pre_detach = {
93
+ GUARD_TEST_TARGETS,
94
+ METRICS_TOTALS_COMMAND,
95
+ PASSED,
96
+ STATIC_CHECKS,
97
+ checks,
98
+ }
81
99
 
82
100
  export { run_ship_pre_detach }
@@ -3,9 +3,11 @@ import { git_gh_command } from '#scripts/gh/git-gh-command'
3
3
  import { git_pr } from '#scripts/gh/git-pr'
4
4
  import { changed_paths } from '#scripts/git/changed-paths'
5
5
  import { git_branch } from '#scripts/git/git-branch'
6
+ import { git_issue } from '#scripts/git/git-issue'
6
7
  import { git_preflight } from '#scripts/git/git-preflight'
7
8
  import type { JoshResult } from '#scripts/josh/josh-run'
8
9
  import { error_text } from '#scripts/lib/error-message'
10
+ import { acceptance_map_check } from '#scripts/review/acceptance-map-check'
9
11
  import { live_evidence } from '#scripts/review/live-evidence'
10
12
  import { run_ship_lock } from './run-ship-lock'
11
13
  import { run_ship_scoped } from './run-ship-scoped'
@@ -22,6 +24,7 @@ import { run_ship_scoped } from './run-ship-scoped'
22
24
 
23
25
  const SUCCESS_EXIT_CODE = 0
24
26
  const FAILURE_EXIT_CODE = 1
27
+ const UNREADABLE = Symbol('unreadable body')
25
28
  const EVIDENCE_PROBLEM = `This change touches runtime code, but the PR body has no ${live_evidence.EVIDENCE_HEADING} section: run the acceptance criteria for real and pass the body with --body-file <path>. Write it ${live_evidence.EVIDENCE_FORMAT}`
26
29
  const EVIDENCE_PENDING = `This change touches runtime code and no PR is open yet: write the ${live_evidence.EVIDENCE_HEADING} section to a file and hand it to josh ship with --body-file <path>, or the ship refuses. Write it ${live_evidence.EVIDENCE_FORMAT}`
27
30
 
@@ -71,16 +74,39 @@ async function caught(problems: Promise<Array<string>>): Promise<Array<string>>
71
74
  }
72
75
  }
73
76
 
77
+ async function readable_body(
78
+ body_path: string | undefined,
79
+ ): Promise<string | undefined | typeof UNREADABLE> {
80
+ try {
81
+ return await body_text(body_path)
82
+ } catch {
83
+ return UNREADABLE
84
+ }
85
+ }
86
+
87
+ // `followup`'s acceptance-map refusal, asked of the body this ship will deliver. A branch with no issue
88
+ // number is `git_preflight`'s problem to name, and an unreadable body the evidence side's.
89
+ async function acceptance_problems(body_path: string | undefined): Promise<Array<string>> {
90
+ const issue = git_issue.branch_number(await git_branch.current())
91
+ const body = await readable_body(body_path)
92
+
93
+ if (issue === undefined || body === UNREADABLE) return []
94
+ const problem = await acceptance_map_check.body_refusal(issue, body)
95
+
96
+ return problem === undefined ? [] : [problem]
97
+ }
98
+
74
99
  // The lock file is asked here too, so a drift stops the ship before the gate
75
100
  // rather than being rewritten by the pre-push hook's install under the push.
76
101
  async function asked_problems(request: PreflightRequest): Promise<Array<string>> {
77
- const [preflight, evidence, lock] = await Promise.all([
102
+ const [preflight, evidence, acceptance, lock] = await Promise.all([
78
103
  caught(git_preflight.problems_of({ cli_input: request.title, will_open_pr: true })),
79
104
  caught(evidence_problems(request.body_path)),
105
+ caught(acceptance_problems(request.body_path)),
80
106
  caught(run_ship_lock.lock_problems()),
81
107
  ])
82
108
 
83
- return [...preflight, ...evidence, ...lock]
109
+ return [...preflight, ...evidence, ...acceptance, ...lock]
84
110
  }
85
111
 
86
112
  async function stage(request: PreflightRequest): Promise<JoshResult> {
@@ -6,8 +6,11 @@ import { lane_relaunch } from '#scripts/lane/lane-relaunch'
6
6
  import { lane_session_broker } from '#scripts/lane/lane-session-broker'
7
7
  import { run_event_stream } from '#scripts/run/event/run-event-stream'
8
8
  import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
9
- import type { ShipResume } from './run-ship-next'
9
+ import type { ShipSection } from './run-ship'
10
+ import { run_ship_next, type ShipResume } from './run-ship-next'
10
11
  import { run_ship_stage, type Stage } from './run-ship-stage'
12
+ import type { ShipArguments } from './run-ship-steps'
13
+ import { run_ship_stop_context } from './run-ship-stop-context'
11
14
  import { run_ship_stop_text } from './run-ship-stop-text'
12
15
 
13
16
  // How a detached `josh ship` supervisor hands a stopped stage back. The agent
@@ -66,19 +69,26 @@ function relaunched(lane: LaneInfo, stop: ShipStop): ReturnOutcome {
66
69
  // child's Bash tool, which Claude Code starts without the token, so a child started here could not sign
67
70
  // in where the token is the only credential (joshuafolkken/kit#3817).
68
71
  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
+ const answer = await lane_session_broker.request(lane.directory, lane.issue, {
73
+ kind: 'relaunch',
74
+ stop,
75
+ })
72
76
 
73
- note('the lane child could not be relaunched: the lane broker did not start it')
77
+ if (answer.is_success) return 'relaunched'
78
+
79
+ note(
80
+ `the lane child could not be relaunched: the lane broker did not start it — ${answer.reason}`,
81
+ )
74
82
 
75
83
  return 'recorded'
76
84
  }
77
85
 
86
+ // `output` is the failed stage's own output: the relaunched child's prompt carries its tail.
78
87
  async function return_control(
79
88
  issue: string,
80
89
  stage: Stage,
81
90
  resume: ShipResume,
91
+ output: string,
82
92
  ): Promise<ReturnOutcome> {
83
93
  await run_event_stream_emit.emit(
84
94
  run_event_stream.EVENT_KIND.SHIP_STOP,
@@ -89,14 +99,28 @@ async function return_control(
89
99
 
90
100
  if (lane === undefined) return 'recorded'
91
101
 
92
- const stop = { stage, ...resume }
102
+ const stop = { stage, ...resume, context: await run_ship_stop_context.build(issue, output) }
93
103
 
94
104
  return lane_session_broker.is_serving(lane.directory, issue)
95
105
  ? await brokered(lane, stop)
96
106
  : relaunched(lane, stop)
97
107
  }
98
108
 
99
- const run_ship_return = { return_control }
109
+ // The stopped ship's arguments and its failed section, as `return_control` takes them: the section's
110
+ // conflicts ride on the resume, its body is the output the prompt carries.
111
+ async function hand_back(
112
+ args: ShipArguments,
113
+ stage: Stage,
114
+ failed: Pick<ShipSection, 'body' | 'conflicts'> | undefined,
115
+ ): Promise<ReturnOutcome> {
116
+ const resume = run_ship_next.resume_of(args)
117
+ const conflicts = failed?.conflicts
118
+ const stopped = conflicts === undefined ? resume : { ...resume, conflicts }
119
+
120
+ return await return_control(args.number, stage, stopped, failed?.body ?? '')
121
+ }
122
+
123
+ const run_ship_return = { hand_back, return_control }
100
124
 
101
125
  export type { ReturnOutcome }
102
126
  export { run_ship_return }
@@ -77,15 +77,18 @@ function note(text: string): void {
77
77
  process.stderr.write(`${text}\n`)
78
78
  }
79
79
 
80
- // Under the reviewer profile — the same model and effort (`JOSH_REVIEWER_*` included) the chain's
81
- // subagent is given — and waited on, so the join below runs only once the review has finished. Each
82
- // call is its own process, so round 2 is a fresh session by construction.
80
+ // Under the round's reviewer profile — the same model and effort (`JOSH_REVIEWER_*` included) the
81
+ // chain's subagent is given — and waited on, so the join below runs only once the review has finished.
82
+ // Each call is its own process, so round 2 is a fresh session by construction.
83
83
  async function run_direct_session(
84
84
  argv: AgentArgv,
85
85
  profile: AgentProfile,
86
86
  log_path: string,
87
+ round: Round,
87
88
  ): Promise<JoshResult> {
88
89
  const request = { argv, cwd: PROJECT_ROOT, log_path, profile }
90
+
91
+ note(agent_role_profile.describe_review(Number(round), profile))
89
92
  const result = await detached_launch.launch_attached(request, note)
90
93
 
91
94
  if (result.kind === 'failed') return failure(`reviewer could not start: ${result.note}`)
@@ -110,19 +113,18 @@ async function run_session(
110
113
  issue: string,
111
114
  round: Round,
112
115
  ): Promise<JoshResult> {
113
- const built = agent_argv.resolve_in(prompt, agent_role_profile.REVIEWER, PROJECT_ROOT)
116
+ const built = agent_argv.review_in(prompt, Number(round), PROJECT_ROOT)
114
117
  if (built.kind === 'rejected') return failure(`reviewer profile rejected: ${built.note}`)
115
118
 
116
119
  if (!is_brokered(built.profile, issue)) {
117
- return await run_direct_session(built.argv, built.profile, log_path)
120
+ return await run_direct_session(built.argv, built.profile, log_path, round)
118
121
  }
119
122
 
120
- const is_success = await lane_session_broker.request(PROJECT_ROOT, issue, {
121
- kind: 'review',
122
- round,
123
- })
123
+ const answer = await lane_session_broker.request(PROJECT_ROOT, issue, { kind: 'review', round })
124
124
 
125
- return is_success ? { code: SUCCESS_EXIT_CODE, out: '' } : failure('isolated reviewer failed')
125
+ return answer.is_success
126
+ ? { code: SUCCESS_EXIT_CODE, out: '' }
127
+ : failure(`isolated reviewer failed: ${answer.reason}`)
126
128
  }
127
129
 
128
130
  // The brief goes to a file and the previous round's findings are removed, so a reviewer that writes
@@ -65,7 +65,7 @@ const COMMIT_STEPS: ReadonlyArray<Step> = [
65
65
  {
66
66
  stage: STAGE.SYNC,
67
67
  header: run_ship.SYNC_HEADER,
68
- run: async () => await run_ship_sync.sync_stage(),
68
+ run: async (args) => await run_ship_sync.sync_stage(args.number),
69
69
  },
70
70
  {
71
71
  stage: STAGE.GATE,