@joshuafolkken/kit 1.953.0 → 1.972.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 (100) hide show
  1. package/.claude/skills/workflow-commands/background-commands.md +3 -0
  2. package/.claude/skills/workflow-commands/backlogrun-child.md +75 -120
  3. package/.claude/skills/workflow-commands/backlogrun-lanes.md +78 -115
  4. package/.claude/skills/workflow-commands/backlogrun-progress.md +82 -146
  5. package/.claude/skills/workflow-commands/backlogrun-steps.md +173 -229
  6. package/.claude/skills/workflow-commands/backlogrun.md +3 -1
  7. package/.claude/skills/workflow-commands/followup.md +3 -0
  8. package/.claude/skills/workflow-commands/fullrun.md +3 -4
  9. package/.claude/skills/workflow-commands/observation-filing.md +31 -25
  10. package/.claude/skills/workflow-commands/pre-gate-cut.md +96 -161
  11. package/.github/workflows/pr-classification.yml +12 -1
  12. package/CLAUDE.md +3 -3
  13. package/README.md +5 -3
  14. package/dist/CLAUDE.md +3 -3
  15. package/dist/hooks/batch-guard.js +4 -4
  16. package/dist/hooks/{chunk-OM5VDM6F.js → chunk-2K52BRWV.js} +2 -2
  17. package/dist/hooks/{chunk-Z5TVZ4AP.js → chunk-BNWIOJFU.js} +29 -16
  18. package/dist/hooks/{chunk-NKVLBB65.js → chunk-BVMSPG3M.js} +3 -3
  19. package/dist/hooks/{chunk-IA7WC4NS.js → chunk-CRGJTD6E.js} +1 -1
  20. package/dist/hooks/{chunk-M4U4YNRE.js → chunk-F6OM6SIG.js} +3 -3
  21. package/dist/hooks/{chunk-O3PEDF3M.js → chunk-KQG4776P.js} +1 -1
  22. package/dist/hooks/{chunk-V7RH56HU.js → chunk-NXL3SBXY.js} +2 -2
  23. package/dist/hooks/{chunk-452OKRQX.js → chunk-WWTMHHZU.js} +120 -18
  24. package/dist/hooks/{chunk-EYFJ74MJ.js → chunk-ZPLXLJ7O.js} +21 -7
  25. package/dist/hooks/{chunk-IBFC25IX.js → chunk-ZYYSBNR6.js} +4 -4
  26. package/dist/hooks/codex-hook-adapter.js +10 -10
  27. package/dist/hooks/duplicate-read-guard.js +5 -5
  28. package/dist/hooks/format-edited.js +2 -2
  29. package/dist/hooks/investigation-guard.js +5 -5
  30. package/dist/hooks/pretool-guard.js +9 -9
  31. package/dist/hooks/stop-guard.js +206 -176
  32. package/dist/josh.js +14 -6
  33. package/package.json +3 -3
  34. package/prompts/collaboration-workflow/wip-cap.md +1 -1
  35. package/scripts/backlog/backlog-arrival.ts +11 -1
  36. package/scripts/backlog/backlog-budget.ts +25 -8
  37. package/scripts/backlog/backlog-drive-cli.ts +5 -1
  38. package/scripts/backlog/backlog-drive-retrospective.ts +20 -0
  39. package/scripts/backlog/backlog-drive.ts +7 -3
  40. package/scripts/backlog/backlog-next.ts +45 -2
  41. package/scripts/backlog/backlog-offer.ts +1 -0
  42. package/scripts/backlog/backlog-plan-cli.ts +66 -27
  43. package/scripts/backlog/backlog-plan.ts +30 -2
  44. package/scripts/backlog/backlog-stalled.ts +9 -0
  45. package/scripts/backlog/backlog-waves.ts +169 -0
  46. package/scripts/document/document-byte-budget.ts +1 -1
  47. package/scripts/document/entry-read-budget.ts +7 -5
  48. package/scripts/epic/epic-lane-offer.ts +69 -4
  49. package/scripts/epic/epic-next.ts +2 -2
  50. package/scripts/epic/epic-solo.ts +147 -0
  51. package/scripts/epic/epic-triage.ts +47 -0
  52. package/scripts/gate/hook-gate-reuse.ts +7 -8
  53. package/scripts/git/git-closes-keyword.ts +15 -0
  54. package/scripts/git/git-followup-flush.ts +5 -6
  55. package/scripts/git/git-followup-issue-close.ts +75 -0
  56. package/scripts/git/git-pr-followup-wrapup.ts +36 -6
  57. package/scripts/git/git-pr-followup.ts +9 -16
  58. package/scripts/git/git-staging.ts +33 -20
  59. package/scripts/git/issue-labels.ts +42 -0
  60. package/scripts/hooks/stop-guard.ts +1 -0
  61. package/scripts/init/init-install.ts +64 -0
  62. package/scripts/init/init.ts +38 -11
  63. package/scripts/init/project-profile.ts +9 -5
  64. package/scripts/init/start-plan.ts +145 -0
  65. package/scripts/init/start-prompt.ts +55 -0
  66. package/scripts/init/start-steps.ts +167 -0
  67. package/scripts/init/start.ts +92 -16
  68. package/scripts/issue/issue-closing-pr.ts +93 -0
  69. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +4 -2
  70. package/scripts/josh/josh-commands-project.ts +11 -3
  71. package/scripts/josh/josh-commands-workflow.ts +3 -3
  72. package/scripts/observations/observation-ledger-prepare.ts +31 -0
  73. package/scripts/observations/observation-ledger.ts +5 -5
  74. package/scripts/observations/observations-flush.ts +11 -30
  75. package/scripts/rules/implementation-cut.ts +49 -16
  76. package/scripts/rules/lane-background.ts +25 -5
  77. package/scripts/rules/stop-rules.ts +16 -4
  78. package/scripts/run/run-carry-cli.ts +21 -0
  79. package/scripts/run/run-cut-cli.ts +92 -110
  80. package/scripts/run/run-cut-report.ts +167 -0
  81. package/scripts/run/run-cut.ts +23 -7
  82. package/scripts/run/run-entry-cli.ts +36 -2
  83. package/scripts/run/run-headless.ts +1 -0
  84. package/scripts/run/run-hold-cli.ts +36 -15
  85. package/scripts/run/run-hold.ts +15 -4
  86. package/scripts/run/run-merge-cli.ts +28 -14
  87. package/scripts/run/run-merge-steps.ts +38 -2
  88. package/scripts/run/run-merge.ts +41 -3
  89. package/scripts/run/run-prep-cli.ts +8 -2
  90. package/scripts/run/run-prep-locate.ts +165 -0
  91. package/scripts/run/run-prep.ts +12 -1
  92. package/scripts/run/run-retrospective.ts +34 -0
  93. package/scripts/run/run-step-cli.ts +19 -5
  94. package/scripts/run/run-step.ts +31 -19
  95. package/scripts/run/run-tail-cli.ts +4 -2
  96. package/scripts/run/run-wake-work.ts +4 -0
  97. package/scripts/safe-chain/ci-installer-pin.ts +8 -4
  98. package/scripts/time-runtime/time-background.ts +11 -0
  99. package/scripts/time-runtime/time-transcript-line.ts +4 -0
  100. package/templates/pnpm-workspace.static.yaml +2 -0
