@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
@@ -1,4 +1,5 @@
1
1
  import { issue_cite } from '#scripts/issue/issue-cite'
2
+ import type { SignalSection } from './epic-audit'
2
3
  import { epic_bundle, type BacklogIssue } from './epic-bundle'
3
4
 
4
5
  // The facts the bundle decision was already made from, printed beside the order it prints.
@@ -20,10 +21,26 @@ import { epic_bundle, type BacklogIssue } from './epic-bundle'
20
21
  type EvidenceKind = 'dependency' | 'reference'
21
22
 
22
23
  // One fact, and the order it points to. `first` is the issue the fact puts ahead of `second`.
24
+ // `section` is where a reference was read, so the reader sees it was the acceptance criteria or the
25
+ // parent line that counted, not a mention elsewhere in the body.
23
26
  interface OrderingEvidence {
24
27
  kind: EvidenceKind
25
28
  first: number
26
29
  second: number
30
+ section?: SignalSection
31
+ }
32
+
33
+ // What one directed test found: nothing, or the fact with whatever detail it carries.
34
+ type EvidenceFound = Pick<OrderingEvidence, 'section'> | undefined
35
+
36
+ function blocker_found(from: BacklogIssue, to: BacklogIssue): EvidenceFound {
37
+ return epic_bundle.names_as_blocker(from, to) ? {} : undefined
38
+ }
39
+
40
+ function reference_found(from: BacklogIssue, to: BacklogIssue): EvidenceFound {
41
+ const section = epic_bundle.reference_section(from, to)
42
+
43
+ return section === undefined ? undefined : { section }
27
44
  }
28
45
 
29
46
  // The two readings, each written as a directed test of one issue against another. Both are
