@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
@@ -45,9 +45,9 @@ interface LaneGuardEntry {
45
45
  const LANE_GUARD_POLICY: ReadonlyArray<LaneGuardEntry> = [
46
46
  {
47
47
  id: 'investigation',
48
- mode_in_lane_child: 'notice',
48
+ mode_in_lane_child: 'refuse',
49
49
  because:
50
- 'a refusal ends a child turn, yet lane children were measured reading files they never edited, so the child is nudged to delegate by a notice rather than killed',
50
+ 'the notice was measured not to move the reading — 52% of lane cost stayed in main-line exploration — and the refusal fires once per accumulation, a round trip the child survives as it survives the rule row, so the child is refused like the main line',
51
51
  },
52
52
  {
53
53
  id: 'batching',
@@ -24,6 +24,13 @@ function is_ship_running(issue: string, directory: string): boolean {
24
24
  )
25
25
  }
26
26
 
27
+ // When the detached ship for `issue` that is still live was launched, or `undefined` where none is.
28
+ function ship_running_since(issue: string, directory: string): number | undefined {
29
+ const repository = git_common_directory.repository(directory)
30
+
31
+ return repository === undefined ? undefined : run_ship_detach.running_since(repository, issue)
32
+ }
33
+
27
34
  // This session is the dispatched lane child for `directory` and its own ship is running.
28
35
  function is_handed_off(directory: string, source: MarkerSource = process.env): boolean {
29
36
  const issue = lane_child_marker.marked_issue(source)
@@ -33,6 +40,6 @@ function is_handed_off(directory: string, source: MarkerSource = process.env): b
33
40
  return is_ship_running(issue, directory)
34
41
  }
35
42
 
36
- const lane_handoff = { is_handed_off, is_ship_running }
43
+ const lane_handoff = { is_handed_off, is_ship_running, ship_running_since }
37
44
 
38
45
  export { lane_handoff }
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import path from 'node:path'
3
+ import { core_budget } from '#scripts/gate/core-budget'
3
4
  import { buffered_process } from '#scripts/lib/buffered-process'
4
5
  import { INSTALL_TIMEOUT_MS } from '#scripts/lib/timeouts'
5
6
 
@@ -68,10 +69,17 @@ function has_manifest(directory: string): boolean {
68
69
  async function install_dependencies(directory: string): Promise<InstallResult> {
69
70
  if (!has_manifest(directory)) return { is_installed: true, output: NO_MANIFEST_OUTPUT }
70
71
 
71
- const result = await buffered_process.run_buffered_process(INSTALL_ARGUMENTS, {
72
- cwd: directory,
73
- timeout_ms: INSTALL_TIMEOUT_MS,
74
- })
72
+ // The install claims its place in the core budget (joshuafolkken/kit#3944): a batch opens its lanes
73
+ // together, and each install ran beside the other lanes' gates off the ledger.
74
+ const result = await core_budget.with_command_reservation(
75
+ core_budget.CORE_WEIGHTS.install,
76
+ async () =>
77
+ await buffered_process.run_buffered_process(INSTALL_ARGUMENTS, {
78
+ cwd: directory,
79
+ timeout_ms: INSTALL_TIMEOUT_MS,
80
+ }),
81
+ { memory_mb: core_budget.MEMORY_MB.install, command: 'lane install' },
82
+ )
75
83
 
76
84
  return { is_installed: !buffered_process.is_process_failed(result), output: result.output }
77
85
  }
@@ -36,9 +36,16 @@ const KIND = {
36
36
  DISPATCH: 'dispatch',
37
37
  STAGE: 'stage',
38
38
  STOP: 'stop',
39
+ OVERLAP: 'overlap',
39
40
  } as const
40
41
 
41
42
  const merge_schema = z.object({ kind: z.literal(KIND.MERGE), at: z.string(), issue: z.number() })
43
+ const check_schema = z.object({
44
+ label: z.string(),
45
+ elapsed_ms: z.number(),
46
+ is_passed: z.boolean(),
47
+ is_reused: z.boolean().optional(),
48
+ })
42
49
  const gate_schema = z.object({
43
50
  kind: z.literal(KIND.GATE),
44
51
  at: z.string(),
@@ -50,6 +57,9 @@ const gate_schema = z.object({
50
57
  // The whole cores busy outside this gate, beyond the machine's baseline, at its start or its end —
51
58
  // whichever was higher. Absent when the machine could not be read.
52
59
  external_cores: z.number().optional(),
60
+ // Each check's own duration and verdict, so `josh lane:stats` can name the check a slow gate waited
61
+ // on. A reused check started no process: its row says so, and its zero is no timing.
62
+ checks: z.array(check_schema).optional(),
53
63
  })
54
64
  // `available_mb`, `swapped_mb` and `lanes` are optional because each read can fail on its own — a
55
65
  // platform with no memory reading, a `git worktree` call that did not answer — and a sample is still
@@ -98,6 +108,18 @@ const stop_schema = z.object({
98
108
  issue: z.number(),
99
109
  reason: z.string(),
100
110
  })
111
+ // A `josh ship` on `issue` whose actual change overlapped the open pull requests of `lanes` on `paths`
112
+ // before its sync: it waited `waited_ms` for their ships to end, and `is_cleared` is false where the
113
+ // wait ran out first (joshuafolkken/kit#3901).
114
+ const overlap_schema = z.object({
115
+ kind: z.literal(KIND.OVERLAP),
116
+ at: z.string(),
117
+ issue: z.number(),
118
+ lanes: z.array(z.number()),
119
+ paths: z.array(z.string()),
120
+ waited_ms: z.number(),
121
+ is_cleared: z.boolean(),
122
+ })
101
123
  const entry_schema = z.discriminatedUnion('kind', [
102
124
  merge_schema,
103
125
  gate_schema,
@@ -105,6 +127,7 @@ const entry_schema = z.discriminatedUnion('kind', [
105
127
  dispatch_schema,
106
128
  stage_schema,
107
129
  stop_schema,
130
+ overlap_schema,
108
131
  ])
109
132
 
110
133
  type LedgerEntry = z.infer<typeof entry_schema>
@@ -114,10 +137,13 @@ type LoadEntry = z.infer<typeof load_schema>
114
137
  type DispatchEntry = z.infer<typeof dispatch_schema>
115
138
  type StageEntry = z.infer<typeof stage_schema>
116
139
  type StopEntry = z.infer<typeof stop_schema>
140
+ type OverlapEntry = z.infer<typeof overlap_schema>
117
141
  // What a finished gate reports; the ledger stamps the kind and the time.
118
142
  type GateRecord = Omit<GateEntry, 'kind' | 'at'>
143
+ type GateCheckRecord = z.infer<typeof check_schema>
119
144
  type StageRecord = Omit<StageEntry, 'kind' | 'at'>
120
145
  type StopRecord = Omit<StopEntry, 'kind' | 'at'>
146
+ type OverlapRecord = Omit<OverlapEntry, 'kind' | 'at'>
121
147
 
122
148
  function target_of(repository: string): string {
123
149
  return stamp_file.stamp_path(LEDGER_PREFIX, repository, LEDGER_SUFFIX)
@@ -194,6 +220,10 @@ async function record_stop(stop: StopRecord, path?: string): Promise<void> {
194
220
  await record({ kind: KIND.STOP, at: now_iso(), ...stop }, path)
195
221
  }
196
222
 
223
+ async function record_overlap(overlap: OverlapRecord, path?: string): Promise<void> {
224
+ await record({ kind: KIND.OVERLAP, at: now_iso(), ...overlap }, path)
225
+ }
226
+
197
227
  function parse_line(line: string): LedgerEntry | undefined {
198
228
  return entry_schema.safeParse(json_value.parse_or_undefined(line)).data
199
229
  }
@@ -224,11 +254,23 @@ const lane_ledger = {
224
254
  record_dispatch,
225
255
  record_gate,
226
256
  record_merge,
257
+ record_overlap,
227
258
  record_stage,
228
259
  record_stop,
229
260
  target,
230
261
  target_of,
231
262
  }
232
263
 
233
- export type { DispatchEntry, GateEntry, LedgerEntry, LoadEntry, MergeEntry, StageEntry, StopEntry }
264
+ export type {
265
+ DispatchEntry,
266
+ GateCheckRecord,
267
+ GateEntry,
268
+ LedgerEntry,
269
+ LoadEntry,
270
+ MergeEntry,
271
+ OverlapEntry,
272
+ OverlapRecord,
273
+ StageEntry,
274
+ StopEntry,
275
+ }
234
276
  export { lane_ledger }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
- import { backlog_ready } from '#scripts/backlog/backlog-ready'
3
+ import { backlog_ready, type LaneUse } from '#scripts/backlog/backlog-ready'
4
4
  import { cli_flags } from '#scripts/lib/cli-flags'
5
5
  import { lane_capacity, type LimitChoice } from './lane-capacity'
6
6
  import { lane_limit_override } from './lane-limit-override'
@@ -37,13 +37,13 @@ interface LimitPorts {
37
37
  read_override: () => Promise<number | undefined>
38
38
  write_override: (limit: number | undefined) => Promise<boolean>
39
39
  emit_raise: (text: string) => Promise<void>
40
- live_lane_count: () => Promise<number>
40
+ lane_use: () => Promise<LaneUse>
41
41
  }
42
42
 
43
43
  interface LimitStatus {
44
44
  limit: number
45
45
  override: number | undefined
46
- in_use: number
46
+ use: LaneUse
47
47
  }
48
48
 
49
49
  const SHOW: LimitRequest = { kind: 'show' }
@@ -55,7 +55,7 @@ const DEFAULT_PORTS: LimitPorts = {
55
55
  read_override: lane_limit_override.read_override,
56
56
  write_override: lane_limit_override.write_override,
57
57
  emit_raise: lane_limit_override.emit_raise,
58
- live_lane_count: backlog_ready.live_lane_count,
58
+ lane_use: backlog_ready.lane_use,
59
59
  }
60
60
 
61
61
  function refuse(message: string): number {
@@ -98,9 +98,10 @@ function limit_text(limit: number): string {
98
98
 
99
99
  function status_line(status: LimitStatus): string {
100
100
  const source = status.override === undefined ? lane_capacity.LANE_LIMIT_KEY : OVERRIDE_SOURCE
101
- const free = lane_capacity.free_lanes(status.limit, status.in_use)
101
+ const { occupied, seated } = status.use
102
+ const free = lane_capacity.free_lanes(status.limit, occupied, seated)
102
103
 
103
- return `lane limit ${limit_text(status.limit)} (${source}) · in use ${String(status.in_use)} · free ${String(free)}`
104
+ return `lane limit ${limit_text(status.limit)} (${source}) · in use ${String(occupied)} · free ${String(free)}`
104
105
  }
105
106
 
106
107
  async function show(ports: LimitPorts): Promise<number> {
@@ -110,9 +111,7 @@ async function show(ports: LimitPorts): Promise<number> {
110
111
 
111
112
  const override = await ports.read_override()
112
113
 
113
- console.info(
114
- status_line({ limit: choice.limit, override, in_use: await ports.live_lane_count() }),
115
- )
114
+ console.info(status_line({ limit: choice.limit, override, use: await ports.lane_use() }))
116
115
 
117
116
  return SUCCESS_EXIT_CODE
118
117
  }
@@ -3,6 +3,7 @@ import { setInterval as every } from 'node:timers/promises'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { cli_flags } from '#scripts/lib/cli-flags'
5
5
  import { lane_capacity } from './lane-capacity'
6
+ import { lane_check_stats } from './lane-check-stats'
6
7
  import { lane_ledger } from './lane-ledger'
7
8
  import { lane_process_stats } from './lane-process-stats'
8
9
  import { lane_sampler, type MeasureContext } from './lane-sampler'
@@ -121,6 +122,7 @@ async function stats_command(
121
122
  lane_stage_stats.table(entries, window),
122
123
  lane_stop_stats.table(entries, window),
123
124
  lane_process_stats.table(entries, window),
125
+ lane_check_stats.table(entries, window),
124
126
  ].join('\n\n'),
125
127
  )
126
128
 
@@ -1,5 +1,6 @@
1
1
  import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
2
2
  import { git_stash } from '#scripts/git/stash/git-stash'
3
+ import { error_text } from '#scripts/lib/error-message'
3
4
 
4
5
  // Keeping a lane's uncommitted work before something discards or restarts it — `run:merge` closing a
5
6
  // lane, `lane:restart` handing it to a fresh session. The stash stack outlives the tree, so the push is
@@ -30,6 +31,32 @@ async function announce(issue: string, message: string, cause: string): Promise<
30
31
  await git_gh_issue_write.issue_try_comment(issue, preserved_comment(message, cause))
31
32
  }
32
33
 
34
+ // The tree's work kept before a stopped child's lane is reused or closed: `saved` with the stash, or
35
+ // none for a clean tree, and `unsaved` with the reason when the tree could not be read or pushed — the
36
+ // caller then leaves the tree as it is rather than handing it on or deleting it.
37
+ type KeptWork =
38
+ | { kind: 'saved'; stash: string | undefined }
39
+ | { kind: 'unsaved'; directory: string; reason: string }
40
+
41
+ async function stash_of(
42
+ issue: string,
43
+ directory: string,
44
+ occasion: string,
45
+ ): Promise<string | undefined> {
46
+ if (!(await git_stash.has_changes(directory))) return undefined
47
+
48
+ return await push_work(issue, directory, occasion)
49
+ }
50
+
51
+ /** Stash the tree's uncommitted work, if any, answering `unsaved` rather than throwing. */
52
+ async function keep_work(issue: string, directory: string, occasion: string): Promise<KeptWork> {
53
+ try {
54
+ return { kind: 'saved', stash: await stash_of(issue, directory, occasion) }
55
+ } catch (error) {
56
+ return { kind: 'unsaved', directory, reason: error_text.message_of(error) }
57
+ }
58
+ }
59
+
33
60
  /** `push_work`, then `announce` — for a caller with no reason to separate the two. */
34
61
  async function stash_work(
35
62
  issue: string,
@@ -44,6 +71,7 @@ async function stash_work(
44
71
  return message
45
72
  }
46
73
 
47
- const lane_preserve = { announce, push_work, stash_work }
74
+ const lane_preserve = { announce, keep_work, stash_work }
48
75
 
76
+ export type { KeptWork }
49
77
  export { lane_preserve }
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs'
1
+ import { existsSync, statSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { ENV_FILE_NAME } from '#ports'
4
4
  import type { AgentProfile } from '#scripts/agent/agent-role-profile'
@@ -38,6 +38,9 @@ interface LaneInfo {
38
38
  // instead of waiting for the pool to drain before it can be cut.
39
39
  output: string | undefined
40
40
  profile?: AgentProfile | undefined
41
+ // When the lane last took a child: `lane:open` writes its `.env`, and every dispatch records its
42
+ // unit there (`lane-output.ts`). `undefined` where the `.env` cannot be read.
43
+ dispatched_ms?: number | undefined
41
44
  is_stranded: boolean
42
45
  }
43
46
 
@@ -106,6 +109,10 @@ function read_environment(directory: string): string | undefined {
106
109
  return file_reader.read_if_readable(path.join(directory, ENV_FILE_NAME))
107
110
  }
108
111
 
112
+ function dispatched_at(directory: string): number | undefined {
113
+ return statSync(path.join(directory, ENV_FILE_NAME), { throwIfNoEntry: false })?.mtimeMs
114
+ }
115
+
109
116
  // A lane is identified by its branch rather than by where it sits, so nothing here compares two
110
117
  // spellings of one path — git prints the resolved one, and a lane root reached through a symlink
111
118
  // would otherwise read as no lane at all.
@@ -151,6 +158,7 @@ function build_lane(
151
158
  preview_port: held.preview_port,
152
159
  output,
153
160
  profile,
161
+ dispatched_ms: is_stranded ? undefined : dispatched_at(directory),
154
162
  is_stranded,
155
163
  }
156
164
  }
@@ -241,11 +249,6 @@ async function has_lanes_in_flight(): Promise<boolean> {
241
249
  return lanes.some((lane) => !lane.is_stranded)
242
250
  }
243
251
 
244
- /** The lanes in use: the open work trees an interruption has not stranded. */
245
- function live_count(lanes: ReadonlyArray<LaneInfo>): number {
246
- return lanes.filter((lane) => !lane.is_stranded).length
247
- }
248
-
249
252
  /** The seats live lanes hold — what a new lane's seat has to avoid. */
250
253
  function used_seats(lanes: ReadonlyArray<LaneInfo>): Array<number> {
251
254
  return lanes.map((lane) => lane.seat).filter((seat): seat is number => seat !== undefined)
@@ -277,7 +280,6 @@ const lane_registry = {
277
280
  find_open_lane,
278
281
  has_lanes_in_flight,
279
282
  list_lanes,
280
- live_count,
281
283
  main_repository_root,
282
284
  parse_block,
283
285
  read_environment,
@@ -1,14 +1,12 @@
1
- import { git_stash } from '#scripts/git/stash/git-stash'
2
1
  import { issue_cite } from '#scripts/issue/issue-cite'
3
2
  import { IN_PROGRESS_LABEL } from '#scripts/issue/issue-labels'
4
3
  import { issue_number_shape } from '#scripts/issue/issue-number-shape'
5
4
  import { session_cite } from '#scripts/issue/session-cite'
6
- import { error_text } from '#scripts/lib/error-message'
7
5
  import { run_cut } from '#scripts/run/cut/run-cut'
8
6
  import { run_event_stream } from '#scripts/run/event/run-event-stream'
9
7
  import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
10
8
  import { lane_dispatch, type DispatchOutcome } from './lane-dispatch'
11
- import { lane_preserve } from './lane-preserve'
9
+ import { lane_preserve, type KeptWork } from './lane-preserve'
12
10
  import { lane_reap } from './lane-reap'
13
11
  import { lane_registry, type LaneInfo } from './lane-registry'
14
12
 
@@ -38,11 +36,7 @@ const NO_LANE_KIND = 'no-lane'
38
36
  const NOT_RUNNING_KIND = 'not-running'
39
37
  const UNCLAIMED_KIND = 'unclaimed'
40
38
 
41
- interface Unsaved {
42
- kind: 'unsaved'
43
- directory: string
44
- reason: string
45
- }
39
+ type Unsaved = Extract<KeptWork, { kind: 'unsaved' }>
46
40
 
47
41
  interface Restarted {
48
42
  kind: 'restarted'
@@ -55,7 +49,6 @@ type Refused =
55
49
  | { kind: typeof NOT_RUNNING_KIND }
56
50
  | { kind: typeof UNCLAIMED_KIND; reason: string }
57
51
  type RestartOutcome = Restarted | Unsaved | Refused
58
- type Saved = Unsaved | { kind: 'saved'; stash: string | undefined }
59
52
 
60
53
  // A cut the stopped child carried would hand its successor the old session's hand-off at
61
54
  // `run:cut --resume`; the fresh session has to start from the Issue instead.
@@ -65,22 +58,6 @@ function end_carried_cut(lane: LaneInfo): void {
65
58
  if (carried !== undefined) run_cut.end_cut(carried.target)
66
59
  }
67
60
 
68
- async function stash_of(lane: LaneInfo): Promise<string | undefined> {
69
- if (!(await git_stash.has_changes(lane.directory))) return undefined
70
-
71
- return await lane_preserve.push_work(lane.issue, lane.directory, RESTART_OCCASION)
72
- }
73
-
74
- // A tree that could not be read or pushed is left as it is: re-dispatching into it would hand the fresh
75
- // session the stopped one's half-done work as though it were its own.
76
- async function save_work(lane: LaneInfo): Promise<Saved> {
77
- try {
78
- return { kind: 'saved', stash: await stash_of(lane) }
79
- } catch (error) {
80
- return { kind: 'unsaved', directory: lane.directory, reason: error_text.message_of(error) }
81
- }
82
- }
83
-
84
61
  async function record_restart(issue: string): Promise<void> {
85
62
  await run_event_stream_emit.emit(
86
63
  run_event_stream.EVENT_KIND.RESTART,
@@ -88,9 +65,11 @@ async function record_restart(issue: string): Promise<void> {
88
65
  )
89
66
  }
90
67
 
68
+ // A tree that could not be read or pushed is left as it is: re-dispatching into it would hand the fresh
69
+ // session the stopped one's half-done work as though it were its own.
91
70
  async function relaunch(lane: LaneInfo): Promise<RestartOutcome> {
92
71
  end_carried_cut(lane)
93
- const saved = await save_work(lane)
72
+ const saved = await lane_preserve.keep_work(lane.issue, lane.directory, RESTART_OCCASION)
94
73
 
95
74
  if (saved.kind === 'unsaved') return saved
96
75
  const dispatch = await lane_dispatch.dispatch_child(lane.issue, 'restart')
@@ -0,0 +1,146 @@
1
+ import { agent_argv } from '#scripts/agent/agent-argv'
2
+ import { agent_role_profile, type AgentProvider } from '#scripts/agent/agent-role-profile'
3
+ import { stamp_file } from '#scripts/josh/stamp-file'
4
+ import { error_text } from '#scripts/lib/error-message'
5
+ import { detached_launch, type AttachedLaunchResult } from '#scripts/run/detached-launch'
6
+ import { run_ship_review } from '#scripts/run/ship/run-ship-review'
7
+ import { run_ship_stage } from '#scripts/run/ship/run-ship-stage'
8
+ import { run_ship_stop_context } from '#scripts/run/ship/run-ship-stop-context'
9
+ import { z } from 'zod'
10
+ import { lane_child_invocation, type ShipStop } from './lane-child-invocation'
11
+ import { lane_child_marker } from './lane-child-marker'
12
+ import { lane_parked } from './lane-parked'
13
+ import type { LaneInfo } from './lane-registry'
14
+ import { lane_relaunch } from './lane-relaunch'
15
+
16
+ // The actions a lane's session broker performs for the ship that asked (`lane-session-broker.ts`):
17
+ // starting the round-1 or round-2 reviewer, or relaunching the lane's child after a stopped ship.
18
+ // **A failure names its reason**: a bare `false` left every relaunch a stale supervisor refused
19
+ // unexplained in the ship log (joshuafolkken/kit#3974).
20
+
21
+ const BRIEF_PREFIX = 'josh-ship-review-brief-'
22
+ const FINDINGS_PREFIX = 'josh-ship-review-findings-'
23
+ const LOG_PREFIX = 'josh-ship-review-log-'
24
+ const ANTHROPIC_PROVIDER: AgentProvider = 'anthropic'
25
+ const stop_schema = z
26
+ .object({
27
+ stage: z.enum(run_ship_stage.STAGE),
28
+ title: z.string(),
29
+ flags: z.array(z.string()).readonly(),
30
+ is_review: z.boolean(),
31
+ conflicts: z.array(z.string()).readonly().default([]),
32
+ context: run_ship_stop_context.stop_context_schema.exactOptional(),
33
+ })
34
+ .strict()
35
+ const action_schema = z.discriminatedUnion('kind', [
36
+ z.object({ kind: z.literal('review'), round: z.enum(['1', '2']) }).strict(),
37
+ z.object({ kind: z.literal('relaunch'), stop: stop_schema }).strict(),
38
+ ])
39
+ type ReviewRound = Extract<z.infer<typeof action_schema>, { kind: 'review' }>['round']
40
+ type BrokerAction = { kind: 'review'; round: ReviewRound } | { kind: 'relaunch'; stop: ShipStop }
41
+ type BrokerAnswer = { is_success: true } | { is_success: false; reason: string }
42
+
43
+ const SUCCESS: BrokerAnswer = { is_success: true }
44
+
45
+ function failure(reason: string): BrokerAnswer {
46
+ return { is_success: false, reason }
47
+ }
48
+
49
+ function review_paths(directory: string): { brief: string; findings: string; log: string } {
50
+ return {
51
+ brief: stamp_file.stamp_path(BRIEF_PREFIX, directory, '.md'),
52
+ findings: stamp_file.stamp_path(FINDINGS_PREFIX, directory, '.txt'),
53
+ log: stamp_file.stamp_path(LOG_PREFIX, directory, '.log'),
54
+ }
55
+ }
56
+
57
+ // The reviewer runs on the lane's own provider, so a Claude lane's server never starts a Codex reviewer
58
+ // and an OpenAI lane's never starts a Claude one.
59
+ function provider_of(lane: LaneInfo): AgentProvider {
60
+ return lane.profile?.provider ?? ANTHROPIC_PROVIDER
61
+ }
62
+
63
+ // **The reviewer carries the lane's mark explicitly**: the supervisor serving a Claude lane is no lane child,
64
+ // so a reviewer inheriting its environment would descend from the carry record's owner unmarked and be
65
+ // read as the `backlogrun` parent by every hook. **Its prompt ends with the review invocation**, so the
66
+ // lane's liveness read and the park's reap find it as they find the child (joshuafolkken/kit#3878).
67
+
68
+ function review_prompt(lane: LaneInfo, round: ReviewRound): string {
69
+ const target = review_paths(lane.directory)
70
+ const prompt_of =
71
+ round === '1' ? run_ship_review.reviewer_prompt : run_ship_review.verification_prompt
72
+ const tail = lane_child_invocation.review_invocation(lane.issue)
73
+
74
+ return `${prompt_of(target.brief, target.findings, lane.issue)} This is ${tail}`
75
+ }
76
+
77
+ function reviewer_answer(result: AttachedLaunchResult): BrokerAnswer {
78
+ if (result.kind === 'failed') return failure(`the reviewer could not start: ${result.note}`)
79
+
80
+ return result.exit_code === 0
81
+ ? SUCCESS
82
+ : failure(`the reviewer exited with code ${String(result.exit_code)}`)
83
+ }
84
+
85
+ async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<BrokerAnswer> {
86
+ const target = review_paths(lane.directory)
87
+ const built = agent_argv.review_in(review_prompt(lane, round), Number(round), lane.directory)
88
+ if (built.kind === 'rejected') return failure(`the reviewer profile was rejected: ${built.note}`)
89
+
90
+ if (built.profile.provider !== provider_of(lane)) {
91
+ return failure(`the reviewer resolved to ${built.profile.provider}, not the lane's provider`)
92
+ }
93
+
94
+ console.error(agent_role_profile.describe_review(Number(round), built.profile))
95
+ const result = await detached_launch.launch_attached(
96
+ {
97
+ argv: built.argv,
98
+ cwd: lane.directory,
99
+ log_path: target.log,
100
+ profile: built.profile,
101
+ env: lane_child_marker.env_for(lane.issue),
102
+ },
103
+ console.error,
104
+ )
105
+
106
+ return reviewer_answer(result)
107
+ }
108
+
109
+ // An OpenAI lane's successor is its supervisor's to start, never a relaunch's.
110
+ function relaunch(lane: LaneInfo, stop: ShipStop): BrokerAnswer {
111
+ if (lane_relaunch.is_openai_lane(lane)) {
112
+ return failure('an OpenAI lane is relaunched by its own supervisor')
113
+ }
114
+
115
+ const invocation = lane_child_invocation.ship_stop_invocation(lane.issue, stop)
116
+ const phase = agent_role_profile.IMPLEMENTATION_PHASE
117
+ const result = lane_relaunch.relaunch(lane, invocation, phase, console.error)
118
+
119
+ return result.kind === 'launched'
120
+ ? SUCCESS
121
+ : failure(`the relaunch did not start: ${result.note}`)
122
+ }
123
+
124
+ // **A parked issue gets no session**: a request can be read long after it was written, so the park is
125
+ // read when the action runs, never when it was asked for (joshuafolkken/kit#3878).
126
+ async function perform(lane: LaneInfo, action: BrokerAction): Promise<BrokerAnswer> {
127
+ if (await lane_parked.is_parked(lane.issue)) return failure('the issue is parked')
128
+
129
+ if (action.kind === 'relaunch') return relaunch(lane, action.stop)
130
+
131
+ return await launch_review(lane, action.round)
132
+ }
133
+
134
+ // What the server answers: a thrown action is a failure naming its error, never a dead server.
135
+ async function answer(lane: LaneInfo, action: BrokerAction): Promise<BrokerAnswer> {
136
+ try {
137
+ return await perform(lane, action)
138
+ } catch (error) {
139
+ return failure(`the broker failed: ${error_text.message_of(error)}`)
140
+ }
141
+ }
142
+
143
+ const lane_session_action = { SUCCESS, action_schema, answer, failure, review_paths }
144
+
145
+ export type { BrokerAction, BrokerAnswer, ReviewRound }
146
+ export { lane_session_action }