@joshuafolkken/kit 1.1136.0 → 1.1178.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 (200) hide show
  1. package/.claude/settings.json +4 -4
  2. package/.claude/skills/epic-commands/SKILL.md +27 -239
  3. package/.claude/skills/epic-commands/epic-bundle.md +115 -0
  4. package/.claude/skills/epic-commands/execution-waves.md +126 -0
  5. package/.claude/skills/workflow-commands/SKILL.md +48 -194
  6. package/.claude/skills/workflow-commands/background-commands.md +1 -1
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +12 -123
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +14 -63
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +16 -15
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -173
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +155 -0
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +65 -144
  13. package/.claude/skills/workflow-commands/backlogrun.md +5 -4
  14. package/.claude/skills/workflow-commands/chain-rule.md +3 -2
  15. package/.claude/skills/workflow-commands/delegation.md +2 -2
  16. package/.claude/skills/workflow-commands/entry-sequence.md +62 -0
  17. package/.claude/skills/workflow-commands/followup.md +104 -181
  18. package/.claude/skills/workflow-commands/fullrun-steps.md +8 -9
  19. package/.claude/skills/workflow-commands/fullrun.md +16 -73
  20. package/.claude/skills/workflow-commands/halfrun.md +10 -36
  21. package/.claude/skills/workflow-commands/into-target.md +4 -4
  22. package/.claude/skills/workflow-commands/issue-comments.md +7 -8
  23. package/.claude/skills/workflow-commands/issue-scout.md +6 -6
  24. package/.claude/skills/workflow-commands/kickoff.md +6 -6
  25. package/.claude/skills/workflow-commands/needs-human-review.md +6 -6
  26. package/.claude/skills/workflow-commands/observation-filing.md +21 -172
  27. package/.claude/skills/workflow-commands/observation-ledger.md +150 -0
  28. package/.claude/skills/workflow-commands/pre-gate-cut.md +35 -95
  29. package/.claude/skills/workflow-commands/prerequisite.md +6 -6
  30. package/.claude/skills/workflow-commands/progress-watcher.md +90 -0
  31. package/.claude/skills/workflow-commands/prrun.md +5 -5
  32. package/.claude/skills/workflow-commands/retrospective.md +9 -9
  33. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  34. package/.claude/skills/workflow-commands/target-repository.md +2 -2
  35. package/.claude/skills/workflow-commands/working-tree-hold.md +7 -5
  36. package/.codex/hooks.json +4 -4
  37. package/.github/workflows/ci.yml +15 -2
  38. package/AGENTS.md +1 -6
  39. package/CLAUDE.md +58 -120
  40. package/GEMINI.md +1 -6
  41. package/dist/CLAUDE.md +58 -120
  42. package/dist/hooks/batch-guard.js +4 -4
  43. package/dist/hooks/{chunk-NFZGFGOR.js → chunk-32GQTL4F.js} +14 -1
  44. package/dist/hooks/{chunk-HBKZLITF.js → chunk-DO6NWWHU.js} +4 -4
  45. package/dist/hooks/{chunk-KBXZLMRH.js → chunk-G6KRCKEC.js} +216 -76
  46. package/dist/hooks/{chunk-JKKTVDAG.js → chunk-GDS5N2KB.js} +186 -309
  47. package/dist/hooks/chunk-I4XSL4TT.js +56 -0
  48. package/dist/hooks/{chunk-GDU26K3J.js → chunk-S6X6WYKR.js} +886 -174
  49. package/dist/hooks/{chunk-FO2I7UT7.js → chunk-UAGLLIGZ.js} +1 -1
  50. package/dist/hooks/{chunk-RFWWUI5F.js → chunk-VESKRQOF.js} +3 -3
  51. package/dist/hooks/{chunk-KOKOAV6D.js → chunk-VQLLNFNT.js} +6 -4
  52. package/dist/hooks/{chunk-LJBLGHEH.js → chunk-ZAL3NRGL.js} +4 -6
  53. package/dist/hooks/codex-hook-adapter.js +10 -10
  54. package/dist/hooks/duplicate-read-guard.js +5 -5
  55. package/dist/hooks/format-edited.js +2 -2
  56. package/dist/hooks/inputs.json +178 -177
  57. package/dist/hooks/investigation-guard.js +5 -5
  58. package/dist/hooks/pretool-guard.js +11 -10
  59. package/dist/hooks/stop-guard.js +20 -6
  60. package/dist/josh.js +29 -3
  61. package/dist/version/index.js +5 -1
  62. package/package.json +2 -1
  63. package/prompts/collaboration-workflow/file-edits.md +11 -10
  64. package/prompts/collaboration-workflow/gh-rest.md +2 -4
  65. package/prompts/collaboration-workflow/glossary.md +1 -1
  66. package/prompts/collaboration-workflow/issue-citation.md +5 -9
  67. package/prompts/collaboration-workflow/issue-template.md +40 -115
  68. package/prompts/collaboration-workflow/operating-rules.md +4 -5
  69. package/prompts/collaboration-workflow/output-bounds.md +12 -78
  70. package/prompts/collaboration-workflow/overview.md +7 -13
  71. package/prompts/collaboration-workflow/plan-comment.md +11 -39
  72. package/prompts/collaboration-workflow/principles.md +3 -4
  73. package/prompts/collaboration-workflow/report-format.md +28 -74
  74. package/prompts/collaboration-workflow/residency.md +12 -109
  75. package/prompts/collaboration-workflow/rule-delivery.md +98 -97
  76. package/prompts/collaboration-workflow/shell-body.md +3 -13
  77. package/prompts/collaboration-workflow/turn-batching.md +14 -88
  78. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -14
  79. package/prompts/collaboration-workflow/wip-cap.md +6 -45
  80. package/prompts/collaboration-workflow.md +1 -13
  81. package/prompts/review-rubric.md +8 -71
  82. package/prompts/review.md +25 -55
  83. package/prompts/testing-guide.md +18 -67
  84. package/scripts/agent/agent-role-profile.ts +1 -1
  85. package/scripts/agent/codex-hooks.ts +3 -12
  86. package/scripts/backlog/backlog-budget.ts +1 -1
  87. package/scripts/backlog/backlog-drive-cli.ts +4 -3
  88. package/scripts/backlog/backlog-drive-offer-argv.ts +10 -1
  89. package/scripts/backlog/backlog-next.ts +15 -4
  90. package/scripts/backlog/backlog-offer.ts +6 -6
  91. package/scripts/backlog/backlog-plan-cli.ts +6 -5
  92. package/scripts/backlog/backlog-rank.ts +18 -9
  93. package/scripts/backlog/backlog-restructure.ts +152 -0
  94. package/scripts/backlog/backlog-waves.ts +15 -17
  95. package/scripts/behavior/behavior-rules.ts +11 -4
  96. package/scripts/cost-runtime/context-cut-threshold.ts +1 -9
  97. package/scripts/cost-runtime/cost-verdict.ts +15 -28
  98. package/scripts/delegation/investigation-reads.ts +5 -5
  99. package/scripts/discovery/repo-discovery.ts +21 -7
  100. package/scripts/discovery/repo-origin.ts +26 -0
  101. package/scripts/discovery/repo-party.ts +42 -1
  102. package/scripts/document/backlogrun-parent-read-set.ts +14 -15
  103. package/scripts/document/document-byte-budget.ts +34 -20
  104. package/scripts/document/entry-read-budget.ts +21 -9
  105. package/scripts/document/lane-child-read-set.ts +12 -14
  106. package/scripts/document/read-set-cli.ts +4 -4
  107. package/scripts/document/read-set-point-of-use.ts +10 -2
  108. package/scripts/document/resident-budget.ts +47 -0
  109. package/scripts/epic/epic-shape.ts +10 -13
  110. package/scripts/epic/epic-solo.ts +1 -1
  111. package/scripts/exports/unused-members-cli.ts +7 -3
  112. package/scripts/followup/git-followup-stages.ts +9 -0
  113. package/scripts/followup/git-pr-followup.ts +1 -1
  114. package/scripts/gh/git-gh-api-path.ts +7 -0
  115. package/scripts/gh/git-gh-issue.ts +5 -3
  116. package/scripts/gh/git-gh-pr-read.ts +4 -0
  117. package/scripts/gh/git-pr-checks-eval.ts +8 -0
  118. package/scripts/gh/git-pr-checks.ts +11 -1
  119. package/scripts/git/constants.ts +10 -0
  120. package/scripts/git/main-merge.ts +57 -21
  121. package/scripts/git/schemas.ts +2 -0
  122. package/scripts/hooks/pretool-guard-cli.ts +2 -2
  123. package/scripts/hooks/pretool-guard.ts +45 -2
  124. package/scripts/hooks/run-hook.sh +41 -0
  125. package/scripts/init/hook-command-rewrite.ts +18 -35
  126. package/scripts/init/hook-launch.ts +21 -32
  127. package/scripts/issue/issue-auto-ok.ts +95 -0
  128. package/scripts/issue/issue-file-cli.ts +53 -7
  129. package/scripts/issue/issue-file.ts +28 -5
  130. package/scripts/issue/issue-labels.ts +12 -12
  131. package/scripts/issue/issue-rank.ts +1 -1
  132. package/scripts/issue/issue-read-cli.ts +3 -3
  133. package/scripts/issue/issue-read.ts +2 -2
  134. package/scripts/issue/issue-scout-cli.ts +2 -2
  135. package/scripts/issue/issue-state-cli.ts +1 -1
  136. package/scripts/issue/issue-state.ts +2 -2
  137. package/scripts/issue/issue-wip.ts +70 -0
  138. package/scripts/josh/hook-decision.ts +17 -0
  139. package/scripts/josh/josh-commands-development.ts +11 -1
  140. package/scripts/josh/josh-commands-issue.ts +2 -2
  141. package/scripts/lane/lane-launch-cli.ts +21 -3
  142. package/scripts/lines/bytes-command.ts +29 -6
  143. package/scripts/notify/telegram-notify.ts +26 -1
  144. package/scripts/observations/observation-ledger-line.ts +1 -1
  145. package/scripts/release/release-progress.ts +167 -0
  146. package/scripts/release/release-publish.ts +19 -8
  147. package/scripts/release/release-tag.ts +22 -8
  148. package/scripts/rules/decision-oracle-stage.ts +1 -1
  149. package/scripts/rules/decision-oracle.ts +6 -6
  150. package/scripts/rules/delivered-rules.ts +11 -11
  151. package/scripts/rules/direct-pr-create.ts +110 -0
  152. package/scripts/rules/file-body.ts +98 -42
  153. package/scripts/rules/filing-cap.ts +2 -2
  154. package/scripts/rules/filing-offer.ts +1 -1
  155. package/scripts/rules/gh-api.ts +18 -1
  156. package/scripts/rules/implementation-cut.ts +3 -1
  157. package/scripts/rules/lane-background.ts +67 -10
  158. package/scripts/rules/permission-guards.ts +10 -2
  159. package/scripts/rules/pre-gate-cut.ts +2 -2
  160. package/scripts/rules/stop-rules.ts +4 -4
  161. package/scripts/rules/test-declared-commit.ts +1 -1
  162. package/scripts/rules/third-party-write.ts +56 -12
  163. package/scripts/run/run-carry.ts +11 -5
  164. package/scripts/run/run-cut-cli.ts +1 -1
  165. package/scripts/run/run-cut-handoff.ts +21 -1
  166. package/scripts/run/run-cut-report.ts +2 -2
  167. package/scripts/run/run-entry-cli.ts +11 -3
  168. package/scripts/run/run-hold-cli.ts +16 -2
  169. package/scripts/run/run-label.ts +50 -0
  170. package/scripts/run/run-merge-steps.ts +8 -3
  171. package/scripts/run/run-merge.ts +1 -1
  172. package/scripts/run/run-prep-cli.ts +6 -2
  173. package/scripts/run/run-prep.ts +17 -0
  174. package/scripts/run/run-ship-cli.ts +27 -6
  175. package/scripts/run/run-ship-next.ts +13 -0
  176. package/scripts/run/run-ship-pre-detach.ts +66 -0
  177. package/scripts/run/run-ship-preflight.ts +34 -3
  178. package/scripts/run/run-ship-return.ts +7 -4
  179. package/scripts/run/run-ship-review-steps.ts +1 -1
  180. package/scripts/run/run-ship-review.ts +26 -5
  181. package/scripts/run/run-ship-stage.ts +13 -0
  182. package/scripts/run/run-ship-steps.ts +10 -2
  183. package/scripts/run/run-ship-sync.ts +114 -0
  184. package/scripts/run/run-ship.ts +6 -0
  185. package/scripts/run/run-step-cli.ts +20 -7
  186. package/scripts/run/run-wake-cli.ts +8 -1
  187. package/scripts/run/run-wake-driver.ts +62 -8
  188. package/scripts/run/run-wake-session.ts +2 -1
  189. package/scripts/test/test-declared-logic.ts +1 -1
  190. package/scripts/test/test-red-logic.ts +17 -2
  191. package/scripts/test/test-red.ts +8 -7
  192. package/scripts/time-runtime/time-batch-guard.ts +52 -16
  193. package/scripts/time-runtime/time-density-hook.ts +20 -2
  194. package/scripts/time-runtime/time-density.ts +4 -3
  195. package/scripts/time-runtime/time-markers.ts +15 -4
  196. package/scripts/time-runtime/time-transcript-line.ts +27 -1
  197. package/scripts/version/npm-registry.ts +11 -1
  198. package/templates/workflows/ci.yml +9 -1
  199. package/.claude/skills/workflow-commands/followup-reference.md +0 -118
  200. package/dist/hooks/chunk-HANSKHES.js +0 -464