@@ -34,14 +51,28 @@ interface OrderingEvidence {
34
51
  // `blocked-by` says so outright: the blocker has to land before the issue that declares it. A
35
52
  // reference says it by having been written — the issue that names another was drafted with that
36
53
  // other one already in existence, so the named one is the earlier of the two.
37
- const EVIDENCE_TESTS: ReadonlyArray<{
54
+ interface EvidenceTest {
38
55
  kind: EvidenceKind
39
- holds: (from: BacklogIssue, to: BacklogIssue) => boolean
40
- }> = [
41
- { kind: 'dependency', holds: (from, to) => epic_bundle.names_as_blocker(from, to) },
42
- { kind: 'reference', holds: (from, to) => epic_bundle.names_in_body(from, to) },
56
+ find: (from: BacklogIssue, to: BacklogIssue) => EvidenceFound
57
+ }
58
+
59
+ const EVIDENCE_TESTS: ReadonlyArray<EvidenceTest> = [
60
+ { kind: 'dependency', find: blocker_found },
61
+ { kind: 'reference', find: reference_found },
43
62
  ]
44
63
 
64
+ // One test in one direction: the fact it found, or none.
65
+ function directed_evidence(
66
+ test: EvidenceTest,
67
+ from: BacklogIssue,
68
+ to: BacklogIssue,
69
+ ): Array<OrderingEvidence> {
70
+ const found = test.find(from, to)
71
+ if (found === undefined) return []
72
+
73
+ return [{ kind: test.kind, first: to.number, second: from.number, ...found }]
74
+ }
75
+
45
76
  // Both directions of one pair, because both are facts. Two issues that name each other produce two
46
77
  // lines pointing opposite ways, which is the honest report: the reader is the one deciding.
47
78
  function pair_evidence(subject: BacklogIssue, other: BacklogIssue): Array<OrderingEvidence> {
@@ -50,10 +81,8 @@ function pair_evidence(subject: BacklogIssue, other: BacklogIssue): Array<Orderi
50
81
  { from: other, to: subject },
51
82
  ]
52
83
 
53
- return EVIDENCE_TESTS.flatMap(({ kind, holds }) =>
54
- directions
55
- .filter((pair) => holds(pair.from, pair.to))
56
- .map((pair) => ({ kind, first: pair.to.number, second: pair.from.number })),
84
+ return EVIDENCE_TESTS.flatMap((test) =>
85
+ directions.flatMap((pair) => directed_evidence(test, pair.from, pair.to)),
57
86
  )
58
87
  }
59
88
 
@@ -85,8 +114,8 @@ function ordering_evidence(
85
114
  return member_pairs(members).flatMap((pair) => pair_evidence(pair.left, pair.right))
86
115
  }
87
116
 
88
- // How each kind reads. `where` says the fact came from prose rather than from a recorded relation,
89
- // and the two `order_*` halves say how strongly the fact settles the order.
117
+ // How each kind reads. The two `order_*` halves say how strongly the fact settles the order; where a
118
+ // reference was read comes from the fact itself (`section_phrase`).
90
119
  //
91
120
  // **A recorded dependency states the order; a reference only points at it.** A `blocked-by` says
92
121
  // outright that the blocker lands first. A reference says it by having been written — which holds
@@ -95,20 +124,14 @@ function ordering_evidence(
95
124
  // the reference line reports the direction it points to and asserts nothing further; deciding is
96
125
  // still the reader's.
97
126
  const EVIDENCE_WORDING: Readonly<
98
- Record<EvidenceKind, { verb: string; where: string; order_prefix: string; order_suffix: string }>
127
+ Record<EvidenceKind, { verb: string; order_prefix: string; order_suffix: string }>
99
128
  > = {
100
- dependency: {
101
- verb: 'is recorded as blocked by',
102
- where: '',
103
- order_prefix: '',
104
- order_suffix: ' comes first',
105
- },
106
- reference: {
107
- verb: 'names',
108
- where: ' in its body',
109
- order_prefix: 'that points to ',
110
- order_suffix: ' first',
111
- },
129
+ dependency: { verb: 'is recorded as blocked by', order_prefix: '', order_suffix: ' comes first' },
130
+ reference: { verb: 'names', order_prefix: 'that points to ', order_suffix: ' first' },
131
+ }
132
+
133
+ function section_phrase(item: OrderingEvidence): string {
134
+ return item.section === undefined ? '' : ` in its ${item.section}`
112
135
  }
113
136
 
114
137
  const EVIDENCE_HEADING = ' Evidence:'
@@ -117,12 +140,12 @@ const EVIDENCE_INDENT_WIDTH = 4
117
140
  const EVIDENCE_INDENT = ' '.repeat(EVIDENCE_INDENT_WIDTH)
118
141
 
119
142
  function format_one(item: OrderingEvidence): string {
120
- const { verb, where, order_prefix, order_suffix } = EVIDENCE_WORDING[item.kind]
143
+ const { verb, order_prefix, order_suffix } = EVIDENCE_WORDING[item.kind]
121
144
  const first = issue_cite.plain(item.first)
122
145
  const second = issue_cite.plain(item.second)
123
146
  const order = `${order_prefix}${first}${order_suffix}`
124
147
 
125
- return `${EVIDENCE_INDENT}${second} ${verb} ${first}${where} — ${order}`
148
+ return `${EVIDENCE_INDENT}${second} ${verb} ${first}${section_phrase(item)} — ${order}`
126
149
  }
127
150
 
128
151
  // The block that goes under `Order:`. Empty when neither reading found anything, which leaves the
@@ -44,10 +44,21 @@ interface ReferencedLookups {
44
44
  dropped: Array<number>
45
45
  }
46
46
 
47
+ // Every number the body names, the ones a bundle signal counts first: the cap below must not leave a
48
+ // signal unread because twenty background mentions came ahead of it in the body.
49
+ function signal_first(subject: BacklogIssue): Array<number> {
50
+ const signals = epic_audit_logic
51
+ .signal_references(subject.body, subject.repo)
52
+ .map((reference) => reference.number)
53
+ const all = epic_audit_logic.parse_references(subject.body, subject.repo)
54
+
55
+ return [...new Set([...signals, ...all])]
56
+ }
57
+
47
58
  function referenced_lookups(subject: BacklogIssue, known: ReadonlySet<number>): ReferencedLookups {
48
- const candidates = epic_audit_logic
49
- .parse_references(subject.body, subject.repo)
50
- .filter((number) => number !== subject.number && !known.has(number))
59
+ const candidates = signal_first(subject).filter(
60
+ (number) => number !== subject.number && !known.has(number),
61
+ )
51
62
 
52
63
  return {
53
64
  numbers: candidates.slice(0, REFERENCED_LOOKUP_LIMIT),
@@ -1,6 +1,6 @@
1
1
  import { issue_cite } from '#scripts/issue/issue-cite'
2
2
  import { has_label_name, RELEASE_LABEL } from '#scripts/issue/issue-labels'
3
- import { epic_audit_logic } from './epic-audit'
3
+ import { epic_audit_logic, type SignalSection } from './epic-audit'
4
4
  import { epic_graph } from './epic-graph'
5
5
  import type { DependencyLink } from './epic-parse'
6
6
  import type { IssueReference } from './epic-reference'
@@ -11,9 +11,9 @@ import type { IssueReference } from './epic-reference'
11
11
  // not reach the other way in: two issues filed days apart that turn out to be the front and back of
12
12
  // one job are executed separately, in whatever order, with the reasoning recorded nowhere.
13
13
  //
14
- // The candidate search reads issue references out of prose — the same reading `epic:audit` does
15
- // inside one epic. That analysis is imported rather than written a second time; the only
16
- // difference is what it is pointed at.
14
+ // The candidate search reads issue references out of the acceptance criteria and the `parent: #N`
15
+ // line — the same reading `epic:audit` does inside one epic. That analysis is imported rather than
16
+ // written a second time; the only difference is what it is pointed at.
17
17
 
18
18
  // An open issue as the search sees it.
19
19
  interface BacklogIssue {
@@ -59,11 +59,19 @@ interface BundleDecision {
59
59
  reason: string
60
60
  }
61
61
 
62
- // Whether `from`'s body names `to`. One direction of the citation test, because which direction it
63
- // was is what `epic-bundle-evidence.ts` reports — and the read is single-sourced here rather than
64
- // written a second time there, so the candidate search and the evidence beside it cannot drift apart.
62
+ // Where `from`'s body names `to` in a place that counts as a relation — its acceptance criteria or
63
+ // its `parent: #N` line — or nothing. One direction of the citation test, because which direction
64
+ // and which section it was is what `epic-bundle-evidence.ts` reports — and the read is single-sourced
65
+ // here rather than written a second time there, so the candidate search and the evidence beside it
66
+ // cannot drift apart. A number written only as the background of a filing is not a relation.
67
+ function reference_section(from: BacklogIssue, to: BacklogIssue): SignalSection | undefined {
68
+ return epic_audit_logic
69
+ .signal_references(from.body, from.repo)
70
+ .find((reference) => reference.number === to.number)?.section
71
+ }
72
+
65
73
  function names_in_body(from: BacklogIssue, to: BacklogIssue): boolean {
66
- return epic_audit_logic.parse_references(from.body, from.repo).includes(to.number)
74
+ return reference_section(from, to) !== undefined
67
75
  }
68
76
 
69
77
  // Whether two issues cite each other, in either direction. A one-way citation is enough: the point
@@ -324,7 +332,7 @@ function bundle_children(
324
332
 
325
333
  const epic_bundle = {
326
334
  names_as_blocker,
327
- names_in_body,
335
+ reference_section,
328
336
  NO_SIGNAL_REASON,
329
337
  already_tracked_reason,
330
338
  has_recorded_dependency,
@@ -7,6 +7,7 @@ import { issue_watch_gate } from '#scripts/issue/issue-watch-gate'
7
7
  import { cli_body } from '#scripts/josh/cli-body'
8
8
  import { josh_environment_file } from '#scripts/josh/josh-environment-file'
9
9
  import { git_notify, type GitNotifyConfig } from '#scripts/notify/git-notify'
10
+ import { acceptance_map_check } from '#scripts/review/acceptance-map-check'
10
11
  import { live_evidence } from '#scripts/review/live-evidence'
11
12
  import { review_attest } from '#scripts/review/review-attest'
12
13
  import { review_record } from '#scripts/review/review-record'
@@ -143,7 +144,7 @@ function is_merge_permitted(status: string): boolean {
143
144
  return status === 'ok' || status === 'not-required'
144
145
  }
145
146
 
146
- function is_valid_issue(issue_number: string | undefined): boolean {
147
+ function is_valid_issue(issue_number: string | undefined): issue_number is string {
147
148
  return issue_number !== undefined && issue_number_shape.ISSUE_NUMBER_PATTERN.test(issue_number)
148
149
  }
149
150
 
@@ -194,6 +195,21 @@ async function assert_live_evidence(should_merge: boolean, branch_name: string):
194
195
  throw new Error(live_evidence.refusal_message())
195
196
  }
196
197
 
198
+ // Every acceptance criterion the Issue carries at the merge maps to a row in the pull request body —
199
+ // `acceptance_map` carries the why. Re-read here rather than at the commit, so a criterion added
200
+ // during the review is refused too. A run whose issue it cannot identify has no criteria to read.
201
+ async function assert_acceptance_mapped(
202
+ should_merge: boolean,
203
+ issue_number: string | undefined,
204
+ branch_name: string,
205
+ ): Promise<void> {
206
+ if (!should_merge || !is_valid_issue(issue_number)) return
207
+
208
+ const refusal = await acceptance_map_check.refusal(issue_number, branch_name)
209
+
210
+ if (refusal !== undefined) throw new Error(refusal)
211
+ }
212
+
197
213
  // Every pre-merge refusal, in order — each one throws, so nothing merges past the first that fails.
198
214
  async function assert_merge_gates(
199
215
  should_merge: boolean,
@@ -203,6 +219,7 @@ async function assert_merge_gates(
203
219
  await assert_review_attested(should_merge)
204
220
  await assert_review_recorded(should_merge, issue_number)
205
221
  await assert_live_evidence(should_merge, branch_name)
222
+ await assert_acceptance_mapped(should_merge, issue_number, branch_name)
206
223
  }
207
224
 
208
225
  // After the gates, before the CI wait: a line appended since the run's commit rides the pull request
@@ -261,6 +278,7 @@ try {
261
278
  }
262
279
 
263
280
  const git_followup_workflow = {
281
+ assert_acceptance_mapped,
264
282
  assert_live_evidence,
265
283
  assert_review_attested,
266
284
  assert_review_recorded,
@@ -0,0 +1,129 @@
1
+ import path from 'node:path'
2
+ import { lint_related_scope } from '#scripts/lint/lint-related-scope'
3
+ import { gate_plan } from './gate-plan'
4
+
5
+ // Which paths each gate check reads, so a gate re-run reuses a check none of whose inputs moved
6
+ // (`check-reuse.ts`). **A check with no scope here always runs**: `cspell` reads every
7
+ // text file, the unit suite reads documents and fixtures as well as code, `behavior` reads a transcript
8
+ // outside the tree and `metrics` reads whatever it measures. Naming a scope for any of them would be a
9
+ // guess, and a wrong guess is a check reported green without having run.
10
+ //
11
+ // **The scopes over-approximate, never under**: a path in doubt is an input. `check` and `exports`
12
+ // read source modules and the JSON they import. `lint` is prettier plus eslint, so its inputs are those
13
+ // sources plus every file prettier formats — the extensions `lint:related` already selects, less what
14
+ // prettier's own ignore files leave out (`.claude/**/*.md`). Prettier answers that itself through
15
+ // `getFileInfo`, so the two ignore files stay the single source.
16
+ //
17
+ // **A configuration file is an input of every scoped check**: a manifest or lock file moves the
18
+ // installed toolchain, and an ignore or editor file moves what prettier reads.
19
+
20
+ const SOURCE_EXTENSIONS: ReadonlySet<string> = new Set([
21
+ '.ts',
22
+ '.tsx',
23
+ '.mts',
24
+ '.cts',
25
+ '.js',
26
+ '.jsx',
27
+ '.mjs',
28
+ '.cjs',
29
+ '.svelte',
30
+ '.vue',
31
+ '.json',
32
+ '.jsonc',
33
+ ])
34
+
35
+ const GIT_IGNORE = '.gitignore'
36
+ const PRETTIER_IGNORE = '.prettierignore'
37
+ const PRETTIER_IGNORE_FILES = [GIT_IGNORE, PRETTIER_IGNORE] as const
38
+
39
+ const CONFIG_FILES: ReadonlySet<string> = new Set([
40
+ 'package.json',
41
+ 'pnpm-lock.yaml',
42
+ 'pnpm-workspace.yaml',
43
+ 'package-lock.json',
44
+ 'yarn.lock',
45
+ 'bun.lock',
46
+ 'bun.lockb',
47
+ '.npmrc',
48
+ '.editorconfig',
49
+ ...PRETTIER_IGNORE_FILES,
50
+ ])
51
+
52
+ const PRETTIER_CONFIG_PREFIX = '.prettierrc'
53
+
54
+ // Whether prettier would leave `relative` out; `false` when prettier cannot be asked, which keeps the
55
+ // path an input.
56
+ type PrettierIgnored = (relative: string) => Promise<boolean>
57
+
58
+ type InputTest = (relative: string, is_ignored: PrettierIgnored) => Promise<boolean>
59
+
60
+ function is_config(relative: string): boolean {
61
+ const name = path.basename(relative)
62
+
63
+ return CONFIG_FILES.has(name) || name.startsWith(PRETTIER_CONFIG_PREFIX)
64
+ }
65
+
66
+ function is_source(relative: string): boolean {
67
+ return SOURCE_EXTENSIONS.has(path.extname(relative))
68
+ }
69
+
70
+ async function is_lint_input(relative: string, is_ignored: PrettierIgnored): Promise<boolean> {
71
+ if (is_source(relative)) return true
72
+ if (!lint_related_scope.is_lintable(relative)) return false
73
+
74
+ return !(await is_ignored(relative))
75
+ }
76
+
77
+ async function is_source_input(relative: string): Promise<boolean> {
78
+ return is_source(relative)
79
+ }
80
+
81
+ function prettier_ignored(root: string): PrettierIgnored {
82
+ const ignore_path = PRETTIER_IGNORE_FILES.map((name) => path.join(root, name))
83
+
84
+ return async function is_ignored(relative: string): Promise<boolean> {
85
+ try {
86
+ const prettier = await import('prettier')
87
+ const info = await prettier.getFileInfo(path.join(root, relative), {
88
+ ignorePath: ignore_path,
89
+ })
90
+
91
+ return info.ignored
92
+ } catch {
93
+ return false
94
+ }
95
+ }
96
+ }
97
+
98
+ const SCOPES: ReadonlyMap<string, InputTest> = new Map([
99
+ [gate_plan.LINT_LABEL, is_lint_input],
100
+ [gate_plan.TYPE_CHECK_LABEL, is_source_input],
101
+ [gate_plan.EXPORTS_LABEL, is_source_input],
102
+ ])
103
+
104
+ function is_scoped(label: string): boolean {
105
+ return SCOPES.has(label)
106
+ }
107
+
108
+ // Whether any of `paths` (repository-root relative) is an input of the check `label`. An unscoped
109
+ // check answers `true` for every set of paths, so the only way to reuse it is never.
110
+ async function touches_inputs(
111
+ label: string,
112
+ paths: ReadonlyArray<string>,
113
+ is_ignored: PrettierIgnored,
114
+ ): Promise<boolean> {
115
+ const scope = SCOPES.get(label)
116
+
117
+ if (scope === undefined || paths.some((relative) => is_config(relative))) return true
118
+
119
+ const answers = await Promise.all(
120
+ paths.map(async (relative) => await scope(relative, is_ignored)),
121
+ )
122
+
123
+ return answers.includes(true)
124
+ }
125
+
126
+ const check_inputs = { is_scoped, prettier_ignored, touches_inputs }
127
+
128
+ export type { PrettierIgnored }
129
+ export { check_inputs }
@@ -0,0 +1,162 @@
1
+ import { git_command } from '#scripts/git/git-command'
2
+ import { git_spawn } from '#scripts/git/git-spawn'
3
+ import { PROJECT_ROOT } from '#scripts/init/init-paths'
4
+ import { file_map_stamp, type FileMapStamp } from '#scripts/josh/file-map-stamp'
5
+ import { stamp_file } from '#scripts/josh/stamp-file'
6
+ import { review_tree } from '#scripts/review/review-tree'
7
+ import { check_inputs, type PrettierIgnored } from './check-inputs'
8
+ import { gate_report, type GateStepResult } from './gate-report'
9
+ import type { GateTree } from './gate-tree'
10
+
11
+ // One green record per scoped check, so a gate re-run starts only the checks whose inputs moved
12
+ // (`check-inputs.ts` decides what each reads). The whole-gate record (`gate-skip.ts`) is refused the
13
+ // moment the merge base moves; a main sync that brought in only `.claude/**/*.md` then re-ran lint and
14
+ // the type check over sources neither had a reason to read again.
15
+ //
16
+ // **What moved is the union of two diffs**, and together they are every path whose content can differ
17
+ // from the tree the record was taken on. The changed-file map is a diff against the merge base, so a
18
+ // path absent from both maps holds the merge base's content — and the base's own diff
19
+ // (`git diff <recorded> <current>`) names every path that content moved for. A path in either map whose
20
+ // digest differs is the other half (`file_map_stamp.changed_since`).
21
+ //
22
+ // **A record is reuse of a result, not a check dropped**: no base, an empty changed-file map, an
23
+ // unreadable base diff or no root answers "not reusable", and a check with no scope is never reusable. The records are written and read
24
+ // only where the CLI entry resolved their destination (`CheckRecords`), so a suite driving the gate
25
+ // never plants a green record over the live tree.
26
+
27
+ const CHECK_PREFIX = 'josh-gate-check-'
28
+ const NAME_SEPARATOR = '\0'
29
+ const BASE_DIFF = ['diff', '--name-only', '--no-renames', '-z'] as const
30
+
31
+ const check_stamp = file_map_stamp.create(CHECK_PREFIX, PROJECT_ROOT)
32
+
33
+ // Where each check's record lives — a parameter so a test plants records in a directory of its own.
34
+ type CheckRecords = (label: string) => string
35
+
36
+ function default_records(label: string): string {
37
+ return stamp_file.stamp_path(`${CHECK_PREFIX}${label}-`, PROJECT_ROOT)
38
+ }
39
+
40
+ async function base_diff(from: string, to: string): Promise<ReadonlyArray<string> | undefined> {
41
+ if (from === to) return []
42
+
43
+ try {
44
+ const names = await git_spawn.read([...BASE_DIFF, from, to])
45
+
46
+ return names.split(NAME_SEPARATOR).filter((name) => name !== '')
47
+ } catch {
48
+ return undefined
49
+ }
50
+ }
51
+
52
+ async function moved_since(
53
+ stamp: FileMapStamp,
54
+ tree: GateTree,
55
+ ): Promise<ReadonlyArray<string> | undefined> {
56
+ // An empty map is also what `gate-tree.ts` answers when git could not be read, so it describes no
57
+ // tree to diff against — the reading `gate_skip.gate_reuse` refuses too.
58
+ const is_unread = Object.keys(tree.files).length === 0
59
+
60
+ if (is_unread || stamp.base === undefined || tree.base === undefined) return undefined
61
+
62
+ const moved_base = await base_diff(stamp.base, tree.base)
63
+
64
+ if (moved_base === undefined) return undefined
65
+
66
+ return [...file_map_stamp.changed_since(stamp, tree.files), ...moved_base]
67
+ }
68
+
69
+ interface ReuseContext {
70
+ tree: GateTree
71
+ records: CheckRecords
72
+ is_ignored: PrettierIgnored
73
+ }
74
+
75
+ async function is_stamp_reusable(
76
+ label: string,
77
+ stamp: FileMapStamp,
78
+ context: ReuseContext,
79
+ ): Promise<boolean> {
80
+ const moved = await moved_since(stamp, context.tree)
81
+
82
+ if (moved === undefined) return false
83
+
84
+ return !(await check_inputs.touches_inputs(label, moved, context.is_ignored))
85
+ }
86
+
87
+ // The time the check's record was taken, or `undefined` when the check has to run.
88
+ async function reused_at(label: string, context: ReuseContext): Promise<string | undefined> {
89
+ const stamp = check_inputs.is_scoped(label) ? check_stamp.read(context.records(label)) : undefined
90
+
91
+ if (stamp === undefined) return undefined
92
+
93
+ return (await is_stamp_reusable(label, stamp, context)) ? stamp.taken_at : undefined
94
+ }
95
+
96
+ async function read_root(): Promise<string | undefined> {
97
+ try {
98
+ return await git_command.repository_root()
99
+ } catch {
100
+ return undefined
101
+ }
102
+ }
103
+
104
+ // Each reusable check's label, mapped to when it was last green.
105
+ async function reusable_checks(
106
+ labels: ReadonlyArray<string>,
107
+ tree: GateTree,
108
+ records: CheckRecords = default_records,
109
+ ): Promise<ReadonlyMap<string, string>> {
110
+ const root = await read_root()
111
+
112
+ if (root === undefined) return new Map()
113
+
114
+ const context = { tree, records, is_ignored: check_inputs.prettier_ignored(root) }
115
+ const answers = await Promise.all(
116
+ labels.map(async (label) => [label, await reused_at(label, context)] as const),
117
+ )
118
+
119
+ return new Map(answers.flatMap(([label, at]) => (at === undefined ? [] : [[label, at] as const])))
120
+ }
121
+
122
+ function is_recordable(result: GateStepResult): boolean {
123
+ return (
124
+ check_inputs.is_scoped(result.label) &&
125
+ !gate_report.is_gate_step_failed(result) &&
126
+ gate_report.has_nothing_to_say(result)
127
+ )
128
+ }
129
+
130
+ function write_records(
131
+ results: ReadonlyArray<GateStepResult>,
132
+ tree: GateTree,
133
+ records: CheckRecords,
134
+ ): void {
135
+ const recordable = results.filter((result) => is_recordable(result))
136
+
137
+ for (const result of recordable) check_stamp.write(tree.files, records(result.label), tree.base)
138
+ }
139
+
140
+ // Every scoped check that passed silently, recorded against the tree it read — a red gate's green
141
+ // checks included, since each record answers for its own check alone. Nothing is written when the tree
142
+ // moved while the checks ran, and a failed write leaves no record, the safe direction either way.
143
+ async function record_green_checks(
144
+ results: ReadonlyArray<GateStepResult>,
145
+ tree: GateTree,
146
+ records: CheckRecords = default_records,
147
+ ): Promise<void> {
148
+ try {
149
+ const after = await review_tree.read_changed_tree()
150
+
151
+ if (JSON.stringify(after) !== JSON.stringify(tree.files)) return
152
+
153
+ write_records(results, { files: after, base: tree.base }, records)
154
+ } catch {
155
+ /* no record is the safe answer */
156
+ }
157
+ }
158
+
159
+ const check_reuse = { DEFAULT_RECORDS: default_records, record_green_checks, reusable_checks }
160
+
161
+ export type { CheckRecords }
162
+ export { check_reuse }