@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
@@ -5,12 +5,14 @@ const SETTLED_KINDS: ReadonlySet<string> = new Set([
5
5
  run_event_stream.EVENT_KIND.MERGE,
6
6
  run_event_stream.EVENT_KIND.PARK,
7
7
  run_event_stream.EVENT_KIND.SPLIT,
8
+ run_event_stream.EVENT_KIND.WITHDRAW,
8
9
  run_event_stream.EVENT_KIND.OUTAGE,
9
10
  ])
10
11
  const DONE_KINDS: ReadonlySet<string> = new Set([
11
12
  run_event_stream.EVENT_KIND.MERGE,
12
13
  run_event_stream.EVENT_KIND.PARK,
13
14
  run_event_stream.EVENT_KIND.SPLIT,
15
+ run_event_stream.EVENT_KIND.WITHDRAW,
14
16
  ])
15
17
 
16
18
  function issue_of(event: RunEvent): string | undefined {
@@ -82,4 +84,10 @@ function restore(
82
84
  )
83
85
  }
84
86
 
85
- export const backlog_drive_restore = { active_issues, restore, settled_issues, parked_issues }
87
+ export const backlog_drive_restore = {
88
+ active_issues,
89
+ mentioned_issues,
90
+ restore,
91
+ settled_issues,
92
+ parked_issues,
93
+ }
@@ -0,0 +1,56 @@
1
+ import type { LaneInfo } from '#scripts/lane/lane-registry'
2
+ import type { RunEvent } from '#scripts/run/event/run-event-stream'
3
+ import { run_invocation } from '#scripts/run/run-invocation'
4
+ import { backlog_drive_restore } from './backlog-drive-restore'
5
+
6
+ // The open lanes that hold a place under the lane limit — the one count `lane:limit`, the drive and
7
+ // `run:board` all subtract from it.
8
+ //
9
+ // **A lane nobody will come back to holds no place.** A `--only` run works its named list and never
10
+ // touches another lane, so a lane an earlier run left with no child in it would fill the limit for the
11
+ // whole run, and the run's own issues would wait on it for good (joshuafolkken/kit#3967). Under a `--only`
12
+ // run such a lane is not counted; its tree and branch stay, for the next run that names its issue.
13
+ //
14
+ // **Everything else keeps its place, so the count errs toward fewer free lanes.** A run that drains
15
+ // the pool may offer any open lane's issue again, a lane whose child still works is busy whoever owns
16
+ // it, and with no run there is nobody to judge ownership for. The read stays local — no issue label is
17
+ // read — because the count gates the network read behind it (`backlog-ready.ts`).
18
+
19
+ interface OccupiedRead {
20
+ // The live run's invocation, `undefined` when no run is live here.
21
+ invocation: string | undefined
22
+ // The live run's own events, for the issues it launched or settled; `undefined` where its scope
23
+ // could not be told, which leaves every open lane counted.
24
+ events: ReadonlyArray<RunEvent> | undefined
25
+ // The issues whose lane has no child working in it; read only for a `--only` run.
26
+ idle: (lanes: ReadonlyArray<LaneInfo>) => ReadonlyArray<string>
27
+ }
28
+
29
+ // The issues a `--only` run owns — those it names and those it launched or settled — or `undefined`
30
+ // where there is no such run, or its events could not be scoped to it.
31
+ function owned_issues(read: OccupiedRead): ReadonlySet<string> | undefined {
32
+ const { invocation, events } = read
33
+ const is_only = invocation !== undefined && run_invocation.has_only(invocation)
34
+
35
+ if (!is_only || events === undefined) return undefined
36
+
37
+ const named = (run_invocation.issue_numbers(invocation) ?? []).map(String)
38
+
39
+ return new Set([...named, ...backlog_drive_restore.mentioned_issues(events)])
40
+ }
41
+
42
+ function occupied_lanes(lanes: ReadonlyArray<LaneInfo>, read: OccupiedRead): Array<LaneInfo> {
43
+ const live = lanes.filter((lane) => !lane.is_stranded)
44
+ const owned = owned_issues(read)
45
+
46
+ if (owned === undefined) return live
47
+
48
+ const idle = new Set(read.idle(live))
49
+
50
+ return live.filter((lane) => owned.has(lane.issue) || !idle.has(lane.issue))
51
+ }
52
+
53
+ const backlog_occupied = { occupied_lanes }
54
+
55
+ export { backlog_occupied }
56
+ export type { OccupiedRead }
@@ -3,9 +3,12 @@ import { issue_cite } from '#scripts/issue/issue-cite'
3
3
  import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
