@joshuafolkken/kit 1.957.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 (91) 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 +3 -3
  14. package/dist/CLAUDE.md +3 -3
  15. package/dist/hooks/batch-guard.js +4 -4
  16. package/dist/hooks/{chunk-XSL4SRMA.js → chunk-2K52BRWV.js} +2 -2
  17. package/dist/hooks/{chunk-M6K7EQDO.js → chunk-BNWIOJFU.js} +29 -16
  18. package/dist/hooks/{chunk-KOCK5FXQ.js → chunk-BVMSPG3M.js} +3 -3
  19. package/dist/hooks/{chunk-3ON7HOYB.js → chunk-CRGJTD6E.js} +1 -1
  20. package/dist/hooks/{chunk-HJXQZ7V4.js → chunk-F6OM6SIG.js} +3 -3
  21. package/dist/hooks/{chunk-LDQDZYHX.js → chunk-KQG4776P.js} +1 -1
  22. package/dist/hooks/{chunk-244WYOEO.js → chunk-NXL3SBXY.js} +2 -2
  23. package/dist/hooks/{chunk-IJPBTDTB.js → chunk-WWTMHHZU.js} +59 -18
  24. package/dist/hooks/{chunk-BELI5XSS.js → chunk-ZPLXLJ7O.js} +15 -5
  25. package/dist/hooks/{chunk-GNOITLRR.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 +8 -4
  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-next.ts +45 -2
  38. package/scripts/backlog/backlog-offer.ts +1 -0
  39. package/scripts/backlog/backlog-plan-cli.ts +66 -27
  40. package/scripts/backlog/backlog-plan.ts +30 -2
  41. package/scripts/backlog/backlog-stalled.ts +9 -0
  42. package/scripts/backlog/backlog-waves.ts +169 -0
  43. package/scripts/document/document-byte-budget.ts +1 -1
  44. package/scripts/document/entry-read-budget.ts +7 -5
  45. package/scripts/epic/epic-lane-offer.ts +69 -4
  46. package/scripts/epic/epic-next.ts +2 -2
  47. package/scripts/epic/epic-solo.ts +147 -0
  48. package/scripts/epic/epic-triage.ts +47 -0
  49. package/scripts/gate/hook-gate-reuse.ts +7 -8
  50. package/scripts/git/git-closes-keyword.ts +15 -0
  51. package/scripts/git/git-followup-flush.ts +5 -6
  52. package/scripts/git/git-followup-issue-close.ts +75 -0
  53. package/scripts/git/git-pr-followup-wrapup.ts +36 -6
  54. package/scripts/git/git-pr-followup.ts +9 -16
  55. package/scripts/git/git-staging.ts +33 -20
  56. package/scripts/git/issue-labels.ts +23 -0
  57. package/scripts/hooks/stop-guard.ts +1 -0
  58. package/scripts/init/init-install.ts +64 -0
  59. package/scripts/init/init.ts +38 -11
  60. package/scripts/issue/issue-closing-pr.ts +93 -0
  61. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +2 -1
  62. package/scripts/josh/josh-commands-project.ts +5 -1
  63. package/scripts/josh/josh-commands-workflow.ts +3 -3
  64. package/scripts/observations/observation-ledger-prepare.ts +31 -0
  65. package/scripts/observations/observation-ledger.ts +5 -5
  66. package/scripts/observations/observations-flush.ts +11 -30
  67. package/scripts/rules/implementation-cut.ts +49 -16
  68. package/scripts/rules/lane-background.ts +25 -5
  69. package/scripts/rules/stop-rules.ts +16 -4
  70. package/scripts/run/run-carry-cli.ts +21 -0
  71. package/scripts/run/run-cut-cli.ts +92 -110
  72. package/scripts/run/run-cut-report.ts +167 -0
  73. package/scripts/run/run-cut.ts +23 -7
  74. package/scripts/run/run-entry-cli.ts +36 -2
  75. package/scripts/run/run-headless.ts +1 -0
  76. package/scripts/run/run-hold-cli.ts +36 -15
  77. package/scripts/run/run-hold.ts +15 -4
  78. package/scripts/run/run-merge-cli.ts +28 -14
  79. package/scripts/run/run-merge-steps.ts +38 -2
  80. package/scripts/run/run-merge.ts +41 -3
  81. package/scripts/run/run-prep-cli.ts +8 -2
  82. package/scripts/run/run-prep-locate.ts +165 -0
  83. package/scripts/run/run-prep.ts +12 -1
  84. package/scripts/run/run-step-cli.ts +16 -0
  85. package/scripts/run/run-step.ts +28 -4
  86. package/scripts/run/run-tail-cli.ts +4 -2
  87. package/scripts/run/run-wake-work.ts +4 -0
  88. package/scripts/safe-chain/ci-installer-pin.ts +8 -4
  89. package/scripts/time-runtime/time-background.ts +11 -0
  90. package/scripts/time-runtime/time-transcript-line.ts +4 -0
  91. package/templates/pnpm-workspace.static.yaml +2 -0
@@ -18,11 +18,9 @@ by that supervisor; an AI parent is started only for a driver branch that requir
18
18
  pnpm josh run:progress --wait --output <the transcript path of each delegated unit> # in the background
19
19
  ```
20
20
 
21
- **`--wait` waits one silence interval out, prints one line and exits — and the exit is what makes the
22
- line arrive.** A harness that delivers a background command's output *when that command exits* — Claude
23
- Code is one — relays nothing from a watcher that never exits; the long-running form (no `--wait`) is
24
- right only where output is streamed. The parent changes no value on the line. **Every interval is three
25
- moves and no fourth:**
21
+ **`--wait` waits one silence interval out, prints one line and exits** — the long-running form (no
22
+ `--wait`) is right only where output is streamed. The parent changes no value on the line. **Every
23
+ interval is three moves and no fourth:**
26
24
 
27
25
  1. Start `pnpm josh run:progress --wait --output <transcript paths>` **in the background**.
28
26
  2. When it exits, **present what it printed as-is** — the command prints the five labelled lines and the
@@ -35,8 +33,7 @@ lane children are in-flight but the watcher's life record has gone stale. **It i
35
33
  until the watcher is restarted — once per run, so the `pnpm josh run:progress --wait` that fixes it is
36
34
  not blocked.
37
35
 
38
- **`run:progress` prints the five labelled lines itself now (joshuafolkken/kit#2026); present them
39
- as-is.** It emits the observation instant (`at`, local first and UTC beside it), the elapsed figures
36
+ **`run:progress` prints the five labelled lines itself (joshuafolkken/kit#2026); present them as-is.** It emits the observation instant (`at`, local first and UTC beside it), the elapsed figures
40
37
  (`quiet` and `unchanged`), the children in flight, the run state (`lanes`, `load`, `record`), and the
41
38
  scheduled `next`, each behind its own label and on its own line — so relay the output verbatim and
42
39
  round, rephrase or re-label nothing. How each field reads when it has nothing to report (`record
@@ -47,30 +44,24 @@ running*; and the `next` line is a schedule — the time *if the silence continu
47
44
  real report resets the clock through `--mark`.
48
45
 
49
46
  **It exits only when it has a line to hand over**, or when `--hours` runs out having never gone quiet
50
- for a whole interval (that exit says so on standard error). **Nothing in flight keeps it waiting**, or
51
- step 3 would make it a poll.
47
+ for a whole interval (that exit says so on standard error). **Nothing in flight keeps it waiting.**
52
48
 
53
49
  **`--mark` at every real report.** Whenever this loop reports something of its own — a child merged,
54
- parked, a stop — run `pnpm josh run:progress --mark` in the same turn to restart the silence clock, so a
55
- heartbeat does not land immediately behind a real report. The clock is silence, never a timer
50
+ parked, a stop — run `pnpm josh run:progress --mark` in the same turn to restart the silence clock. The clock is silence, never a timer
56
51
  (`docs/josh-commands.md` → "`josh run:progress`").
57
52
 
58
- **Do not keep a progress clock of your own** — the hook refuses the arm rather than asking. A `Bash`
59
- call that only sleeps adds a second clock nobody reconciles, so `scripts/rules/early-heartbeat.ts` →
60
- `decide` refuses a sleep-only `Bash` call on three tests — a timer it already allowed is still live, the
53
+ **Do not keep a progress clock of your own** — the hook refuses the arm rather than asking.
54
+ `scripts/rules/early-heartbeat.ts` → `decide` refuses a sleep-only `Bash` call on three tests — a timer it already allowed is still live, the
61
55
  report it would produce would land before the interval is up, or the wait runs longer than the interval
62
56
  — so **a single correctly-spaced arm is allowed**. It is still not the way to report; the step above
63
57
  prints without arming anything, and `--wait` is not a wait timer.
64
58
 
65
59
  **The default interval is twenty minutes, overridable — by the person, not the run.**
66
60
  `JOSH_PROGRESS_INTERVAL_MINUTES` moves both sides (the guard reads it through the watcher's reader);
67
- `josh` → `progress_interval_minutes` in `package.json` sits one step below the variable, so a cadence
68
- set once holds on every machine and cloud session. **`--interval` moves the watcher alone** — a hook
69
- has no command line — so it makes the watcher quieter than the floor, never the guard stricter. Twenty
70
- rather than ten because a child measures 20–46 minutes.
61
+ `josh` → `progress_interval_minutes` in `package.json` sits one step below the variable. **`--interval`
62
+ moves the watcher alone** — it makes the watcher quieter than the floor, never the guard stricter.
71
63
 
72
- **An explicit ask is not a heartbeat, and it is exempt by construction** — what is refused is arming a
73
- *timer*, and a person asking "how is it going" arrives with no timer in front of it.
64
+ **An explicit ask is not a heartbeat, and it is exempt by construction.**
74
65
  **Every unscheduled progress statement is answered by `pnpm josh run:progress --once`** — presented in
75
66
  the same five field lines, the reply to an explicit ask and the note just after a run starts alike.
76
67
  **Never write a clock time the command did not print** (not an approximation, not a placeholder — where
@@ -79,15 +70,14 @@ is counted from the record the guard writes when it allows one**, never from the
79
70
  processes.
80
71
 
81
72
  **Every report opens with the time the observation was taken** — not only how long it has been quiet: an
82
- absolute instant, `at YYYY-MM-DD HH:MM±HH:MM / YYYY-MM-DDTHH:MMZ`. A relative figure means something
83
- only while the reports keep coming, and unattended execution is made of the events that break that — a
84
- suspend, a rate limit, a restart. **The date is part of it**, and **the local clock leads with UTC
85
- beside it and the offset that ties them** (the line is relayed to other machines and read in cloud
86
- sessions). **It is added, never substituted for the elapsed figure**, and **the presented line needs no
87
- stamp computed for it** — the command prints the `at` stamp and the `next` field.
73
+ absolute instant, `at YYYY-MM-DD HH:MM±HH:MM / YYYY-MM-DDTHH:MMZ`. **The date is part of it**, and **the local clock
74
+ leads with UTC beside it and the offset that ties them**. **It is added, never substituted for the
75
+ elapsed figure**, and **the presented line needs no stamp computed for it** — the command prints the
76
+ `at` stamp and the `next` field.
88
77
 
89
78
  **The line carries observations, never "still running", and nothing in it is a verification result** —
90
- no gate, CI or check rollup, because the command reads none of them.
79
+ no gate, CI or check rollup, because the command reads none of them. Rationale:
80
+ `docs/maintainers/backlogrun-progress-rationale.md` → "Why the heartbeat reads as it does".
91
81
 
92
82
  **The invariant is a tier, not a mechanism** (joshuafolkken/kit#2156, joshuafolkken/kit#2207). Three
93
83
  tiers say how much of a person's attention a signal takes: **interrupt** reaches for it now (a Telegram),
@@ -101,59 +91,48 @@ stop, a merge — are appended to one ordered stream keyed to the run's identity
101
91
  which survives the cut because it is the run's and not any one session's. **Every session is a writer;
102
92
  the reader is a script in a pane of the person's own, never a conversation** (#2492):
103
93
  `pnpm josh run:event --watch` prints each event the moment it lands, in the session language, and the
104
- `heartbeat` events say the run is alive when quiet. A relaying session re-read its whole history per
105
- event, so **no session relays the stream** — name the watch command once, at the first cut or when
94
+ `heartbeat` events say the run is alive when quiet. **No session relays the stream** — name the watch command once, at the first cut or when
106
95
  asked. **It is the same reader before and after the cut**: a cut moves who executes, never where the
107
96
  stream lives.
108
97
 
109
- **Interrupt is withheld on purpose**: no Telegram, because `confirmation` and `completion` are what
110
- interrupt a person, and a line every fifteen minutes is the fatigue that stops them being
111
- read. **`pnpm josh run:wake --list` is the requested tier** — the last-event read of the stream the
98
+ **Interrupt is withheld on purpose**: no Telegram for the heartbeat. **`pnpm josh run:wake --list` is the requested tier** — the last-event read of the stream the
112
99
  watch pane reads, for a person who types for one line rather than watching. **`tail -F` on the
113
100
  raw stream file is a recovery path, not the ambient surface** — the watch pane is how progress is seen
114
101
  without asking, and the raw `tail` is the fallback when the pane has stopped, named for that role in
115
102
  `--list`'s own output.
116
103
 
117
- **A stop is an interrupt, and only a stop** (joshuafolkken/kit#2136). The heartbeat says a run is still
118
- going; a run that has _stopped_ — every remaining child blocked behind a parked one, the backlog drained
119
- of anything runnable, the failure streak tripped — is the event the ambient tier was hiding, because
120
- after a cut "quiet" and "stopped" look identical until a person reads for it. So the terminal stop is
121
- pushed to the interrupt tier: `pnpm josh run:carry --end --stopped "<reason>"` sends one ⏸️ confirmation
104
+ **A stop is an interrupt, and only a stop** (joshuafolkken/kit#2136). A run that has _stopped_ — every
105
+ remaining child blocked behind a parked one, the backlog drained of anything runnable, the failure
106
+ streak tripped — is pushed to the interrupt tier: `pnpm josh run:carry --end --stopped "<reason>"` sends one ⏸️ confirmation
122
107
  as it ends the record, and because the record is gone by the second `--end` the same stop never notifies
123
108
  twice. A parked child is pushed by the child itself, which records the park and sends its own
124
109
  `confirmation` before it stops (`backlogrun-park.md`). **Nothing new carries either one** — both ride the
125
- existing notification types and the record the watcher already keeps.
110
+ existing notification types and the record the watcher already keeps. Rationale for the tiers:
111
+ `docs/maintainers/backlogrun-progress-rationale.md` → "Why the signal tiers are split this way".
126
112
 
127
113
  **A heartbeat is emitted from the moment a run has started, even before any child carries
128
114
  `in-progress`** — "a run has started" is read from a mechanical record (a registered lane, a held work
129
115
  tree, or a carried budget), and the line names that stage as an observed fact (`no in-progress child
130
- yet`). **Only a checkout with no run recorded at all stays silent**, which keeps an ordinary
131
- conversational session outside the heartbeat.
116
+ yet`). **Only a checkout with no run recorded at all stays silent.**
132
117
 
133
118
  **The scope is every implementing run, not this command alone.** `fullrun`, `halfrun` and `backlogrun`
134
119
  start the same watcher under the same rules, and this section is the single source for all four; `kickoff`
135
- starts none. **`halfrun` is included** because the trigger is silence rather than command identity — it
136
- still implements, runs the whole gate, both review rounds and `pnpm josh test:e2e`, most of the 20–46
137
- minutes.
120
+ starts none. **`halfrun` is included.**
138
121
 
139
122
  **One watcher per run, and the outermost invocation is the one that starts it.** A `fullrun` running as
140
123
  a `backlogrun` named issue or an `backlogrun` child starts none: the brief names the invocation it descends
141
124
  from. `backlogrun` starts one for the whole batch. **A dispatched lane child is refused a watcher by its
142
125
  `JOSH_LANE_CHILD` mark** — it still runs `--mark` for the parent's clock, but every reporting form
143
- (`--wait`, `--once`, the default watch) exits at once with a notice, so a child that misreads the prose
144
- is harmless.
126
+ (`--wait`, `--once`, the default watch) exits at once with a notice.
145
127
 
146
- **In a single-issue run it starts immediately after `pnpm josh run:hold` succeeds** — the hold is itself
147
- the record that says the run has started. **It is started in the target repository's checkout, and
148
- `--mark` is run there too** — a cross-repository run is implemented in that repository's checkout, and a
149
- watcher started in the session's own tree would read the wrong `in-progress` listing.
128
+ **In a single-issue run it starts immediately after `pnpm josh run:hold` succeeds.** **It is started in the target repository's checkout, and
129
+ `--mark` is run there too.**
150
130
 
151
131
  **What counts as a real report when the run has one issue.** A `fullrun` has exactly one child, so the
152
132
  unit is **any turn that puts a progress statement in front of the person** — four of them, `--mark` run
153
133
  in the same turn as each: **the Step 0 work summary, the pull request opening, each review round's
154
134
  verdict, and any `confirmation` / `failure` / `completion` notification or stop.** **A tool result only
155
- you read is not one** — a gate run, a `gh` read, an edit — and marking on those would hide the silence
156
- the interval measures.
135
+ you read is not one** — a gate run, a `gh` read, an edit.
157
136
 
158
137
  **On a quiet tick the turn is the relayed line plus at most two lines of the run's own prose**, saying
159
138
  only what changed stage and what is being waited on. **A tick is quiet when none of the real reports
@@ -163,9 +142,9 @@ plan** — `epic:next`, the epic body and the progress comment hold all three. *
163
142
  bounded by that**: `--mark` restarts the clock, so a merge, a park or a stop is where the run may be
164
143
  long.
165
144
 
166
- **`--output` is omitted in a single-issue run, and `record` reads `unread`** — there is no delegated
167
- unit to name (a batch's unit changes every issue), so a fixed path would age a finished unit's file;
168
- `unread` is the command's defined answer for "no path was given".
145
+ **`--output` is omitted in a single-issue run, and `record` reads `unread`.** Rationale for the
146
+ watcher's scope and placement: `docs/maintainers/backlogrun-progress-rationale.md` → "Why the watcher
147
+ runs where it does".
169
148
 
170
149
  **A run that merges needs no teardown; a run that stops has to end the reporting itself.** The issue
171
150
  leaves the `in-progress` listing at the merge, so whatever is waiting prints nothing and `--hours` ends
@@ -210,21 +189,16 @@ single non-numeric line as the verdict.
210
189
 
211
190
  **Start the unit without blocking on it — `pnpm josh lane:dispatch <N>` when the child runs in a
212
191
  lane — and in the same turn start `pnpm josh lane:await <N...>` in the background** (joshuafolkken/kit#2113).
213
- `lane:await` watches local process presence and exits when any named child confirms-complete,
214
- waking the parent at the actual completion rather than at the next heartbeat interval. **The
192
+ `lane:await` watches local process presence and exits when any named child confirms-complete. **The
215
193
  re-confirm delay and poll interval are the command's, not the agent's** — pass only the issue
216
194
  numbers. Without that wake (Codex), hand off instead — "A parent without a completion callback".
217
195
 
218
196
  **A `lane:await` wake is a confirmed-gone process, so classify the ending at once — do not wait out
219
- the silent-unit window** (joshuafolkken/kit#2277). `lane:await` exits only after the child's process
220
- has disappeared and stayed gone across its re-confirm delay, which is the definitive end signal; a
221
- child that died reaching the API wrote its last line — an `API Error` — just before it went, so a
222
- `run:liveness` poll would read that fresh output as `alive` and sit for the whole 30-minute silent-unit
223
- window before it ever said `stopped` (the measured 402-/52-minute waits). So on a `lane:await` wake go
224
- straight to `pnpm josh run:merge <N> --output <path>` — the composite reads the child's **exit
197
+ the silent-unit window** (joshuafolkken/kit#2277). On a `lane:await` wake go straight to `pnpm josh run:merge <N> --output <path>` — the composite reads the child's **exit
225
198
  record**, decisive whether the child merged, died on an outage, or abandoned mid-implementation
226
199
  ("Running a named epic's children" reads its tokens). An outage child is re-dispatched in the same run
227
- and a run of them trips the environment guard; nothing waits for a person.
200
+ and a run of them trips the environment guard; nothing waits for a person. Rationale:
201
+ `docs/maintainers/backlogrun-progress-rationale.md` → "Why a child's ending is classified at once".
228
202
 
229
203
  **`run:liveness` stays the fallback for a lane no `lane:await` is watching** — a handed-over lane a
230
204
  resumed session polls ("Picking the lanes up in the fresh session"). Ask
@@ -247,8 +221,7 @@ single non-numeric line as the verdict.
247
221
  the repository this session runs in.
248
222
 
249
223
  `state: CLOSED` is the only answer that means the child finished. **Read the `human_review:` and
250
- `labels:` lines before calling anything else a failure** — three outcomes look alike from here, which
251
- is why one command prints all of them. **A non-zero exit is not `OPEN`**: the command exits non-zero
224
+ `labels:` lines before calling anything else a failure.** **A non-zero exit is not `OPEN`**: the command exits non-zero
252
225
  without a state when the number resolves to nothing (`does not resolve`) or the read failed (`could
253
226
  not read`, a rate limit or expired auth). Re-read before deciding.
254
227
 
@@ -256,22 +229,17 @@ single non-numeric line as the verdict.
256
229
  **not** count it against the consecutive-failure guard, and go back to step 1.
257
230
  - **Open, and `human_review: yes`** — the child **stopped before its commit**, the run's own ending
258
231
  (§2z). **`Open` is part of the test** — a CLOSED child carrying the label finished and merged.
259
- **Read that line, not the `labels:` one**: GitHub keeps the spelling a label was created with, so
260
- matching the lowercase string by eye drops the child into the failure branch below. Leave
232
+ **Read that line, not the `labels:` one.** Leave
261
233
  `in-progress` **on**, do not park it, do not count it, and do **not** go back to step 1. Finish the
262
234
  session and report. **Do not send a second `confirmation` Telegram** — the unit already sent one.
263
235
  Where the child ran in this session's own context, that first notification is yours to send.
264
236
  - **Open, without `needs-decision`** — it failed. Remove the stale `in-progress` here (Tier A), count
265
- it against the consecutive-failure guard, and **park it**. Parking is what stops the next
266
- `epic:next` from handing the same child straight back.
237
+ it against the consecutive-failure guard, and **park it**.
267
238
 
268
- **Never ask `epic:next` in place of this read.** A child that did not finish still carries
269
- `in-progress`, which `epic:next` classifies as waiting on time — the loop would poll to the 90-minute
270
- stale window and learn nothing.
239
+ **Never ask `epic:next` in place of this read.**
271
240
 
272
241
  **A merge is one event, and one call of the parent's — `pnpm josh run:merge <N>`**
273
- (joshuafolkken/kit#2024). What was a reading turn and an acting turn is one composite command: it
274
- confirms the child from GitHub, does the post-merge steps, folds in the hand-off check, and prints
242
+ (joshuafolkken/kit#2024). The composite command confirms the child from GitHub, does the post-merge steps, folds in the hand-off check, and prints
275
243
  the next child number — or a control verdict — for the parent to read. Pass the merged child's
276
244
  number and the offer's source — `--epic <E> --repo <owner/repo>` for a named epic,
277
245
  nothing for the opted-in backlog — with `--owner "$PPID"` so it counts into the carry record under
@@ -282,9 +250,9 @@ single non-numeric line as the verdict.
282
250
  # a child number (or several, one per free lane) to run next, or a verdict token
283
251
  ```
284
252
 
285
- **Pass `--output <path>` — the child's transcript** (joshuafolkken/kit#2240). It is what lets the
286
- composite tell an API-outage ending apart from a genuine child failure: without it an OPEN, unparked
287
- child is a plain failure as before, so the flag is how the outage split is turned on.
253
+ **Pass `--output <path>` — the child's transcript** (joshuafolkken/kit#2240). Without it the
254
+ composite cannot tell an API-outage ending from a genuine failure: an OPEN, unparked child is a plain
255
+ failure.
288
256
 
289
257
  **What the one call does is decided by what the child turned out to be** — read from its GitHub state
290
258
  (and, for the failed case alone, its exit record), never from a log: a **merged** child (CLOSED) is
@@ -295,8 +263,7 @@ single non-numeric line as the verdict.
295
263
  **not** parked and **not** counted against the consecutive-failure guard, and stays re-dispatchable
296
264
  (joshuafolkken/kit#2240); a **failed** child (OPEN, neither label, not an outage) has its stale
297
265
  `in-progress` dropped, is parked with `needs-decision`, and is counted against the consecutive-failure
298
- guard. **A turn whose whole content is one read, or one two-line progress report, is the shape this
299
- collapses.**
266
+ guard.
300
267
 
301
268
  **Beyond the offer `epic:next` prints** (`run` becomes numbers; `wait` / `stop` / `complete` /
302
269
  `error` pass through), the composite adds six verdict tokens: `over` — the merge crossed the budget,
@@ -311,8 +278,7 @@ single non-numeric line as the verdict.
311
278
  call** — `pnpm josh cost --cut`, run after `pnpm josh ms`; `under` offers the next child,
312
279
  `over` prints `over` so the parent hands its lanes over and takes the cut ("The hand-off" below), and
313
280
  a session that cannot measure is read as `over`. **Never read the condition off `pnpm josh delegate
314
- epic-child`** — a static policy lookup that answers `delegate` everywhere, so a gate built on it never
315
- fires.
281
+ epic-child`** ("The check is asked at every merge" below).
316
282
  3. **`wait`** — go back to step 1. **With something of this run's own in flight, that happens on the
317
283
  wake the progress watcher's exit delivers** and the parent starts no sleep of its own; the 60 s
318
284
  figure bounds how soon the ask may be repeated. **With nothing in flight the watcher declines and
@@ -336,19 +302,18 @@ in the repository that owns it ("Who sends the summary, and who propagates" belo
336
302
 
337
303
  ## The hand-off — one session does not have to run the whole batch
338
304
 
339
- **A session pays for every child it has already run, on every later turn**, because every turn re-reads
340
- the accumulated preamble. **So the run reads the marginal cost off a line rather than feeling for it** —
341
- at **every** child's merge, and crossing it **hands the session off to a fresh one, carrying the budget
342
- with it** (`backlogrun-steps.md` → "The session cut is inside the invocation"). The cut is where a `backlogrun` spans several
343
- sessions; it is not a stop.
305
+ **The run reads the marginal cost off a line rather than feeling for it** — at **every** child's
306
+ merge, and crossing it **hands the session off to a fresh one, carrying the budget with it**
307
+ (`backlogrun-steps.md` → "The session cut is inside the invocation"). The cut is where a `backlogrun`
308
+ spans several sessions; it is not a stop. Rationale: `docs/maintainers/backlogrun-progress-rationale.md`
309
+ → "Why the hand-off check is asked at every merge".
344
310
 
345
311
  ```bash
346
312
  pnpm josh cost --cut
347
313
  ```
348
314
 
349
315
  It prints `over` or `under` on standard output and the measured figure on standard error. `over` means
350
- the next turn of this session costs more than the threshold in billed input, and the number is passed
351
- explicitly so a run cannot drift it by remembering it wrong.
316
+ the next turn of this session costs more than the threshold in billed input.
352
317
 
353
318
  **135,000 is shared by the scheduler entry, scheduler hand-off and lane worker implementation cut.**
354
319
  The value is `CONTEXT_CUT_THRESHOLD` in `scripts/cost-runtime/context-cut-threshold.ts`; `cost --cut`,
@@ -357,23 +322,18 @@ numbers. The output ceiling and the entry-read figure are a separate 150,000, no
357
322
 
358
323
  ### The check is asked at every merge, and delegation does not excuse it
359
324
 
360
- **A delegating parent reaches the threshold too**, because most of its billed input is conversation
361
- history rather than resident preamble, and the cost is not linear — n requests bill about n²/2, so a
362
- condition that delays the first cut multiplies a cost rather than deferring it. **So there is no
363
- condition: `pnpm josh cost --cut` is asked after every child's merge**, delegated or not.
364
- **Never wire the question to `pnpm josh delegate epic-child`** — a static policy lookup answering
365
- `delegate` on every machine forever, so a gate built on it never fires.
325
+ **A delegating parent reaches the threshold too, so there is no condition: `pnpm josh cost --cut` is
326
+ asked after every child's merge**, delegated or not.
327
+ **Never wire the question to `pnpm josh delegate epic-child`** — a static policy lookup that answers
328
+ `delegate` everywhere.
366
329
 
367
330
  `over` and `under` are not the only answers: the command **exits 1 with empty standard output** when
368
- there is no transcript, or no request in it. **Neither is `under`.** Reading "could not measure" as
369
- "still cheap" is the same mistake as reading an unreadable comment listing as "no findings" — report that
370
- the check could not answer, and take `over`'s branch at that child.
331
+ there is no transcript, or no request in it. **Neither is `under`** — report that the check could not
332
+ answer, and take `over`'s branch at that child.
371
333
 
372
334
  ### When to ask, and what to do
373
335
 
374
- **Ask once per child, immediately after its merge and `pnpm josh ms`** — never mid-child. That is the
375
- only moment where **this** child's work is all written down: the PR is merged, the working tree is clean
376
- on the default branch, and the epic's state on GitHub is complete.
336
+ **Ask once per child, immediately after its merge and `pnpm josh ms`** — never mid-child.
377
337
 
378
338
  ### A parent without a completion callback hands off at its first dispatch
379
339
 
@@ -383,16 +343,13 @@ below the cut cap, `wait` at it** (joshuafolkken/kit#2653): dispatch the wave, r
383
343
  ### The lane child reuses this measurement mid-implementation
384
344
 
385
345
  **The same `pnpm josh cost --over` measurement bounds a lane child's context _during_ implementation, not
386
- only the parent's between children** — the pre-gate cut fires only once implementation is done, so it
387
- never caps the thinking a child accumulates while implementing. The child measures its own per-request
346
+ only the parent's between children**. The child measures its own per-request
388
347
  context with **this command** at the shared `CONTEXT_CUT_THRESHOLD`, 135_000, and cuts with
389
348
  `pnpm josh run:cut --impl <N> --handoff <path>`. **The measurement and threshold are single-sourced**:
390
349
  `cost_verdict.per_request_cost` is what both seams compare, and `cost --cut` selects the same constant
391
350
  for both. The boundary and the resume: `pre-gate-cut.md` → "The implementation-phase cut".
392
351
 
393
- **A merge is not by itself a safe seam, because another lane may still be running.** The reference to
394
- where each unit writes lives in the lane, so a session that never dispatched the child can poll it and
395
- the pool is handed over instead of drained. **Read the lanes rather than judging them:**
352
+ **A merge is not by itself a safe seam, because another lane may still be running.** **Read the lanes rather than judging them:**
396
353
 
397
354
  ```bash
398
355
  pnpm josh lane:list # `none`, or one line per lane with its state and its recorded output path
@@ -409,9 +366,7 @@ unit_output=$(pnpm josh lane:output <N>) &&
409
366
 
410
367
  **`--process` carries what `pgrep` found, and with a dispatched child that is the whole answer.** Run
411
368
  `pgrep -laf "(fullrun|run-ship-cli\.ts .*) #<N>$"` first and pass `alive` where it found the child and `none` where it did not
412
- — **never `alive` because the child was dispatched.** The child's command line holds no path, so a
413
- `pgrep` on the lane's directory never matches a live child; the trace is the deciding input, because a
414
- log that has stopped moving is a session thinking rather than one that died.
369
+ — **never `alive` because the child was dispatched**, and never from a `pgrep` on the lane's directory.
415
370
 
416
371
  **The `&&` is load-bearing.** A lane that records nothing prints `none` and exits non-zero; substituted
417
372
  straight into `--output`, that `none` is a relative path and `run:liveness` answers `undetermined` for
@@ -421,8 +376,7 @@ ever.
421
376
  - **`over`** — **the run hands its lanes over and takes the cut at once.** Open no new lane and take no
422
377
  new child. Confirm every lane still in flight records an output path — one missing is filled in with
423
378
  `pnpm josh lane:output <N> <path>` — then take one of the next two bullets in the same turn. **There
424
- is no waiting here at all**: nothing has to finish, because nothing is being abandoned. The child is
425
- an operating-system process of its own (`pnpm josh lane:dispatch`), so the lanes keep running.
379
+ is no waiting here at all**: the lanes keep running as processes of their own.
426
380
  - **A lane nobody could poll** — `unreadable`, or `open` with no recorded path and none that can be
427
381
  supplied — **and the cut does not happen.** Name that lane in the progress comment and go back to
428
382
  step 1 of the loop; the reading is asked again at the next merge.
@@ -435,20 +389,17 @@ ever.
435
389
  `backlogrun-steps.md` → "The session cut is inside the invocation" is the single source of the
436
390
  carry; this reading is only where the cut is *taken*.
437
391
  - **`capped`** — the invocation has taken its `MAX_CUTS` cuts (joshuafolkken/kit#2346), so `--cut`
438
- refused and left the record un-handed-off. **Do not `run:wake` or hand off**: a wake over it recovers
439
- as a crashed owner into a fresh cold session — the churn the cap prevents. Carry **this** session on
392
+ refused and left the record un-handed-off. **Do not `run:wake` or hand off.** Carry **this** session on
440
393
  uncut and re-ask at the next merge.
441
394
 
442
- **The hand-over is what makes the cut reachable, and the drain it replaced cost the pool.** `epic:next
443
- --lanes` keeps the seats full, so a cut gated on an idle pool that merely happened would read `over` at
444
- every merge and cut at none. Recording the path removes that cost.
395
+ Rationale for handing lanes over: `docs/maintainers/backlogrun-progress-rationale.md` → "Why lanes are
396
+ handed over rather than drained".
445
397
 
446
398
  **A lane is handed over only where the next session can actually poll it.** A lane whose state is
447
399
  `open` **and** whose recorded path is not `-` is handed over. A **`stranded`** lane has no work tree and
448
400
  so no running child — `pnpm josh lane:prune` closes it. An **`unreadable`** one cannot be told apart
449
401
  from a running child, and an `open` lane recording **no path** is the same: **the cut does not happen**,
450
- the lane is named in the epic progress comment, and the run goes back to step 1. **Never assume idle** —
451
- a wrong cut abandons a child, a missed cut only costs tokens.
402
+ the lane is named in the epic progress comment, and the run goes back to step 1. **Never assume idle.**
452
403
 
453
404
  **The hand-off report belongs to the stop, not to the reading.** For the one reading that does not stop —
454
405
  an `unreadable` lane, or an `open` one recording no path — the run goes back to step 1 and writes no
@@ -469,8 +420,7 @@ a fresh session did not start those processes, so it reports `none` unless it ha
469
420
  lane needs when its child has to be finished by hand.
470
421
 
471
422
  **Restart the progress watcher by name** — `pnpm josh run:progress --wait --output <handed-over paths>`
472
- in the background, in the same turn as that first `lane:list`. The cut session's watcher exited with it,
473
- and the wired `run:watcher:guard` stops a resume that skips this (joshuafolkken/kit#2353).
423
+ in the background, in the same turn as that first `lane:list`. The wired `run:watcher:guard` stops a resume that skips this (joshuafolkken/kit#2353).
474
424
 
475
425
  ### A carried-over merge does not stand in front of the next lane
476
426
 
@@ -479,8 +429,7 @@ of its merge.** A lane handed over at the cut is still running its own `fullrun`
479
429
  `pnpm josh followup` and the CI wait included — in a process of its own (background-commands.md →
480
430
  "`pnpm josh followup` — foreground": foreground is *within the unit*, the background from the parent).
481
431
  **So the parent never runs a carried-over child's `followup` itself**: it polls the handed-over lane and
482
- opens new work beside it, rather than finishing carried-over merges one after another before a single
483
- new lane opens. **The reads and the dispatches go out together, in one turn** — `pnpm josh lane:list`,
432
+ opens new work beside it. **The reads and the dispatches go out together, in one turn** — `pnpm josh lane:list`,
484
433
  `pnpm josh run:liveness`, `epic:next --lanes`, and opening a lane for a child it offers take none of each
485
434
  other's results (the turn-batching rule this file states for a merge event, applied at the resume).
486
435
  **Independence is the offer command's answer**: a new child `blocked-by` a carried-over Issue is
@@ -492,16 +441,12 @@ survive the cut is re-dispatched, never adopted** — `pnpm josh lane:open <N>`
492
441
 
493
442
  **The single source of the run's counters is the carry record (`pnpm josh run:carry`), and the epic
494
443
  progress comment is generated from it** (joshuafolkken/kit#2024) — children run, Issues filed,
495
- **consecutive failures**, and the time the run started, held in one place rather than counted twice.
496
- `pnpm josh run:merge` writes the merge into the record and mirrors the counters onto the epic comment at
497
- every child's merge, so the human-readable comment and the guard the run reads can never disagree. **The
498
- consecutive-failure count is the one that matters** — it is what the stopped-unit section leans on to
499
- notice the environment is at fault, and a merge resets it; lost, a run keeps feeding children into a
500
- broken environment and never reaches three. **The record survives a session cut and a compaction
501
- alike** — that is what it is for — so the counters a run's own guards rest on are never taken by the
502
- moment the context is dropped. **This is not what "Nothing is carried in the conversation" denies**:
503
- that is about the state a *next session* needs, all of it on GitHub, while the record is about *this*
504
- run's own guards.
444
+ **consecutive failures**, and the time the run started. `pnpm josh run:merge` writes the merge into
445
+ the record and mirrors the counters onto the epic comment at every child's merge. **The
446
+ consecutive-failure count is the one that matters** — the stopped-unit section leans on it to notice
447
+ the environment is at fault, and a merge resets it. **The record survives a session cut and a
448
+ compaction alike.** Rationale: `docs/maintainers/backlogrun-progress-rationale.md` → "Why state lives
449
+ in records, not the conversation".
505
450
 
506
451
  ### What carries over, and where it lives
507
452
 
@@ -514,11 +459,6 @@ run's own guards.
514
459
  | What each remaining child is | the child Issue body |
515
460
  | What already merged | the epic's task list, and the closed children |
516
461
 
517
- That is the same state a resumed run has always used, which is why the hand-off needs no new mechanism.
518
- **A planned hand-off is strictly more certain than an interruption**: an interruption can land mid-child
519
- with a dirty tree and a stale `in-progress` label, and this cannot, because it is only taken when a child
520
- has just closed.
521
-
522
462
  **A resumed session is a new session**, so it asks `pnpm josh latest:scope` once before its first child.
523
463
 
524
464
  ## Waiting, and never waiting forever
@@ -526,8 +466,8 @@ has just closed.
526
466
  | Setting | Value | Why |
527
467
  | --- | --- | --- |
528
468
  | Polling interval | 60 s | **A floor between two asks, never a clock the parent sets.** It bounds a re-ask made while the parent is *already awake*; what wakes it is "The parent keeps no clock of its own" below. |
529
- | `backlogrun` idle-watch poll | 5 min | Not the interval above, and a floor in the same sense. What a watch waits on happens on human timescales, and every ask bills the parent session's whole history. |
530
- | Silent delegated unit | 30 min | Not the child's duration — the time its output has gone **unchanged**. A working unit rewrites its transcript continuously. Past it, run the traces above and book a stopped unit as a failure. |
469
+ | `backlogrun` idle-watch poll | 5 min | Not the interval above, and a floor in the same sense. |
470
+ | Silent delegated unit | 30 min | Not the child's duration — the time its output has gone **unchanged**. Past it, run the traces above and book a stopped unit as a failure. |
531
471
  | Stale `in-progress` | 90 min | Longer than any single child has taken; past it, the other session is gone. |
532
472
  | Publish wait | 10 min | `josh propagate`'s own budget. A failed publish never appears. |
533
473
  | Whole run | 8 h | An unattended run that has not finished overnight needs a person, not more waiting. |
@@ -543,8 +483,9 @@ completion and the backlog inside the detached process, without waking an AI par
543
483
  instructions below apply only to a returned judgment branch. `run:progress --wait` stays the heartbeat
544
484
  source and `run:report` the final report.
545
485
 
546
- **The numbers above are floors between asks, not a timer the parent sets** — one tick per turn, at the
547
- largest context, is the cost `run:progress` removed.
486
+ **The numbers above are floors between asks, not a timer the parent sets.** Rationale:
487
+ `docs/maintainers/backlogrun-progress-rationale.md` → "Why the parent waits on classification, not a
488
+ clock".
548
489
 
549
490
  The driver checks lane completion every five seconds and makes a new backlog offer no sooner than one
550
491
  minute after the previous offer, except after collecting a child — waits that cost no AI turns. The
@@ -575,10 +516,6 @@ Waiting is decided by `epic:next`'s classification, never by reading labels:
575
516
  | Nothing runnable, nothing resolves on its own, children remain | **Stop and report the parked children** |
576
517
  | No open child | Post the epic summary and finish |
577
518
 
578
- The distinction is not academic. When kit's child has closed and app-kit's child is waiting for the
579
- release to publish, there is no runnable child, nothing carries `in-progress` and nothing carries
580
- `needs-decision` — a label-based reading calls that "done" and stops, in the one moment it must wait.
581
-
582
519
  ## Who sends the summary, and who propagates
583
520
 
584
521
  With several sessions on one epic, **exactly one does the end-of-epic work: the session standing in the
@@ -591,8 +528,7 @@ Per-child completion notifications are unchanged: `pnpm josh followup` sends one
591
528
  compose that summary by hand — `pnpm josh run:report` generates it from this invocation's events**
592
529
  (joshuafolkken/kit#2249, scoped in #2393): it renders what merged, what parked and why, and what was cut,
593
530
  reusing `format_event`, closing with the release tail below. Its output is the Telegram body too — pass it to
594
- `pnpm josh notify --body-file` so the summary a session shows and the message off-screen are one text
595
- from one generator.
531
+ `pnpm josh notify --body-file`.
596
532
 
597
533
  **That same session asks `pnpm josh release:scope` once, after the last child has merged** — in the
598
534
  primary checkout, after the last lane is closed, and never once per child. `followup-reference.md` →