@@ -8,6 +8,9 @@
8
8
  - Issue `pnpm josh git -y` in the background; its completion resumes the run, so the turn never ends
9
9
  at the push.
10
10
  - Keep `pnpm josh followup` in the foreground — nearly every following step reads its result.
11
+ - **Record before the CI wait ends, never after the merge** (joshuafolkken/kit#2763): the commit
12
+ carries the observation ledger lines recorded so far, and while CI runs the run makes the records
13
+ that need no CI result — observation Issues, the completion report draft.
11
14
  - Overlap only tree-readers, and compose merge-independent tail data before `followup` (the table
12
15
  below is the full mapping); keep `josh ms`, state checks, next-child selection, lane closing and the
13
16
  session-cost check after the merge.
@@ -3,24 +3,21 @@
3
3
  **Read this file in full once per session — before the first child is dispatched**, in the turn that
4
4
  reaches `pnpm josh delegate epic-child` (or `pnpm josh lane:dispatch`). **A later child does not re-read
5
5
  the whole file; it fetches only the section that child needs** — `pnpm josh doc:section backlogrun-child.md
6
- "<heading>"` — because a full re-read per child stacks this file's whole length onto the parent's
7
- conversation for every remaining request, which is exactly the n²/2 growth the hand-off exists to
8
- avoid (`backlogrun-progress.md`). It is a point-of-use document, never an entry read: the entry
9
- procedure is `backlogrun.md`, which points here at that step (joshuafolkken/kit#2010). This file is the
10
- single source of how one `backlogrun` child — of a named epic, a named issue, or the opted-in pool — is
11
- run.
6
+ "<heading>"`. It is a point-of-use document, never an entry read: the entry procedure is `backlogrun.md`,
7
+ which points here at that step. This file is the single source of how one `backlogrun` child — of a
8
+ named epic, a named issue, or the opted-in pool — is run. Rationale:
9
+ `docs/maintainers/backlogrun-child-rationale.md` → "Why the file is read once and then by section".
12
10
 
13
11
  ## Running a child — the shared mechanics
14
12
 
15
- Everything below runs **one child** — of a named epic, or of the opted-in backlog. It was the `epicrun` procedure until joshuafolkken/kit#1985 folded that keyword into `backlogrun`; a single-issue named item, a named epic's child and a bare backlog child all run the same `fullrun` in the same way, and this part is the single source of how. A named epic runs these mechanics over its own children until every one is processed, then the run advances to the next named item.
13
+ Everything below runs **one child** — of a named epic, or of the opted-in backlog. A single-issue named item, a named epic's child and a bare backlog child all run the same `fullrun` in the same way, and this part is the single source of how. A named epic runs these mechanics over its own children until every one is processed, then the run advances to the next named item.
16
14
 
17
15
  ## When `#N` is not an epic
18
16
 
19
17
  `backlogrun` accepts an ordinary Issue as well as an epic. `backlogrun #<N>` on an Issue with no task list
20
- runs `#<N>` as a `fullrun` and then finishes. Typing `backlogrun` up front is the batch authorization
21
- given once, before anything is known; a `fullrun` that discovers a split has to stop because it only
22
- had one Issue's authorization. So inside `backlogrun #<N>`, a prerequisite or a split found mid-run does
23
- **not** stop the run:
18
+ runs `#<N>` as a `fullrun` and then finishes. Inside `backlogrun #<N>`, a prerequisite or a split found
19
+ mid-run does **not** stop the run (rationale: `docs/maintainers/backlogrun-child-rationale.md` → "Why a
20
+ bare Issue promotes itself under `backlogrun` but not under `fullrun`"):
24
21
 
25
22
  1. File the new Issue(s) with the matching route label — `route:split` for a split, `route:tier-a`
26
23
  for a prerequisite — no confirmation.
@@ -28,9 +25,7 @@ had one Issue's authorization. So inside `backlogrun #<N>`, a prerequisite or a
28
25
  "A prerequisite discovered mid-run" do — `git stash push -u -m "..."` with the `-u`, the
29
26
  `gh api repos/{owner}/{repo}/issues/<N>/comments` post that records the stash, and
30
27
  `gh api -X DELETE repos/{owner}/{repo}/issues/<N>/labels/in-progress 2>/dev/null || true`.
31
- 3. **Ask `pnpm josh epic:bundle <N>` whether an epic already tracks `#<N>` before creating one.** It
32
- names the epic rather than only reporting that one exists — creating a second epic over it gives
33
- the auto-close two task lists to disagree about.
28
+ 3. **Ask `pnpm josh epic:bundle <N>` whether an epic already tracks `#<N>` before creating one.**
34
29
 
35
30
  | Answer | What to do |
36
31
  | --- | --- |
@@ -48,8 +43,7 @@ had one Issue's authorization. So inside `backlogrun #<N>`, a prerequisite or a
48
43
  | A split into independent children | `pnpm josh epic "<title>" <N> <N1> ...` — **no `--ordered`** |
49
44
  | A split whose children do have an order | `pnpm josh epic "<title>" ... --ordered`, arguments in that order |
50
45
 
51
- 4. **Run `pnpm josh epic:audit <E>` now**, not earlier — there is no epic to audit until step 3, and
52
- `epic:audit` refuses an Issue with no task list exactly as `epic:next` does.
46
+ 4. **Run `pnpm josh epic:audit <E>` now**, not earlier — there is no epic to audit until step 3.
53
47
  5. **Do not stop.** Continue into the loop below against the new epic `#<E>`.
54
48
 
55
49
  **Nothing found means no epic.** If `#<N>` reaches its merge without a prerequisite or a split turning
@@ -73,7 +67,8 @@ reference is named, the refusal is the whole answer. Where several are, a task-l
73
67
  ## Each child runs in a delegated unit
74
68
 
75
69
  **A child is not run in the parent loop's context.** One child goes to an isolated execution unit,
76
- and only its summary comes back.
70
+ and only its summary comes back. Rationale: `docs/maintainers/backlogrun-child-rationale.md` → "Why
71
+ each child runs in a delegated unit".
77
72
 
78
73
  **The supervisor's driver is the mechanical parent after hand-off.** It uses the same lane dispatch
79
74
  and GitHub verification commands; an AI parent handles only a returned judgment branch. *Every* child takes a
@@ -82,8 +77,8 @@ one sentence: **whatever offers a child — the loop, or a person clearing a lab
82
77
  to a lane, never to the parent.**
83
78
 
84
79
  **The mechanism is the one `pnpm josh delegate` defines** — the enumeration plus the command — with
85
- the unit changed from one step of a run to one child of an epic. Building a second is the clone
86
- `CLAUDE.md` prohibits. Ask the command rather than deciding:
80
+ the unit changed from one step of a run to one child of an epic. Never build a second. Ask the command
81
+ rather than deciding:
87
82
 
88
83
  ```bash
89
84
  pnpm josh delegate epic-child # → delegate
@@ -91,9 +86,8 @@ pnpm josh delegate epic-child # → delegate
91
86
 
92
87
  **In a lane, that unit is a detached operating-system process rather than a subagent of this
93
88
  session.** The verifier is still `pnpm josh issue:state <N>` read from GitHub, and the summary is
94
- still bounded at 25 lines. An in-process subagent could not survive the session cut the hand-off
95
- takes. `pnpm josh lane:dispatch` is where a lane's child is started; "Handing the child over" below
96
- carries the command.
89
+ still bounded at 25 lines. `pnpm josh lane:dispatch` is where a lane's child is started; "Handing the
90
+ child over" in `backlogrun-lanes.md` carries the command.
97
91
 
98
92
  **The lane child uses the invoking CLI's `worker` profile.** Claude Code defaults to Anthropic
99
93
  `claude-opus-5-5` / `medium`; Codex uses `codex exec`, OpenAI `gpt-6-sol` / `medium`, workspace-write and
@@ -109,11 +103,9 @@ loop on the summary alone** — that discards the verifier.
109
103
 
110
104
  ### What the summary carries, and how long it may be
111
105
 
112
- **The summary's only job is to carry what GitHub does not.** Anything already on GitHub is re-billed
113
- on every remaining turn of the run if it is put in the summary too. **"Make it shorter" is not the
114
- rule, because it deletes the wrong half** — a report cut by feel loses the observation nobody else
115
- recorded and keeps the file list anyone could have fetched. So both lists are written out, and the
116
- brief hands them to the unit.
106
+ **The summary's only job is to carry what GitHub does not.** Both lists below are written out, and the
107
+ brief hands them to the unit. Rationale: `docs/maintainers/backlogrun-child-rationale.md` → "Why the
108
+ summary is bounded the way it is".
117
109
 
118
110
  **Always kept — five things, because none of them is anywhere else:**
119
111
 
@@ -134,26 +126,23 @@ brief hands them to the unit.
134
126
  pull request's own file list), the per-round review detail (each round's verdict is one line; the
135
127
  findings are on the pull request), and restatements of rules the parent already holds.
136
128
 
137
- **The bound is 25 lines, and it is a number so that it is not a judgement.** A summary that cannot be
138
- said in 25 lines is describing the work rather than reporting it. **The brief states the bound**,
139
- because a unit never told it writes to the length its own report format suggests.
129
+ **The bound is 25 lines, and it is a number so that it is not a judgement.** **The brief states the
130
+ bound.**
140
131
 
141
132
  **This does not shorten the person-facing completion report.**
142
133
  `prompts/collaboration-workflow/report-format.md` → 「完了報告(セッション向け)」 is unchanged. What is
143
- bounded here is the child's hand-back to the parent, because the hand-back is re-read.
134
+ bounded here is the child's hand-back to the parent.
144
135
 
145
136
  **Read the state directly rather than asking `epic:next` again.** A child that did not finish still
146
137
  carries `in-progress`, which `epic:next` classifies as waiting on time before it consults any blocker.
147
138
 
148
- **The merge authorization reaches the unit**, and **so does the explicit invocation** — `CLAUDE.md` →
149
- "Explicit invocation required" forbids *inferring* a workflow, not requiring the keystroke in the
150
- unit's own transcript. **The brief therefore names the invocation it descends from** — `backlogrun #<E> --only`,
151
- the child number, and that the child is to be run as `fullrun #<N>` under that authorization. A brief
152
- that omits it is the defect: the unit is then guessing, and refusing is the correct answer to a guess.
139
+ **The merge authorization reaches the unit**, and **so does the explicit invocation**. **The brief
140
+ therefore names the invocation it descends from** — `backlogrun #<E> --only`, the child number, and that
141
+ the child is to be run as `fullrun #<N>` under that authorization. A brief that omits it is the defect,
142
+ and the unit refuses it.
153
143
 
154
- **Where no isolated unit exists, run the child in the parent's context.** The hand-off below is asked
155
- at every merge; delegation keeps each child's own context small and the hand-off bounds the parent
156
- session's length, so it is not an alternative to it.
144
+ **Where no isolated unit exists, run the child in the parent's context.** The hand-off below is still
145
+ asked at every merge; delegation is not an alternative to it.
157
146
 
158
147
  ## A delegated unit that stopped without reporting
159
148
 
@@ -161,7 +150,7 @@ session's length, so it is not an alternative to it.
161
150
  trips none of this file's guards, which all assume it is running. **So the parent checks rather than
162
151
  waiting — which means it must not be waiting.** **Hand the child to the unit without blocking on its
163
152
  return, and poll** at the loop's polling interval; ask once the unit's output has been unchanged for
164
- the silent-unit window (`| Silent delegated unit | 30 min |` below).
153
+ the silent-unit window (`| Silent delegated unit | 30 min |` in `backlogrun-progress.md`).
165
154
 
166
155
  **Note where the unit writes at hand-off; the modification time is read from the file, not carried.**
167
156
  **Where the child runs in a lane, that note goes into the lane rather than the conversation** —
@@ -189,13 +178,11 @@ pnpm josh run:liveness <N> --output <path> --process none --window 45 --repo <ow
189
178
  consecutive `undetermined` on the same child ends the polling: send a `confirmation` Telegram naming
190
179
  the trace that failed, and stop. **It is never escalated to a `stopped`** — nothing was read.
191
180
 
192
- **Silence and no process together — never either one alone**, because each alone has an innocent reading
193
- (a unit inside a long check writes nothing; a unit only reading has no check process) and together they
194
- do not. **Which way an error falls is the whole design**: a live unit booked as stopped has its work
195
- killed, a stopped one booked as alive only costs waiting — so **a trace that could not be read answers
181
+ **Silence and no process together — never either one alone.** **A trace that could not be read answers
196
182
  `undetermined`, never `stopped`**, and a process trace nobody gave is an unasked question rather than
197
183
  "no process". **Output that moved answers `alive` on its own; a live process is weighed only once every
198
- trace has answered.**
184
+ trace has answered.** Rationale: `docs/maintainers/backlogrun-child-rationale.md` → "Why liveness needs
185
+ silence and no process together".
199
186
 
200
187
  **The path passed to `--output` is absolute** — the command refuses a relative one. **The process
201
188
  trace is the one the command does not read for itself**: run it in the unit's checkout and read the
@@ -224,44 +211,34 @@ guard, and go back to step 1.
224
211
  2. **Classify how the child ended and act, in one call** — `pnpm josh run:merge <N> --output <path>`
225
212
  (add `--epic <E> --repo <owner/repo> --owner "$PPID"` for a named epic). **It is the same composite a
226
213
  returned child takes** (`backlogrun-progress.md` → "Running a named epic's children"), reached here
227
- from the poll rather than from a return — so a stopped child and a returned one run through **one**
228
- decision, never a second copy of it (joshuafolkken/kit#2277). It **reads the child's exit record
229
- without waiting for the unit to return**, telling an `outage` — a child that could not reach the API
230
- (joshuafolkken/kit#2240) — apart from an `abandoned` one that stopped mid-implementation, and it drops
214
+ from the poll rather than from a return — **one** decision, never a second copy of it. It **reads the
215
+ child's exit record without waiting for the unit to return**, telling an `outage` — a child that
216
+ could not reach the API — apart from an `abandoned` one that stopped mid-implementation, and it drops
231
217
  the stale `in-progress` itself, so there is no separate label-removal step:
232
218
  - **outage** — the child never reached the API, so it is counted into its own outage streak and
233
219
  **re-dispatched in the same run** by being offered again, **not** parked with `needs-decision` and
234
220
  **not** counted against the consecutive-failure guard. **The re-dispatch resumes the child's
235
- session** (joshuafolkken/kit#2317): `lane:dispatch` reads the `session_id` off the exit record and
236
- relaunches with `--resume`, keeping its context and losing only the last round-trip; with no session
237
- id it falls back to a fresh `fullrun`, and the report says which path it took. **A burst of outages
238
- from one network event folds into a single streak step** — outages inside a two-minute window count
239
- once — so one disconnection hitting several children no longer trips the guard. The re-dispatch is
240
- bounded by `CONSECUTIVE_OUTAGE_LIMIT` in `scripts/run/run-merge.ts`, so *distinct* outages (spaced
241
- past the fold window, as a dead API produces) trip the separate outage guard, at which point the
242
- command prints `environment` and the run stops. It never re-dispatches into a dead API forever.
221
+ session**: `lane:dispatch` reads the `session_id` off the exit record and relaunches with
222
+ `--resume`; with no session id it falls back to a fresh `fullrun`, and the report says which path
223
+ it took. **Outages inside a two-minute window count once** toward the streak. The re-dispatch is
224
+ bounded by `CONSECUTIVE_OUTAGE_LIMIT` in `scripts/run/run-merge.ts`: *distinct* outages trip the
225
+ separate outage guard, at which point the command prints `environment` and the run stops. It never
226
+ re-dispatches into a dead API forever.
243
227
  - **abandoned** — counted against the consecutive-failure guard and parked with `needs-decision`,
244
- exactly as a failed child. A silent retry would re-run a half-written tree; that guard is the only
245
- thing that notices the environment rather than the children is at fault.
228
+ exactly as a failed child. Never retried silently.
246
229
  3. **Read the token it printed and take that branch** — a next child number to run (the re-dispatched
247
230
  outage child among them), `environment` / `stop` to end the run, or `busy` / `retry` to re-read —
248
231
  the same tokens the merge event reads (`backlogrun-progress.md` → "Running a named epic's children").
249
232
  Then go back to step 1 of the loop.
250
233
 
251
- **Why the poll routes through `run:merge` rather than booking the stop itself.** The counting, the
252
- re-dispatch cap and the park all live in one place, so the poll cannot drift from the return path:
253
- counting the outage into its streak is what lets the cap trip, and skipping the count would
254
- re-dispatch into a dead API without bound. Reading the exit record here — not waiting for a unit that
255
- an API outage may never return — is what turns a 400-minute wait into a same-run re-dispatch
256
- (joshuafolkken/kit#2277).
234
+ Rationale: `docs/maintainers/backlogrun-child-rationale.md` → "Why the poll routes through `run:merge`".
257
235
 
258
236
  ## Audit before the first child
259
237
 
260
238
  Run `pnpm josh epic:audit <E>` before step 1 below. **When the run began from a bare Issue there is
261
- nothing to audit yet** — that path runs the audit at the moment it creates the epic instead. An epic
262
- whose children contradict each other stalls the moment the run reaches the contradiction, and
263
- unattended is the worst time to find that out. Errors stop the run; warnings are read and carried on
264
- past. Fixing what it finds is Tier A.
239
+ nothing to audit yet** — that path runs the audit at the moment it creates the epic instead. Errors
240
+ stop the run; warnings are read and carried on past. Fixing what it finds is Tier A. Rationale:
241
+ `docs/maintainers/backlogrun-child-rationale.md` → "Why the audit runs before the first child".
265
242
 
266
243
  ## `josh latest` runs once per session, not once per child
267
244
 
@@ -280,65 +257,50 @@ On `required`, load the `dependency-update` skill and follow its procedure — t
280
257
  `pnpm-workspace.yaml` and `package.json`, and the one expected `devEngines` pnpm bump. **The answer is
281
258
  the command's, never a judgement**, and `latest-gate.md` is its single source.
282
259
 
283
- **Session, not run** — the two differ whenever an epic spans repositories. Each session runs one
284
- repository's children, so each updates its own checkout: a second session that read "once per run" and
285
- skipped it would merge that repository's children against stale dependencies and never run `pnpm audit`
286
- there.
287
-
288
- **Waiting until a child is in hand is what keeps the tree clean.** Run it before the first `epic:next`
289
- and a run whose first answer is `wait`, `stop` or `complete` leaves a rewritten `pnpm-lock.yaml`
290
- modified on the default branch with nothing to commit it, which the next `git pull` refuses to merge
291
- over.
260
+ **Session, not run** — each session runs one repository's children, so each updates its own checkout;
261
+ never skip it because another repository's session already ran it. **Never ask it before a child is
262
+ in hand.**
292
263
 
293
264
  **The lock file the update rewrites lands with the first child.** The first child's `pnpm josh git -y`
294
- commits it — so that one PR carries the dependency bumps. What the hoist removes is the other N-1
295
- children carrying them; each child's PR would otherwise carry unrelated bumps that `/code-review` and
296
- CodeRabbit read, whose CI failures are attributed to the child, and which miss the eslint cache key
297
- (`hashFiles('pnpm-lock.yaml')`) on every PR. Should the first child fail CI on a bump, fix it forward
298
- before parking it.
265
+ commits it — so that one PR carries the dependency bumps and the other children carry none. Should the
266
+ first child fail CI on a bump, fix it forward before parking it.
299
267
 
300
268
  **`git switch main && git pull` stays per child** — it brings the previous child's merge into the tree,
301
269
  and a child that skips it implements on a stale main. Only the dependency update moves to the run. **In
302
270
  lanes it changes hands**: no lane can switch to the default branch, so the parent runs it in the primary
303
271
  checkout **before each `lane:open`**. **And in a lane `josh latest` is not even asked** — `latest:scope`
304
- skips and `latest:guard` refuses. The stash that carries the lock file into the first lane is in "Once
305
- per repository, before the first lane opens" above.
272
+ skips and `latest:guard` refuses. The stash that carries the lock file into the first lane is in
273
+ `backlogrun-lanes.md` → "Once per repository, before the first lane opens".
306
274
 
307
275
  This is the same rule `latest-gate.md` is the single source of.
308
276
 
309
- **A resumed `backlogrun` is a new session**, so it asks once again before its first child. The tree the
310
- resumed session finds may be days old — a session resumed within the window is told `skip`, one resumed
311
- a day later `required`.
277
+ **A resumed `backlogrun` is a new session**, so it asks once again before its first child.
312
278
 
313
- ## Preflight — reclaim what an interrupted run left, before the next child starts
279
+ Rationale: `docs/maintainers/backlogrun-child-rationale.md` → "Why `josh latest` is hoisted to the
280
+ session".
314
281
 
315
- **An unattended run ends abnormally** — a crash, a Ctrl-C, a laptop asleep, an expired token — and what
316
- it leaves is a **working tree**: a feature branch, an open pull request, uncommitted changes. The loop
317
- opens every child with `git switch main && git pull`, which refuses over a dirty tree, while an agent
318
- may not reach for `git stash` on its own judgement. So an unattended batch could not recover from its
319
- own crash.
282
+ ## Preflight — reclaim what an interrupted run left, before the next child starts
320
283
 
321
284
  **The child's claim is what asks it.** `pnpm josh run:hold <N>` — the first call of the child's
322
- `fullrun` procedure — now runs the preflight check before it takes the tree
323
- (joshuafolkken/kit#1965): it hands back a hold **only on a clean tree**, and returns
324
- `reclaim` / `resume` / `park` without claiming otherwise. There is no separate command to ask first;
325
- the two-step "preflight, then hold" is one call.
285
+ `fullrun` procedure — runs the preflight check before it takes the tree: it hands back a hold **only on
286
+ a clean tree**, and returns `reclaim` / `resume` / `park` without claiming otherwise. There is no
287
+ separate command to ask first. Rationale: `docs/maintainers/backlogrun-child-rationale.md` → "Why the
288
+ preflight is part of the claim".
326
289
 
327
290
  ```bash
328
291
  answer=$(pnpm josh run:hold 926) # alias: josh rh ; one token on stdout, prose on stderr
329
292
  ```
330
293
 
331
- | Answer | What it found | What to do |
332
- | --------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
333
- | `hold` | Nothing left behind; the tree is claimed | Start the child. This is the ordinary answer. |
334
- | `reclaim` | Uncommitted changes, or HEAD off the default branch | Run what stderr printed: `git stash push -u -m "run:hold reclaimed before #<N>"`, then `git switch <default> && git pull`. **`-u` is not optional** (a new `*.test.ts` is untracked). **Then record the stash on `#<N>` as a comment, and ask `run:hold` again** — the check is re-askable. |
335
- | `resume` | A branch for `#<N>`, or an open pull request, is still here | **Reuse it and run the whole verification gate from the start** — never only the part the interrupted run had not reached. The tools are idempotent; what was missing was that **nobody had verified what the dead run already committed**. |
336
- | `park` | The pull request for `#<N>` is **merged** or **closed** | **Park the child** — `needs-decision` plus a comment naming what was found — and continue. Carrying on over a merged PR duplicates work; over a closed one revives rejected work. Do not delete the branch, reopen the PR, or commit on top of it. |
337
- | `busy` | Another **live** run holds this tree | Stop before filing anything — `run:hold`'s own answer, unchanged. |
338
- | `unknown` | The tree could not be read | Stop the session and report. It is not "the tree is clean". |
294
+ | Answer | What it found | What to do |
295
+ | --- | --- | --- |
296
+ | `hold` | Nothing left behind; the tree is claimed | Start the child. This is the ordinary answer. |
297
+ | `reclaim` | Uncommitted changes, or HEAD off the default branch | Run what stderr printed: `git stash push -u -m "run:hold reclaimed before #<N>"`, then `git switch <default> && git pull`. **`-u` is not optional** (a new `*.test.ts` is untracked). **Then record the stash on `#<N>` as a comment, and ask `run:hold` again** — the check is re-askable. |
298
+ | `resume` | A branch for `#<N>`, or an open pull request, is still here | **Reuse it and run the whole verification gate from the start** — never only the part the interrupted run had not reached. The tools are idempotent; what was missing was that **nobody had verified what the dead run already committed**. |
299
+ | `park` | The pull request for `#<N>` is **merged** or **closed** | **Park the child** — `needs-decision` plus a comment naming what was found — and continue. Carrying on over a merged PR duplicates work; over a closed one revives rejected work. Do not delete the branch, reopen the PR, or commit on top of it. |
300
+ | `busy` | Another **live** run holds this tree | Stop before filing anything — `run:hold`'s own answer, unchanged. |
301
+ | `unknown` | The tree could not be read | Stop the session and report. It is not "the tree is clean". |
339
302
 
340
- **The rule answers, so the run does not judge.** "There is a branch, I will carry on" and "there is a
341
- branch, I had better stop" are both defensible in the moment, which is why the choice is not left to it.
303
+ **The rule answers, so the run does not judge.**
342
304
 
343
305
  **The `git stash` above is one of the flows that authorizes automatic stashing**
344
306
  (`prompts/collaboration-workflow/operating-rules.md` → the `git stash` bullet). **It is the one entry
@@ -350,15 +312,8 @@ divert.
350
312
  **Report what was reclaimed** — a child that started from a `reclaim` or a `resume` says so, with the
351
313
  stash reference where there was one.
352
314
 
353
- **The claim and the preflight are one call now** (joshuafolkken/kit#1965). `run:hold` used to ask only
354
- *whether another live run owns this tree* — claimed once, per child — while a separate `run:preflight`
355
- asked *what a dead run left in it*. The claim now does both, taking the tree only when the preflight
356
- reads `clean`. The `reclaim` arm stays re-askable, because the check writes nothing: recover what it
357
- named and ask `run:hold` once more, and the now-clean tree is claimed.
358
-
359
- **It is not asked in a lane.** Every lane's HEAD is on `<N>-lane`, so the `reclaim` arm would fire on
360
- all of them and its recovery cannot run in a linked work tree — so `run:hold` claimed inside a lane
361
- skips the check, and `lane:open`'s own answer covers an interrupted lane instead. **Asked in the
362
- primary checkout while a lane for that child is open, `run:hold` would read `resume`, which is
363
- correct** — the branch read `git branch --list '<N>-*'` matches `<N>-lane`. The loop never reaches it
364
- that way, because `epic:next` does not offer a child already carrying `in-progress`.
315
+ **It is not asked in a lane** — `run:hold` claimed inside a lane skips the check, and `lane:open`'s own
316
+ answer covers an interrupted lane instead. **Asked in the primary checkout while a lane for that child
317
+ is open, `run:hold` would read `resume`, which is correct** — the branch read
318
+ `git branch --list '<N>-*'` matches `<N>-lane`. The loop never reaches it that way, because `epic:next`
319
+ does not offer a child already carrying `in-progress`.