4
4
  import { lane_capacity } from '#scripts/lane/lane-capacity'
5
5
  import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
6
+ import { lane_spent } from '#scripts/lane/lane-spent'
6
7
  import type { RunCarry } from '#scripts/run/carry/run-carry'
8
+ import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
7
9
  import { run_headless } from '#scripts/run/run-headless'
8
10
  import { run_invocation } from '#scripts/run/run-invocation'
11
+ import { backlog_occupied, type OccupiedRead } from './backlog-occupied'
9
12
  import { backlog_stalled } from './backlog-stalled'
10
13
 
11
14
  // The ready line reports runnable work to the driving backlogrun parent on a watcher wake.
@@ -18,14 +21,46 @@ interface ReadyReading {
18
21
  free_lanes: number
19
22
  }
20
23
 
24
+ // How many open lanes hold a place under the limit, and how many hold a port seat.
25
+ interface LaneUse {
26
+ occupied: number
27
+ seated: number
28
+ }
29
+
21
30
  interface ReadyPorts {
22
31
  free_lane_count: () => Promise<number>
23
32
  ready_issues: () => Promise<ReadonlyArray<string>>
24
33
  }
25
34
 
26
- // The lanes in use: the open worktrees an interruption has not stranded. `josh lane:limit` prints it.
27
- async function live_lane_count(): Promise<number> {
28
- return lane_registry.live_count(await lane_registry.list_lanes())
35
+ // What decides which open lanes hold a place, read from this checkout's live run.
36
+ async function occupied_read(): Promise<OccupiedRead> {
37
+ const carry = await run_headless.current_carry()
38
+
39
+ return {
40
+ invocation: carry?.invocation,
41
+ events: carry === undefined ? [] : await run_event_stream_emit.scoped_current_events(),
42
+ idle: lane_spent.idle_lanes,
43
+ }
44
+ }
45
+
46
+ // The lanes that hold a place under the limit (`backlog-occupied.ts`).
47
+ async function occupied_lanes(lanes?: ReadonlyArray<LaneInfo>): Promise<Array<LaneInfo>> {
48
+ const open = lanes ?? (await lane_registry.list_lanes())
49
+
50
+ return backlog_occupied.occupied_lanes(open, await occupied_read())
51
+ }
52
+
53
+ // The open lanes as the free count reads them: those that hold a place under the limit, and every
54
+ // open one, since an uncounted lane still holds its port seat.
55
+ async function lane_use(lanes?: ReadonlyArray<LaneInfo>): Promise<LaneUse> {
56
+ const open = lanes ?? (await lane_registry.list_lanes())
57
+ const occupied = await occupied_lanes(open)
58
+
59
+ return { occupied: occupied.length, seated: open.filter((lane) => !lane.is_stranded).length }
60
+ }
61
+
62
+ function free_of(limit: number, use: LaneUse): number {
63
+ return lane_capacity.free_lanes(limit, use.occupied, use.seated)
29
64
  }
30
65
 
31
66
  // Free lanes beside `lanes`, `undefined` where the limit could not be read: the count `lane:limit`
@@ -35,11 +70,11 @@ async function free_lanes_beside(lanes: ReadonlyArray<LaneInfo>): Promise<number
35
70
 
36
71
  if (limit.kind !== 'limit') return undefined
37
72
 
38
- return lane_capacity.free_lanes(limit.limit, lane_registry.live_count(lanes))
73
+ return free_of(limit.limit, await lane_use(lanes))
39
74
  }
40
75
 
41
76
  async function free_under(limit: number): Promise<number> {
42
- return lane_capacity.free_lanes(limit, await live_lane_count())
77
+ return free_of(limit, await lane_use())
43
78
  }
44
79
 
45
80
  // Free lanes right now: the limit less the live lanes. An unreadable limit reads as no free lane — the