package/prompts/review.md CHANGED
@@ -15,17 +15,10 @@ points to them rather than restating them:
15
15
 
16
16
  ## When to run
17
17
 
18
- - **Pre-commit self-review** (implementing session, inline): before every `git commit` on a feature
19
- branch — scope: the staged diff (`git diff --staged`), level from
20
- `pnpm josh review:brief --level-only`.
21
- - **Workflow review step** (same session, inline): the last stage of the verification gate in `fullrun`
22
- / `halfrun` / `backlogrun`, driven by the brief `pnpm josh review:brief` prints. Round 1 runs before the
23
- commit — scope: `git diff main`. Round 2 runs after it, beside the CI the commit started
24
- (`.claude/skills/workflow-commands/chain-rule.md` → "The pull request opens between the rounds, so CI
25
- runs beside round 2").
26
-
27
- The workflow review uses the `reviewer` role profile printed by `pnpm josh review:brief`. Pass its
28
- model and effort explicitly to the review subagent; do not substitute the review level for effort.
18
+ When a review runs is `CLAUDE.md` → "Pre-commit Self-Review"; inside a workflow, round 1 runs before
19
+ the commit and round 2 beside CI (`.claude/skills/workflow-commands/chain-rule.md`). Pass the
20
+ `reviewer` profile's model and effort that `pnpm josh review:brief` prints to the review subagent;
21
+ do not substitute the review level for effort.
29
22
 
30
23
  Re-run after applying fixes until **no high or medium findings remain — or until the round cap below is
31
24
  reached, the first review included — whichever comes first.** The cap below is not optional. The second round
@@ -46,10 +39,8 @@ pnpm josh review:brief --level-only # the branch diff
46
39
  pnpm josh review:brief --level-only --staged # the staged diff
47
40
  ```
48
41
 
49
- **The level is decided from the changed paths and nothing else.** "This one is small" is a judgement
50
- made under cost pressure, and cost pressure resolves it toward "small" exactly when a defect is most
51
- likely to be shipped. A rule an agent applies from memory is a rule an agent can talk itself out of;
52
- one it has to run answers the same way every time.
42
+ **The level is decided from the changed paths and nothing else** — never "this one is small"
43
+ (`docs/maintainers/review-history.md`).
53
44
 
54
45
  | Every changed path is… | Level | Rounds |
55
46
  | ---------------------------------------------------------------------------------------- | -------- | ----------------------- |
@@ -64,13 +55,8 @@ would hand a reduced level to a caller that failed to read the diff.
64
55
  in `package.json`'s `files` and are written into every consumer project by `josh init` / `josh sync`,
65
56
  so a defect in one reaches a consumer and is reviewed at `medium` like any other shipped file.
66
57
 
67
- **Documentation is not inert either, and that is deliberate.** `CLAUDE.md`, `prompts/**`, `.claude/**`
68
- and `docs/**` are all reviewed at `medium`. The "Non-runtime updates" exception in `CLAUDE.md` exempts
69
- them from _testing_ — which asks whether an automated test could have caught the defect — while this
70
- asks whether a human reading the diff is the only thing that can. Measured on joshuafolkken/kit#963 and
71
- #965, both documentation-only by that classification: a `medium` review found ten real defects in each
72
- — pointers into sections that had been removed, and citations naming the wrong file, in artifacts
73
- distributed to every consumer — that nothing else would have caught.
58
+ **Documentation is not inert either.** `CLAUDE.md`, `prompts/**`, `.claude/**` and `docs/**` are all
59
+ reviewed at `medium`, though exempt from _testing_: a reader is the only detector a prose defect has.
74
60
 
75
61
  **A confirmed High blocks regardless of round count**, and the round cap below does not change that.
76
62
 
@@ -78,26 +64,19 @@ distributed to every consumer — that nothing else would have caught.
78
64
 
79
65
  ## Review round cap (2 rounds)
80
66
 
81
- The severity rule in the rubric is not a stopping condition on its own. Every fix creates new surface,
82
- and a review whose scope is the whole change finds something in it — so the loop is bounded by how much
83
- new code the fixes produce, which is unbounded.
67
+ The severity rule in the rubric is not a stopping condition on its own: every fix creates new surface a
68
+ further round would find something in.
84
69
 
85
70
  **Two rounds is the ceiling, not the schedule.** Whether the second one is due at all is
86
71
  `pnpm josh review:round2`'s answer — "When round 2 is skipped entirely, and when it is not" below is
87
- the single source of that condition (joshuafolkken/kit#1433).
88
-
89
- This is measured, not theorized. On joshuafolkken/kit#854 four rounds produced 18 findings; on #855
90
- two rounds produced 19. Almost none of them was a repeat: each round found new things, and many of
91
- those were about code the **previous round's fix** had just written. Two rounds of that is diligence; a
92
- third is the review chasing its own tail.
72
+ the single source of that condition.
93
73
 
94
74
  ### A merge-conflict resolution review is not one of the two
95
75
 
96
76
  **When `pnpm josh followup` reports `PR checks failed (merge conflict)`, the child resolves the conflict
97
- in its own lane and reviews the resolution — and that round does not count against the cap**
98
- (joshuafolkken/kit#1623). The cap bounds re-reading _the change under review_; a resolution review reads
77
+ in its own lane and reviews the resolution — and that round does not count against the cap**. The cap bounds re-reading _the change under review_; a resolution review reads
99
78
  a different subject — not the change, but what merging a moved `main` into it did.
100
- `.claude/skills/workflow-commands/backlogrun-lanes.md` → "Conflicts are not predicted" is the single source of
79
+ `.claude/skills/workflow-commands/backlogrun-recovery.md` → "Conflicts are not predicted" is the single source of
101
80
  the procedure and of the four conditions under which the run steps back instead of resolving.
102
81
  `pnpm josh review:attest --check` must answer `ok` before the merge is re-issued, and a confirmed High
103
82
  parks the child rather than buying it a further round.
@@ -105,7 +84,7 @@ parks the child rather than buying it a further round.
105
84
  ### When round 2 is skipped entirely, and when it is not
106
85
 
107
86
  **Two rounds is the ceiling; whether the second round runs at all is a command's answer, not a reading
108
- of a list** (joshuafolkken/kit#1433):
87
+ of a list**:
109
88
 
110
89
  ```bash
111
90
  pnpm josh review:round2 --round-1-closed # → required | skip ; the reason on stderr
@@ -116,8 +95,7 @@ round-1 High/Medium finding actually closed — by a fix in this working tree, o
116
95
  positive — and none was filed or deferred. **Without the flag the answer is `required`**, which is also
117
96
  what the command answers to every uncertainty it meets, a missing round-1 snapshot included.
118
97
 
119
- **`skip` has exactly two arms, and both are states in which round 1's fix code needs no review**
120
- (joshuafolkken/kit#1222 — the round exists because round 1's fix code is otherwise unreviewed):
98
+ **`skip` has exactly two arms, and both are states in which round 1's fix code needs no review**:
121
99
 
122
100
  | Arm | What it is |
123
101
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
@@ -126,9 +104,7 @@ what the command answers to every uncertainty it meets, a missing round-1 snapsh
126
104
 
127
105
  **Neither arm weakens the standard.** A round-1 High/Medium that did not close is not a closed finding,
128
106
  so the flag is not passable; the cap is still two rounds; and a confirmed High still blocks the merge.
129
- **The wider line joshuafolkken/kit#1433 proposed — exempting anything that is not a runtime code path —
130
- is not adopted**: a documentation-only diff is exactly where the review is the only detector (the #963 /
131
- #965 measurement above), so a prompt fix answers `required`.
107
+ A documentation-only fix delta is not inert, so a prompt fix answers `required`.
132
108
 
133
109
  **A skipped round is recorded on the Issue**, so the condition stays auditable and can be withdrawn if
134
110
  a defect is later traced to a skipped delta:
@@ -142,7 +118,7 @@ a defect is later traced to a skipped delta:
142
118
  ### Three-way disposition after the cap
143
119
 
144
120
  **After the second round, place every remaining non-High finding in exactly one of three exits, decided
145
- from what the finding is rather than from the filer's discretion** (joshuafolkken/kit#1469). **The
121
+ from what the finding is rather than from the filer's discretion**. **The
146
122
  default exit is branch 3, and branch 2 has to be earned**. Read them in order — branch 1 where the fix is
147
123
  trivial and local, branch 2 only where the finding clears the bar it names, branch 3 for everything
148
124
  left, which is most of them. "It might matter later" is branch 3.
@@ -167,12 +143,6 @@ left, which is most of them. "It might matter later" is branch 3.
167
143
  suspicion included. **The note is not optional**: one line naming the finding and why it was dropped
168
144
  is what keeps a dropped finding auditable rather than invisible.
169
145
 
170
- **What it costs, and why that is the right trade.** A dropped finding that later turns out to matter is
171
- re-found by the next review of that code, at the price of one round it would have paid anyway. A filed
172
- finding that never mattered is carried forever — placed in an epic, offered by `epic:next`, read by
173
- everyone who scans the backlog. The two errors are not symmetric, and the old default was on the
174
- expensive side of them.
175
-
176
146
  **Findings that reduce to one root judgement are filed as one Issue, not several** — a single follow-up
177
147
  Issue with a section per symptom (`## 現象 1` / `## 現象 2`).
178
148
 
@@ -187,7 +157,7 @@ Only branch 2 files an Issue. What follows applies to that branch.
187
157
  the commit.** Where the run opens a pull request — `fullrun`, `backlogrun` — they go after
188
158
  `pnpm josh git -y` and before `pnpm josh followup`; where it does not — `halfrun`, or a standalone
189
159
  pre-commit self-review — they run as soon as the disposition is decided. **The chain may run in a
190
- delegated unit** — `pnpm josh delegate followup-filing` (joshuafolkken/kit#1892).
160
+ delegated unit** — `pnpm josh delegate followup-filing`.
191
161
  - **When the round files a second follow-up, run `pnpm josh issue:fold` first.** Several findings from
192
162
  one review fold into one Issue by default — the filing-time counterpart to the split assessment,
193
163
  reading the same two questions (`.claude/skills/workflow-commands/split-assessment.md` → "The same
@@ -204,13 +174,13 @@ Only branch 2 files an Issue. What follows applies to that branch.
204
174
  write command yourself — never a hand edit of the epic body, which leaves the task list and the
205
175
  `blocked-by` relations disagreeing and `epic:next` returning `error`.
206
176
 
207
- | Answer | Do | Tier |
208
- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
209
- | `add_to_epic` | `pnpm josh epic --add <E> <new>` — add `--before <M>` / `--after <M>` when the relation carries an order | **A — no confirmation** |
210
- | `create_epic` | `pnpm josh epic "<title>" <new> <other> [--ordered]` | **A — no confirmation** |
211
- | `ask` — candidates spread across two or more epics | **Choose the epic you recommend, run its write command, and record the decision** — what was taken, what was rejected, why — on both the new Issue and that epic's `## Decisions`. This does not stop a run and does not park a child (joshuafolkken/kit#1339); stop only where the two epics are genuinely too close to separate | **A — no confirmation** |
212
- | `none` | Nothing | — |
213
- | **The command could not answer** — non-zero exit, or **any** ⚠ warning above `Nothing to bundle.` | Stop and report, naming what it said. **A `none` printed after such a warning is not "nothing to bundle"** — the search was incomplete | — |
177
+ | Answer | Do | Tier |
178
+ | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
179
+ | `add_to_epic` | `pnpm josh epic --add <E> <new>` — add `--before <M>` / `--after <M>` when the relation carries an order | **A — no confirmation** |
180
+ | `create_epic` | `pnpm josh epic "<title>" <new> <other> [--ordered]` | **A — no confirmation** |
181
+ | `ask` — candidates spread across two or more epics | **Choose the epic you recommend, run its write command, and record the decision** — what was taken, what was rejected, why — on both the new Issue and that epic's `## Decisions`. This does not stop a run and does not park a child; stop only where the two epics are genuinely too close to separate | **A — no confirmation** |
182
+ | `none` | Nothing | — |
183
+ | **The command could not answer** — non-zero exit, or **any** ⚠ warning above `Nothing to bundle.` | Stop and report, naming what it said. **A `none` printed after such a warning is not "nothing to bundle"** — the search was incomplete | — |
214
184
 
215
185
  - **A confirmed High is never deferred.** A real defect does not ship because a round counter ran out,
216
186
  so a standing High blocks the merge — never fixed-in-place, filed, or dropped as one of the three
@@ -22,7 +22,8 @@ Req N: ...
22
22
  ### Rules
23
23
 
24
24
  - **Every user-facing behavior change** (UI interaction, keyboard shortcut, visible state change) must have a corresponding E2E test.
25
- - **Every logic/utility change** (pure functions, filters, transforms) must have a unit test.
25
+ - **Every logic/utility change** (pure functions, filters, transforms) must have a unit test. A bug fix takes a regression test; a refactor takes tests pinning the existing behavior BEFORE the change (`prompts/refactoring.md`).
26
+ - **Non-runtime updates (pre-approved manual-only exception):** a change touching no executable runtime code path — docs, non-executable config, editor/IDE files, cosmetic asset swaps with no selector/path change — may be verified by hand; declare it in Step 0 with the reason. The mechanically exempt (`*.md`, `.editorconfig`, `.idea/**`, `.vscode/**`, `prompts/**`) need no declaration. A runtime change whose test is infeasible needs the reason and user approval.
26
27
  - **basic で kit がテストできない言語だけの変更:** `package.json` に `josh.profile: basic`(改名前の `static` も同じ)が記録され、変更が HTML/CSS や Lua などのソースだけなら自動テストは不要。HTML/CSS はブラウザーで対象ページを開き、想定する画面幅でレイアウト・リンク・HTML 内の操作を確認する。そのほかのソースは変更した処理を実際に動かす。どちらも結果を記録する。同じ変更に JavaScript または TypeScript のファイルが含まれれば自動テストを行う。
27
28
  - If a test is genuinely infeasible (e.g., native OS date-picker popup cannot be driven by Playwright), write a comment in the test file explaining why and test the closest observable behavior instead (e.g., that the editor remains open after the `change` event fires).
28
29
  - Do **not** report a requirement as done if its test is missing.
@@ -59,7 +60,7 @@ There is exactly **one** convention for every test file — never choose between
59
60
  - **The `.svelte.` infix is required for component/browser tests and must be preserved.** `*.svelte.test.ts` routes the file to the **browser/component** vitest project (`include: src/**/*.svelte.{test,spec}.{js,ts}`); plain `*.test.ts` routes to the **node/unit** project (`include: src/**/*.{test,spec}.{js,ts}`). Renaming `Foo.svelte.test.ts` → `Foo.test.ts` silently moves it to the wrong project — do not drop the `.svelte.` infix.
60
61
  - **Colocate every test next to the code it exercises.** A top-level `tests/` directory is **not used** — place `foo.test.ts` beside `foo.ts`, and E2E specs under the relevant `src/routes/**` path. The top-level `tests/` ban is **lint-enforced** by the same rule (`eslint/rules/test-filename.js`). Note: `playwright.config.ts` discovers E2E via `testMatch: '**/*.e2e.{ts,js}'`, so an `*.e2e.ts` placed outside `src/routes/**` would still run — that specific `src/routes/**` placement is enforced by **convention/review, not config**, and must be upheld manually.
61
62
 
62
- > **Enforcement:** the `*.spec.ts` ban and the top-level `tests/` ban are enforced by the test-filename rule (`eslint/rules/test-filename.js`), wired into the shared `eslint/base.js`, so they fail loudly in `josh lint` for kit itself, for every consumer of that base config, and for every AI tool. Left as an export a consumer had to remember to wire in, the ban reached only the projects that did — and never the repository distributing it (joshuafolkken/kit#1233). This was chosen over tightening the vitest `{test,spec}` matchers to `{test}` only, because matcher-tightening causes **silent non-execution** of stray `*.spec.ts` files whereas a lint rule fails loudly. (Doc-only guidance had already failed to prevent this drift twice.)
63
+ > **Enforcement:** the `*.spec.ts` ban and the top-level `tests/` ban are enforced by the test-filename rule (`eslint/rules/test-filename.js`), wired into the shared `eslint/base.js`, so they fail loudly in `josh lint` for kit itself, for every consumer of that base config, and for every AI tool (`docs/maintainers/testing-guide-history.md`).
63
64
 
64
65
  ---
65
66
 
@@ -177,19 +178,8 @@ test('returns expected values in sequence', () => {
177
178
 
178
179
  ## 6. Closing the E2E gate without a human run
179
180
 
180
- The completion gate used to end by asking the user to run `pnpm josh test` and paste the output.
181
- That step is gone (joshuafolkken/kit#902). It is the rule below that replaces it, and the
182
- replacement is **not** a relaxation — nothing that a red local run stopped is allowed through.
183
-
184
- ### Why it existed, and why it no longer does
185
-
186
- The reason was never that a human reads E2E output better. It was that `followup` waited 180
187
- seconds for CI, which no suite containing E2E can finish in — so the command exited red beside a CI
188
- that was still running, and the only reliable E2E signal left was a person's terminal. That budget
189
- is now derived from the distributed `templates/workflows/ci.yml` rather than picked as a round
190
- number: 1920 seconds, the longest run its `needs` graph permits plus runner-queue headroom, with a
191
- unit test that fails if a declared job budget outgrows it (joshuafolkken/kit#851). CI can now be
192
- waited on, so it can be relied on.
181
+ No human E2E run closes the gate, and nothing a red local run stopped is let through
182
+ (`docs/maintainers/testing-guide-history.md`).
193
183
 
194
184
  ### Which result closes the gate
195
185
 
@@ -219,60 +209,21 @@ a **defined** outcome, not an accident: a project with no E2E suite has nothing
219
209
  read, and the run says so out loud. **A skip you did not see printed is not one** — it is an unread
220
210
  result, and it falls under the previous paragraph.
221
211
 
222
- **CI decides the same question, and now by the same rule.** The `e2e-detect` job in
212
+ **CI decides the same question by the same rule.** The `e2e-detect` job in
223
213
  `templates/workflows/ci.yml` enables the `e2e` job when any `*.e2e.{ts,js}` exists outside
224
- `node_modules` — the guard's own glob, dot-directories left out the way the guard's default leaves
225
- them out — and it does not look at the config's filename at all (joshuafolkken/kit#991). Before
226
- that it required a file named exactly `playwright.config.ts` **and** an `*.e2e.{ts,js}` under `tests` or
227
- `src/routes`, so a suite kept anywhere else, or a config named `playwright.config.js`, read as
228
- "no E2E" in CI while `josh test:e2e` ran it locally: the job's `if:` went false, GitHub recorded it
229
- as `skipped`, the rollup parser counted the skip as passing exactly as it does for every other
230
- conditional job, and the first row of the table above closed the gate on a suite nobody ran.
231
- **Where the specs live was a precondition on the CI signal; it is now a property of the workflow.**
232
- The agreement is executed rather than described — `scripts/ci/ci-yml-e2e-detect.test.ts` runs the
233
- workflow's own script against each layout and compares its verdict to the guard's, and asserts over
234
- the whole matrix that no layout containing specs can yield `enabled=false`. § 1's placement
235
- convention still stands on its own merits (`eslint/rules/test-filename.js` bans a top-level
236
- `tests/` directory outright), but the CI signal no longer depends on following it.
237
-
238
- **One difference from the guard remains, and it is deliberate and one-sided.** The guard also skips
239
- when `@playwright/test` is not installed, which is what keeps that optional peer optional for the
240
- pre-push hook; CI enables the job on the spec alone, so a spec that cannot run ends red rather than
241
- silently green. Enabling more than the guard costs a failed job, enabling less costs a merge — so
242
- the difference may widen in that direction and never narrow.
214
+ `node_modules` — the guard's own glob — whatever the config's filename;
215
+ `scripts/ci/ci-yml-e2e-detect.test.ts` pins the agreement. CI enables the job on the spec alone, even
216
+ where the guard skips for a missing `@playwright/test`, so a spec that cannot run ends red rather than
217
+ silently green.
243
218
 
244
219
  ### The gate is not weakened
245
220
 
246
- Two properties carry the verification the human step used to carry, and both are asserted by tests
247
- rather than left to prose:
248
-
249
221
  - **A non-passing check keeps the merge closed.** `evaluate_pr_state` returns `success` only when
250
- GitHub reports the pull request `CLEAN` **and** every required check passes. A failing E2E job
251
- makes GitHub report `UNSTABLE`, and the one opening in that wall — `is_unstable_only_from_coderabbit`,
252
- the temporary kit#753 policy — applies only when _every_ non-passing check is CodeRabbit's. So a
253
- red E2E can never reach `success`, and `followup` exits non-zero instead of merging.
254
- - **Weakening the gate is a workaround, and the workaround rules apply to it.** Filtering,
255
- narrowing or reinterpreting E2E output to get past it is the violation `CLAUDE.md` names under
256
- "Cross-package problems"; reporting the filtered result honestly does not make it compliant.
257
-
258
- **`E2E` stays off the required-check list, and that is a decision rather than an oversight**
259
- (joshuafolkken/kit#991). A required check is satisfied by a _skipped_ job — the rollup parser maps
260
- `SKIPPED` to `pass` — so requiring `E2E` would have passed on exactly the skipped job described
261
- above, which is the failure it looks like it would have caught. It adds nothing to the failure path
262
- either: a red E2E already ends the wait on the poll that sees it. What it would add is a cost —
263
- a repository whose CI reports no `E2E` context at all resolves the required entry to `missing`,
264
- which is never `pass`, so the evaluation stays `pending` until the 32-minute wait times out with
265
- nothing actually wrong. What guarantees the job runs is the detection rule above, not the required
266
- list. A project that wants it required opts in through `JOSH_REQUIRED_CHECKS`, which
267
- `scripts/gh/git-pr-checks-eval.test.ts` covers; the decision itself — `E2E` off the default list,
268
- and a repository reporting no `E2E` check still mergeable — is pinned in
269
- `scripts/gh/git-pr-checks-e2e-gate.test.ts`.
270
-
271
- **A red E2E is reported as soon as it goes red, and by name.** `E2E` is not on the required list, so
272
- until joshuafolkken/kit#990 nothing ended the wait when it failed: the rollup read as pending,
273
- `followup` ran out its 32-minute budget, and the run ended in `Timed out while waiting for PR checks
274
- to complete.` — a message naming neither the job nor the cause. Any failing check now ends the wait
275
- on the poll that sees it, and the error names it (`PR checks failed (failed checks: E2E).`).
276
- The poll loop keeps CodeRabbit exempt under the temporary kit#753 policy. **This is a change to how fast the gate
277
- reports, never to what it lets through** — no failing check gained a path to `success`, which
278
- `scripts/gh/git-pr-checks-e2e-gate.test.ts` continues to assert.
222
+ GitHub reports the pull request `CLEAN` **and** every required check passes; the only exception is a
223
+ non-passing set made entirely of CodeRabbit checks (the temporary kit#753 policy). A failing check
224
+ ends the wait on the poll that sees it, by name (`PR checks failed (failed checks: E2E).`).
225
+ - **Filtering, narrowing or reinterpreting E2E output to get past it is a workaround** —
226
+ `CLAUDE.md` → "Cross-package problems"; reporting the filtered result honestly does not make it
227
+ compliant.
228
+ - **`E2E` stays off the default required-check list**; a project opts in through
229
+ `JOSH_REQUIRED_CHECKS`. `scripts/gh/git-pr-checks-e2e-gate.test.ts` pins both properties.
@@ -74,7 +74,7 @@ const PROVIDER_PROFILES = { anthropic: DEFAULT_PROFILES, openai: OPENAI_PROFILES
74
74
 
75
75
  // **Effort as a function of the run phase, not the role alone** (joshuafolkken/kit#2382). For the first
76
76
  // merge only the mechanical ship/bookkeeping region is lowered: the pre-gate resume drives the gate,
77
- // commit, PR and merge, applying fixes the gate has already named — work SKILL.md §2b calls the opposite
77
+ // commit, PR and merge, applying fixes the gate has already named — work delegation.md calls the opposite
78
78
  // of judgement. The design-judgment implementation phases keep the role default, as does any role/phase
79
79
  // with no entry, and the review's own judgement is the reviewer role's (`high`), untouched by lowering
80
80
  // the worker here. The investigation phase — highest output density — is left for a later merge, once
@@ -33,22 +33,13 @@ const UNMATCHED_EVENTS: ReadonlySet<string> = new Set(['UserPromptSubmit'])
33
33
  const JSON_INDENT = '\t'
34
34
 
35
35
  function adapter_command(mode: string): string {
36
- return hook_launch.bundle_launch_command(
37
- `codex-hook-adapter.js ${mode}`,
38
- `pnpm exec tsx scripts/hooks/codex-hook-adapter.ts ${mode}`,
39
- )
36
+ return hook_launch.hook_launch_command(`codex-hook-adapter ${mode}`)
40
37
  }
41
38
 
42
39
  // Each Claude-side guard command the adapter stands in for, keyed by that command verbatim.
43
40
  const ADAPTED_COMMANDS: ReadonlyMap<string, string> = new Map([
44
- [
45
- hook_launch.hook_launch_command('pretool-guard.js', 'pretool:guard'),
46
- adapter_command('pretool'),
47
- ],
48
- [
49
- hook_launch.hook_launch_command('format-edited.js', 'format:edited'),
50
- adapter_command('posttool'),
51
- ],
41
+ [hook_launch.hook_launch_command('pretool-guard'), adapter_command('pretool')],
42
+ [hook_launch.hook_launch_command('format-edited'), adapter_command('posttool')],
52
43
  ])
53
44
 
54
45
  function codex_handler(handler: HookHandler): HookHandler {
@@ -59,7 +59,7 @@ type BudgetVerdict =
59
59
  typeof RUN_VERDICT | typeof WATCH_VERDICT | typeof STOP_VERDICT | typeof TRIAGE_VERDICT
60
60
 
61
61
  // What `backlog:next` answered, in the words this decision needs. The mapping is mechanical and is
62
- // written once, in `backlogrun-steps.md` → "The loop": numbers are `candidates`, `wait` is `blocked` while
62
+ // written once, in `docs/josh-commands-automation.md` → "`josh backlog:offer`": numbers are `candidates`, `wait` is `blocked` while
63
63
  // this run has children in flight and `exhausted` when it does not, `none` is `exhausted`, `stop` is
64
64
  // `parked`, `triage` is `untriaged`, and `error` or a failed listing is `unreadable`.
65
65
  const ANSWERS = ['blocked', 'candidates', 'exhausted', 'parked', 'unreadable', 'untriaged'] as const
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { setTimeout as sleep } from 'node:timers/promises'
3
3
  import { fileURLToPath } from 'node:url'
4
- import { CONTEXT_CUT_THRESHOLD } from '#scripts/cost-runtime/context-cut-threshold'
5
4
  import { hook_decision } from '#scripts/josh/hook-decision'
6
5
  import { josh_command } from '#scripts/josh/josh-run'
7
6
  import { lane_await, type AwaitState } from '#scripts/lane/lane-await'
@@ -217,7 +216,8 @@ function merge_token(out: string, code: number): string {
217
216
  }
218
217
 
219
218
  // The child's transcript, where its lane recorded one, so `run:merge` can tell an API outage from a
220
- // failure exactly as it does for the parent that passes `--output` by hand.
219
+ // failure exactly as it does for the parent that passes `--output` by hand. No hand-off threshold: the
220
+ // supervisor runs this loop, and a process has no context to cut (joshuafolkken/kit#3156).
221
221
  async function merge(issue: string, owner: string): Promise<MergeResult> {
222
222
  await backlog_drive_owner.assert_current(owner)
223
223
  const lane = await lane_registry.find_open_lane(issue)
@@ -225,7 +225,7 @@ async function merge(issue: string, owner: string): Promise<MergeResult> {
225
225
  child: issue,
226
226
  epic: undefined,
227
227
  repo: undefined,
228
- over: CONTEXT_CUT_THRESHOLD,
228
+ over: undefined,
229
229
  owner: run_carry.owner_of(Number(owner)),
230
230
  output: lane?.output,
231
231
  })
@@ -381,6 +381,7 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
381
381
  const backlog_drive_cli = {
382
382
  end_line,
383
383
  finish: backlog_drive_finish.finish,
384
+ merge,
384
385
  merge_token,
385
386
  offer_argv: backlog_drive_offer_argv.offer_argv,
386
387
  parse,
@@ -3,11 +3,20 @@ import type { DriveState } from './backlog-drive'
3
3
 
4
4
  const LIST_SEPARATOR = ','
5
5
 
6
+ // **A child in flight is never offered** (joshuafolkken/kit#3156). A lane child merges its own PR and
7
+ // drops `in-progress` before GitHub closes the issue and before the loop collects it, so `backlog:next`
8
+ // can offer it again; launching it then is refused by its still-open lane and handed to an AI session.
9
+ function excluded(state: DriveState): ReadonlyArray<string> {
10
+ return [...new Set([...state.exclude, ...state.in_flight])]
11
+ }
12
+
6
13
  function offer_argv(
7
14
  state: DriveState,
8
15
  carry: RunCarry,
9
16
  forwarded: ReadonlyArray<string>,
10
17
  ): ReadonlyArray<string> {
18
+ const exclude = excluded(state)
19
+
11
20
  return [
12
21
  'backlog:offer',
13
22
  '--json',
@@ -21,7 +30,7 @@ function offer_argv(
21
30
  String(state.in_flight.length),
22
31
  '--retries',
23
32
  String(state.retries),
24
- ...(state.exclude.length === 0 ? [] : ['--exclude', state.exclude.join(LIST_SEPARATOR)]),
33
+ ...(exclude.length === 0 ? [] : ['--exclude', exclude.join(LIST_SEPARATOR)]),
25
34
  ...forwarded,
26
35
  ]
27
36
  }
@@ -16,7 +16,8 @@ import { PROJECT_ROOT } from '#scripts/init/init-paths'
16
16
  import { issue_citation } from '#scripts/rules/issue-citation'
17
17
  import { backlog_defect_priority } from './backlog-defect-priority'
18
18
  import { backlog_pool } from './backlog-pool'
19
- import { backlog_rank } from './backlog-rank'
19
+ import { backlog_rank, type GateScope } from './backlog-rank'
20
+ import { backlog_restructure } from './backlog-restructure'
20
21
 
21
22
  // `josh backlog:next` — what the whole opted-in backlog may run next (joshuafolkken/kit#1630).
22
23
  //
@@ -278,7 +279,7 @@ function report_retry(context: PoolContext): number {
278
279
  return SUCCESS_EXIT_CODE
279
280
  }
280
281
 
281
- // The rows the offer's cap bounds — the standalone half, which `backlog:plan --waves` reads too.
282
+ // The rows the offer's cap bounds — the standalone half.
282
283
  function standalone_keys(context: PoolContext): ReadonlySet<string> {
283
284
  return backlog_pool.standalone_keys(context.opted_in.issues, {
284
285
  tracked: epic_index.withheld_children(context.tracking.index, context.opted_in.issues),
@@ -287,6 +288,16 @@ function standalone_keys(context: PoolContext): ReadonlySet<string> {
287
288
  })
288
289
  }
289
290
 
291
+ // The running lanes still carry the pickup label, so the opted-in listing holds their bodies too —
292
+ // which is what lets a candidate be held back from a restructure a running lane already makes
293
+ // (joshuafolkken/kit#3221). `backlog:plan --waves` reads the same scope, so its first wave is this offer.
294
+ function gate_scope(context: PoolContext): GateScope {
295
+ return {
296
+ standalone: standalone_keys(context),
297
+ declared: backlog_restructure.declared_of(context.opted_in.issues),
298
+ }
299
+ }
300
+
290
301
  // The `run:solo` gate is applied here rather than in `resolve`, because `backlog:plan` shares
291
302
  // `resolve` and a plan made before the run lists everything (joshuafolkken/kit#2776). The holders are
292
303
  // read only on a `run` answer — the one answer the gate can change. The cap follows the gate
@@ -302,7 +313,7 @@ async function gate_solo(result: EpicNextResult, context: PoolContext): Promise<
302
313
 
303
314
  if (notice !== undefined) console.error(notice)
304
315
 
305
- const gated = backlog_rank.gate(result, read, repo, standalone_keys(context))
316
+ const gated = backlog_rank.gate(result, read, repo, gate_scope(context))
306
317
 
307
318
  if (gated.notice !== undefined) console.error(gated.notice)
308
319
 
@@ -418,7 +429,7 @@ const backlog_next = {
418
429
  READ_FAILURES,
419
430
  VERDICT_TOKENS,
420
431
  resolve,
421
- standalone_keys,
432
+ gate_scope,
422
433
  context_of,
423
434
  run,
424
435
  }
@@ -5,15 +5,15 @@ import { backlog_next } from './backlog-next'
5
5
  // answered — its stdout tokens and its exit code — into the one word `backlog:budget` is then asked
6
6
  // with, plus the issue numbers to start and the consecutive-retry count carried to the next ask.
7
7
  //
8
- // **The mapping is `backlogrun-steps.md` → "The loop"'s table, held here so it cannot drift.** That table
9
- // fixed which `backlog:next` answer becomes which `backlog:budget` word, and it carried two context
10
- // branches an agent had to apply by hand every iteration: `wait` is `blocked` while this run has
11
- // children in flight and `exhausted` when it has none, and `retry` is `blocked` until the third
8
+ // **The mapping is `docs/josh-commands-automation.md` → "`josh backlog:offer`", held here so it cannot
9
+ // drift.** It fixes which `backlog:next` answer becomes which `backlog:budget` word, and it carries two
10
+ // context branches an agent once applied by hand every iteration: `wait` is `blocked` while this run
11
+ // has children in flight and `exhausted` when it has none, and `retry` is `blocked` until the third
12
12
  // consecutive one, which is `unreadable`. Both are decided from a count here rather than from a
13
- // judgement.
13
+ // judgement (joshuafolkken/kit#3175 dropped the prose copy from `backlogrun-steps.md`).
14
14
 
15
15
  // Three consecutive `retry` answers end the run: below that a transport hiccup is re-asked, at it the
16
- // outage is not a hiccup. `backlogrun-steps.md` → "The loop" is the single source of the count.
16
+ // outage is not a hiccup. "`josh backlog:offer`" is the single source of the count.
17
17
  const RETRY_LIMIT = 3
18
18
  const FAILURE_EXIT_CODE = 1
19
19
  const NO_RETRIES = 0
@@ -10,6 +10,7 @@ import { run_invocation } from '#scripts/run/run-invocation'
10
10
  import { backlog_named } from './backlog-named'
11
11
  import { backlog_next, type OptedIn } from './backlog-next'
12
12
  import { backlog_plan, type NamedPlan, type PlanContext } from './backlog-plan'
13
+ import type { GateScope } from './backlog-rank'
13
14
  import { backlog_scope } from './backlog-scope'
14
15
  import { backlog_waves } from './backlog-waves'
15
16
 
@@ -104,9 +105,9 @@ interface Plan {
104
105
  // past the listing cap. Carrying the *whole* index here instead would move that misreport rather
105
106
  // than remove it: the sentence would name an epic that is withholding nothing.
106
107
  tracked: ReadonlyMap<number, number>
107
- // The rows the offer's cap bounds, so `--waves` cuts each wave where `backlog:next` does
108
- // (joshuafolkken/kit#2928).
109
- standalone: ReadonlySet<string>
108
+ // The rows the offer's cap bounds and the paths each issue restructures, so `--waves` cuts each
109
+ // wave where `backlog:next` does (joshuafolkken/kit#2928, joshuafolkken/kit#3221).
110
+ scope: GateScope
110
111
  }
111
112
 
112
113
  // The listing plus whether it was cut. Carried together because the rows alone cannot say whether
@@ -141,7 +142,7 @@ async function classify(
141
142
  repo: context.repo,
142
143
  exclude,
143
144
  tracked: epic_index.withheld_children(context.tracking.index, context.opted_in.issues),
144
- standalone: backlog_next.standalone_keys(context),
145
+ scope: backlog_next.gate_scope(context),
145
146
  }
146
147
  }
147
148
 
@@ -172,7 +173,7 @@ function print_plan(plan: Plan, listing: OpenListing, view: PlanView): void {
172
173
  const context = context_of(plan, listing)
173
174
 
174
175
  if (view.is_waves) {
175
- console.info(backlog_waves.format_waves(plan.result, context, plan.standalone))
176
+ console.info(backlog_waves.format_waves(plan.result, context, plan.scope))
176
177
 
177
178
  return
178
179
  }
@@ -4,6 +4,7 @@ import { epic_rank } from '#scripts/epic/epic-rank'
4
4
  import type { EpicNextResult } from '#scripts/epic/epic-report'
5
5
  import { epic_solo, type SoloGate, type SoloSelection } from '#scripts/epic/epic-solo'
6
6
  import { git_next_issues } from '#scripts/issue/git-next-issues'
7
+ import { backlog_restructure, type Declared } from './backlog-restructure'
7
8
 
8
9
  // What the backlog offers, in the order it is decided (joshuafolkken/kit#2928): rank, then the
9
10
  // `run:solo` gate, then the cap. `backlog:next` and `backlog:plan --waves` both call `select`, so the
@@ -78,29 +79,37 @@ interface OfferInput {
78
79
  repo: string
79
80
  // The keys of the standalone rows — the ones the cap bounds.
80
81
  standalone: ReadonlySet<string>
82
+ // The paths each issue restructures; two candidates claiming one are not offered together
83
+ // (joshuafolkken/kit#3221). Absent, nothing is separated.
84
+ declared?: Declared
81
85
  }
82
86
 
87
+ // The restructure separation runs before the cap, for the reason the gate does: the cap only ever
88
+ // removes what would otherwise be offered.
83
89
  function select(input: OfferInput): SoloSelection {
84
90
  const ranked = rank(input.candidates, input.pool)
91
+ const { read, repo } = input
92
+ const gated = epic_solo.select(ranked, read, repo)
93
+ const separated = backlog_restructure.separate(gated, input.declared ?? new Map(), { read, repo })
85
94
 
86
- return cap(epic_solo.select(ranked, input.read, input.repo), input.standalone)
95
+ return cap(separated, input.standalone)
96
+ }
97
+
98
+ interface GateScope {
99
+ standalone: ReadonlySet<string>
100
+ declared: Declared
87
101
  }
88
102
 
89
103
  // `backlog:next`'s gate: `epic_solo.gate` with this selection in place of its bare one.
90
- function gate(
91
- result: EpicNextResult,
92
- read: BusyRead,
93
- repo: string,
94
- standalone: ReadonlySet<string>,
95
- ): SoloGate {
104
+ function gate(result: EpicNextResult, read: BusyRead, repo: string, scope: GateScope): SoloGate {
96
105
  const pool = everything_in(result)
97
106
 
98
107
  return epic_solo.gate(result, read, repo, (candidates) =>
99
- select({ candidates, pool, read, repo, standalone }),
108
+ select({ candidates, pool, read, repo, ...scope }),
100
109
  )
101
110
  }
102
111
 
103
112
  const backlog_rank = { everything_in, gate, rank_result, select }
104
113
 
105
114
  export { backlog_rank }
106
- export type { OfferInput }
115
+ export type { GateScope, OfferInput }