@@ -48,9 +83,10 @@ async function free_under(limit: number): Promise<number> {
48
83
  //
49
84
  // **Occupancy is the local worktree count, not the dispatch budget's busy read.** `epic-lane-offer`
50
85
  // measures occupancy with `epic_busy.occupied_lanes`, a network read of in-progress issue labels; this
51
- // counts live worktrees instead, deliberately — it runs on the cheap-first path where the costly read
52
- // (`backlog:next`) is gated behind a free lane, so a network read here would defeat that design. The
53
- // two can diverge, but only toward over-counting live lanes → fewer free lanes, the safe direction.
86
+ // counts the worktrees that hold a place instead (`backlog-occupied.ts`), deliberately — it runs on the
87
+ // cheap-first path where the costly read (`backlog:next`) is gated behind a free lane, so a network
88
+ // read here would defeat that design. The two can diverge, but only toward over-counting live lanes →
89
+ // fewer free lanes, the safe direction.
54
90
  async function free_lane_count(): Promise<number> {
55
91
  return (await free_lanes_beside(await lane_registry.list_lanes())) ?? NO_FREE
56
92
  }
@@ -172,7 +208,8 @@ const backlog_ready = {
172
208
  print_offer_hint,
173
209
  free_lane_count,
174
210
  free_lanes_beside,
175
- live_lane_count,
211
+ lane_use,
212
+ occupied_lanes,
176
213
  drive_free_lane_count,
177
214
  drains_pool,
178
215
  print_ready_line,
@@ -182,4 +219,4 @@ const backlog_ready = {
182
219
  }
183
220
 
184
221
  export { backlog_ready }
185
- export type { ReadyPorts, ReadyReading }
222
+ export type { LaneUse, ReadyPorts, ReadyReading }
@@ -176,8 +176,8 @@ function is_candidate(call: GuardedCall): boolean {
176
176
  }
177
177
 
178
178
  // A refusable call is refused only where the mode is `refuse`. A lane child is `notice`, so it reaches
179
- // here and is withheld the refusal — the notice branch carries the guidance instead, because a refusal
180
- // ends a headless child's turn.
179
+ // here and is withheld the refusal — the notice branch carries the guidance instead, for the reason its
180
+ // `because` in `lane-guard-policy.ts` records.
181
181
  function should_block_here(tail: string, call: GuardedCall, refused_at_ms: number): boolean {
182
182
  if (duplicate_mode() !== 'refuse') return false
183
183
 
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { text } from 'node:stream/consumers'
3
3
  import { fileURLToPath } from 'node:url'
4
+ import { agent_session_role } from '#scripts/agent/agent-session-role'
4
5
  import { hook_decision, type GuardRun } from '#scripts/josh/hook-decision'
5
- import { lane_guard_policy, type LaneGuardMode } from '#scripts/lane/lane-guard-policy'
6
+ import { lane_guard_policy } from '#scripts/lane/lane-guard-policy'
6
7
  import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
7
8
  import { investigation_reads } from './investigation-reads'
8
9
 
@@ -21,91 +22,42 @@ import { investigation_reads } from './investigation-reads'
21
22
  // file — debugging the guard itself, or a session whose reading really is all edit targets.
22
23
  const SWITCH_ENV_KEY = 'JOSH_INVESTIGATION_GUARD'
23
24
 
24
- // This guard's refusal record, and the notice's own record — distinct so a lane-child notice never
25
- // spends the refusal's stamp, the same split the batching guard keeps.
26
25
  const STAMP_PREFIX = 'josh-investigation-guard-'
27
- const NOTICE_STAMP_PREFIX = 'josh-investigation-guard-notice-'
28
26
 
29
- // **How the guard behaves for the session in hand, decided from the one-place enumeration**.
30
- // Outside a lane child the mode is `refuse` and the
31
- // guard refuses; in a dispatched lane child it is `notice`, because a lane child does read unedited
32
- // files and can dispatch sub-agents, so the reading is there to send out. The enumeration reads `JOSH_LANE_CHILD` against this checkout's own
33
- // issue only for a guard whose mode it would change, so a person working in a lane sees the guard
34
- // unchanged. It wraps `investigation_reads.should_block` here rather than inside it so the pure counting
35
- // rule stays free of process state and its own suite keeps testing it directly.
36
- function investigation_mode(): LaneGuardMode {
37
- return lane_guard_policy.mode_here('investigation')
38
- }
39
-
40
- // A refusable call is refused only where the mode is `refuse` — the main line. A lane child is
41
- // `notice`, so it reaches the notice branch and is withheld the refusal that would end its headless
42
- // turn; an `off` some future enumeration sets stays silent here too.
27
+ // **How the guard behaves for the session in hand, decided from the one-place enumeration**. It is
28
+ // `refuse` in a dispatched lane child too (joshuafolkken/kit#3927): a hook denial returns to the model
29
+ // as guidance, and the notice it replaced was measured not to move the reading. It wraps
30
+ // `investigation_reads.should_block` here rather than inside it so the pure counting rule stays free of
31
+ // process state and its own suite keeps testing it directly. A reviewer session stands down: reading
32
+ // the diff's files is its whole task, and it carries the lane's marker, so it would otherwise be refused
33
+ // like the child it reviews — the stand-down `implementation-cut.ts` takes for the same session.
43
34
  function should_block(
44
35
  tail: string,
45
36
  call: GuardedCall,
46
37
  refused_at_ms: number,
47
38
  run?: GuardRun,
48
39
  ): boolean {
49
- if (investigation_mode() !== 'refuse') return false
40
+ if (
41
+ agent_session_role.is_reviewer() ||
42
+ lane_guard_policy.mode_here('investigation') !== 'refuse'
43
+ ) {
44
+ return false
45
+ }
50
46
 
51
47
  return investigation_reads.should_block(tail, call, refused_at_ms, run)
52
48
  }
53
49
 
54
- // The lane-child notice: the same threshold, delivered without a `permissionDecision`. It
55
- // reads the notify stamp's own record, so a notice never spends the refusal's stamp and re-arms on the
56
- // next accumulation exactly as the refusal does.
57
- function should_notify(
58
- tail: string,
59
- call: GuardedCall,
60
- notified_at_ms: number,
61
- run?: GuardRun,
62
- ): boolean {
63
- if (investigation_mode() !== 'notice') return false
64
-
65
- return investigation_reads.should_block(tail, call, notified_at_ms, run)
66
- }
67
-
68
- // **The lane-child counterpart of REASON, delivered as a notice rather than a refusal**.
69
- // A dispatched lane child ends its turn on a denial, so
70
- // the same guidance is carried without a `permissionDecision`: the read proceeds and the child is nudged
71
- // to delegate rather than killed. It carries no ⛔, so a person watching reads it as advice.
72
- const NOTICE = `💡 investigation: files read and not edited, or search turns, since the last delegated unit reached the threshold; the read proceeds. Run \`pnpm josh delegate investigation\`, then dispatch the \`${investigation_reads.INVESTIGATOR_AGENT}\` agent (\`kit:${investigation_reads.INVESTIGATOR_AGENT}\` in a consumer); it returns \`file:line\` citations, never the file text. Keep main-line reads to files you will edit.`
73
-
74
- // The notice as it reaches the model: the base guidance with the concrete unedited reads named, so a
75
- // lane child is shown *which* reads to send to a unit rather than only told to delegate. An empty
76
- // set names nothing and the notice falls back to its guidance alone. The call is part of the notify
77
- // contract but this guard's wording depends on the tail alone.
78
- function notice_text(_call: GuardedCall, tail: string): string {
79
- const pending = investigation_reads
80
- .tally_of(tail)
81
- .pending.map((absolute) => investigation_reads.repository_relative(absolute))
82
-
83
- return pending.length === 0
84
- ? NOTICE
85
- : `${NOTICE} Read and not edited so far: ${pending.join(' ')}.`
86
- }
87
-
88
50
  const GUARD = hook_decision.create_transcript_guard({
89
51
  prefix: STAMP_PREFIX,
90
52
  switch_key: SWITCH_ENV_KEY,
91
53
  is_candidate: investigation_reads.is_refusable_call,
92
54
  should_block,
93
55
  reason: investigation_reads.REASON,
94
- // The lane-child notice fires where the refusal is withheld, on a record of its own so it
95
- // never spends the refusal's stamp.
96
- notify: {
97
- prefix: NOTICE_STAMP_PREFIX,
98
- should_notify,
99
- text: notice_text,
100
- },
101
56
  })
102
57
 
103
58
  const investigation_refusal = GUARD.refusal
104
59
  const investigation_outcome = GUARD.outcome
105
60
  const { is_enabled, refusal_path } = GUARD
106
- // The notice's stamp path, exposed the way `refusal_path` is so a test can clean it apart from the
107
- // refusal's record.
108
- const investigation_notice_path = hook_decision.create_refusal_stamp(NOTICE_STAMP_PREFIX).path
109
61
  const { deny_envelope, load_environment_file, DISABLED_VALUES } = hook_decision
110
62
 
111
63
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
@@ -115,13 +67,11 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
115
67
 
116
68
  export {
117
69
  deny_envelope,
118
- investigation_notice_path,
119
70
  investigation_outcome,
120
71
  investigation_refusal,
121
72
  is_enabled,
122
73
  load_environment_file,
123
74
  refusal_path,
124
75
  DISABLED_VALUES,
125
- NOTICE,
126
76
  SWITCH_ENV_KEY,
127
77
  }
@@ -362,7 +362,6 @@ const investigation_reads = {
362
362
  is_session_artifact,
363
363
  is_subject_file,
364
364
  projected_count,
365
- repository_relative,
366
365
  resolved,
367
366
  should_block,
368
367
  tally_of,
@@ -73,18 +73,19 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
73
73
  { path: '.claude/skills/workflow-commands/prerequisite.md', bytes: 8192 },
74
74
  { path: '.claude/skills/workflow-commands/target-repository.md', bytes: 4096 },
75
75
  { path: '.claude/skills/workflow-commands/working-tree-hold.md', bytes: 8192 },
76
- { path: 'docs/josh-commands-automation.md', bytes: 45_056 },
77
- { path: 'docs/josh-commands-run.md', bytes: 61_440 },
78
- { path: 'docs/josh-commands-backlog.md', bytes: 49_152 },
79
- { path: 'docs/josh-commands.md', bytes: 45_056 },
76
+ { path: 'docs/josh-commands-automation.md', bytes: 49_152 },
77
+ { path: 'docs/josh-commands-run.md', bytes: 65_536 },
78
+ { path: 'docs/josh-commands-backlog.md', bytes: 53_248 },
79
+ { path: 'docs/josh-commands.md', bytes: 49_152 },
80
80
  { path: 'docs/labels-and-run-states.md', bytes: 8192 },
81
81
  { path: 'prompts/coding-standards.md', bytes: 4096 },
82
82
  { path: 'prompts/collaboration-workflow.md', bytes: 4096 },
83
- { path: 'prompts/collaboration-workflow/file-edits.md', bytes: 8192 },
83
+ { path: 'prompts/collaboration-workflow/file-edits.md', bytes: 4096 },
84
84
  { path: 'prompts/collaboration-workflow/gh-rest.md', bytes: 4096 },
85
85
  { path: 'prompts/collaboration-workflow/glossary.md', bytes: 4096 },
86
86
  { path: 'prompts/collaboration-workflow/issue-citation.md', bytes: 4096 },
87
87
  { path: 'prompts/collaboration-workflow/issue-template.md', bytes: 8192 },
88
+ { path: 'prompts/collaboration-workflow/lane-instructions.md', bytes: 4096 },
88
89
  { path: 'prompts/collaboration-workflow/operating-rules.md', bytes: 8192 },
89
90
  { path: 'prompts/collaboration-workflow/output-bounds.md', bytes: 4096 },
90
91
  { path: 'prompts/collaboration-workflow/overview.md', bytes: 4096 },
@@ -2,6 +2,7 @@
2
2
  import { existsSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
+ import { find_package_directory } from '#scripts/josh/package-directory'
5
6
  import { capped_output } from './capped-print'
6
7
  import { document_section } from './document-section'
7
8
  import { entry_read_set } from './entry-read-set'
@@ -35,14 +36,25 @@ const USAGE = 'Usage: josh doc:section <file.md> "<heading>"'
35
36
  // directory, a bare name that happened to exist beside the caller would beat the document under
36
37
  // `root` and print a section of the wrong file — silently, which is the failure this command exists
37
38
  // to refuse.
38
- function resolve_document(name: string, root: string = process.cwd()): string {
39
- const named = path.resolve(root, name)
39
+ //
40
+ // **The kit package is the fallback.** A consumer keeps no `prompts/` of its own — the package ships
41
+ // it under `node_modules/@joshuafolkken/kit/` — so `prompts/review.md` resolves there when `root` has
42
+ // no copy, exactly as `review:brief` resolves the rubric.
43
+ const PACKAGE_DIRECTORY = find_package_directory(path.dirname(fileURLToPath(import.meta.url)))
40
44
 
41
- if (existsSync(named)) return named
45
+ function candidates_under(directory: string, name: string): Array<string> {
46
+ return [path.resolve(directory, name), entry_read_set.document_path(directory, name)]
47
+ }
42
48
 
43
- const in_skill = entry_read_set.document_path(root, name)
49
+ function resolve_document(
50
+ name: string,
51
+ root: string = process.cwd(),
52
+ package_directory: string = PACKAGE_DIRECTORY,
53
+ ): string {
54
+ const named = path.resolve(root, name)
55
+ const candidates = [...candidates_under(root, name), ...candidates_under(package_directory, name)]
44
56
 
45
- return existsSync(in_skill) ? in_skill : named
57
+ return candidates.find((candidate) => existsSync(candidate)) ?? named
46
58
  }
47
59
 
48
60
  function report_missing(markdown: string, heading: string): number {
@@ -47,8 +47,8 @@ const ENTRY_READ_BUDGET: ReadonlyArray<EntryBudget> = [
47
47
  { entry: 'kickoff', bytes: 36_864 },
48
48
  { entry: 'fullrun', bytes: 53_248 },
49
49
  { entry: 'halfrun', bytes: 49_152 },
50
- { entry: 'prrun', bytes: 57_344 },
51
- { entry: 'backlogrun', bytes: 86_016 },
50
+ { entry: 'prrun', bytes: 53_248 },
51
+ { entry: 'backlogrun', bytes: 81_920 },
52
52
  { entry: LANE_CHILD, bytes: 45_056 },
53
53
  ]
54
54
 
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
+ import { lane_instructions } from '#scripts/agent/lane-instructions'
3
4
  import { backlogrun_parent_read_set } from './backlogrun-parent-read-set'
4
5
  import { entry_read_set, type ReadSetCost, type SectionCost } from './entry-read-set'
5
6
  import { lane_child_read_set } from './lane-child-read-set'
@@ -192,7 +193,9 @@ function total_lines(report: ReadSetCost, width: number): Array<string> {
192
193
  }
193
194
 
194
195
  function note_lines(report: ReadSetCost): Array<string> {
195
- if (report.entry === lane_child_read_set.LANE_CHILD) return [...LANE_CHILD_NOTE, '']
196
+ if (report.entry === lane_child_read_set.LANE_CHILD) {
197
+ return [...LANE_CHILD_NOTE, lane_instructions.resident_line(), '']
198
+ }
196
199
 
197
200
  if (report.entry === BACKLOGRUN) return [...BACKLOGRUN_PARENT_NOTE, '']
198
201
 
@@ -14,7 +14,6 @@ import { epic_parse } from './epic-parse'
14
14
  // the children the epic already names, while this one searches for issues the epic does *not* name.
15
15
 
16
16
  const SEARCH_LIMIT = 50
17
- const PARENT_MARKERS: ReadonlyArray<string> = ['親:', 'Parent:', '親:']
18
17
  // Named apart from `epic_audit_checks.ORPHAN_CHILD`: that check reports an orphan it found, this one
19
18
  // reports that the search for orphans was incomplete, and a reader has to be able to tell them apart.
20
19
  const ORPHAN_SEARCH = 'orphan search'
@@ -25,18 +24,15 @@ const CAPPED_BY_MATCHES = `The open-issue scan filled its ${String(SEARCH_LIMIT)
25
24
 
26
25
  const searched_issue_schema = z.object({ number: z.number(), body: z.string().nullable() })
27
26
 
28
- function has_parent_marker(line: string): boolean {
29
- return PARENT_MARKERS.some((marker) => line.includes(marker))
30
- }
31
-
32
27
  // Whether an issue's body names this epic as its parent, in the shape the issue template uses.
33
28
  //
34
29
  // Both halves must be on the *same line*. An issue parented to a different epic routinely backlinks
35
30
  // this one elsewhere in its body, and matching the marker and the number independently reported
36
31
  // every such issue as an orphan.
37
32
  //
38
- // The number is read through the same reference parse the checks use, so a parent line (a PARENT_MARKERS
39
- // prefix) such as `owner/other#858` names that repository's epic and not this one's.
33
+ // The number is read through the same reference parse the checks use, so a parent line (one
34
+ // `epic_audit_logic.has_parent_marker` accepts) such as `owner/other#858` names that repository's
35
+ // epic and not this one's.
40
36
  // The known set is this repository alone — a parent line names this epic or it names nothing here —
41
37
  // and it is passed rather than omitted so a repository whose own name contains a dot still recognizes
42
38
  // `joshuafolkken/site.com#858`.
@@ -49,7 +45,8 @@ function names_this_epic(line: string, epic_number: number, repo: string): boole
49
45
  function claims_parent(body: string | null, epic_number: number, repo: string): boolean {
50
46
  return (body ?? '')
51
47
  .split('\n')
52
- .some((line) => has_parent_marker(line) && names_this_epic(line, epic_number, repo))
48
+ .filter((line) => epic_audit_logic.has_parent_marker(line))
49
+ .some((line) => names_this_epic(line, epic_number, repo))
53
50
  }
54
51
 
55
52
  // Why the scan stopped before the end of the open backlog, or that it did not. Both cutoffs hide the
@@ -180,6 +180,55 @@ function acceptance_section(body: string | undefined): string {
180
180
  return markdown_section.section_lines_matching(body, is_acceptance_heading).join('\n')
181
181
  }
182
182
 
183
+ // Where a body reference that counts as a relation was written. Only these two places state what the
184
+ // issue delivers or where it belongs; a number in the background, symptom or origin section is the
185
+ // example that prompted the filing, and reading it as a relation bundled issues whose work never
186
+ // overlaps.
187
+ type SignalSection = 'acceptance criteria' | 'parent line'
188
+
189
+ interface SignalReference {
190
+ number: number
191
+ section: SignalSection
192
+ }
193
+
194
+ // What marks a line as naming the issue's parent, in both spellings for the same reason as
195
+ // `ACCEPTANCE_HEADINGS`. `epic:audit`'s orphan search matches it anywhere in the line, then asks
196
+ // whether the same line names the epic.
197
+ const PARENT_MARKERS: ReadonlyArray<string> = ['親:', '親:', 'Parent:', 'parent:']
198
+
199
+ function has_parent_marker(line: string): boolean {
200
+ return PARENT_MARKERS.some((marker) => line.includes(marker))
201
+ }
202
+
203
+ // The reference written directly after a parent marker, bold markup allowed (`- **Parent:** #N`).
204
+ // Only that reference is the parent: a background sentence that mentions `parent:` and names an
205
+ // issue elsewhere on the line is not a parent line.
206
+ const PARENT_REFERENCE = /(?:親|Parent|parent)\**[::]\**\s*(?:[\w.-]+\/[\w.-]+)?#\d+/gu
207
+
208
+ function parent_references(body: string): string {
209
+ return markdown_section
210
+ .unfenced_lines(body)
211
+ .flatMap((line) => [...line.matchAll(PARENT_REFERENCE)].map((match) => match[0]))
212
+ .join('\n')
213
+ }
214
+
215
+ function references_in(text: string, repo: string, section: SignalSection): Array<SignalReference> {
216
+ return parse_references(text, repo).map((number) => ({ number, section }))
217
+ }
218
+
219
+ // The same-repository references a body makes in the places that count as a relation, each with the
220
+ // section it was read from. A number both places name is reported once, at the first.
221
+ function signal_references(body: string, repo = ''): Array<SignalReference> {
222
+ const read = [
223
+ ...references_in(acceptance_section(body), repo, 'acceptance criteria'),
224
+ ...references_in(parent_references(body), repo, 'parent line'),
225
+ ]
226
+
227
+ return read.filter((reference, index) => {
228
+ return read.findIndex((other) => other.number === reference.number) === index
229
+ })
230
+ }
231
+
183
232
  // Everything `node` depends on, directly or through a chain. Walked iteratively so a graph that
184
233
  // still contains a cycle terminates — `epic:next` rejects one first, but an auditor must not hang on
185
234
  // the input it exists to examine.
@@ -223,8 +272,10 @@ const epic_audit_logic = {
223
272
  parse_issue_references,
224
273
  parse_references,
225
274
  acceptance_section,
275
+ signal_references,
276
+ has_parent_marker,
226
277
  depends_on,
227
278
  }
228
279
 
229
- export type { AuditFinding, FindingLevel, ReferenceState }
280
+ export type { AuditFinding, FindingLevel, ReferenceState, SignalReference, SignalSection }
230
281
  export { epic_audit_logic }