@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
@@ -1,12 +1,11 @@
1
1
  # `backlogrun` — the detailed procedure behind the manifest
2
2
 
3
3
  **This is the detailed procedure behind `backlogrun.md`'s manifest, relocated here so the entry read
4
- carries the manifest, not the prose** (joshuafolkken/kit#2190). `backlogrun.md` is the resident
5
- manifest; it routes here for the fine print of each step, read on demand rather than at the entry.
6
- This file is **reference-only** and is read at its point of use — it is classified point-of-use in
7
- `entry-read-set.ts`, exactly as the four `backlogrun-*.md` phase documents are, so a `backlogrun`
8
- entry never opens it whole up front. Each section below is the single source of the rule it carries;
9
- `backlogrun.md` points here rather than restating any of it.
4
+ carries the manifest, not the prose** (joshuafolkken/kit#2190). This file is **reference-only** and is
5
+ read at its point of use — it is classified point-of-use in `entry-read-set.ts`, exactly as the four
6
+ `backlogrun-*.md` phase documents are, so a `backlogrun` entry never opens it whole up front. Each
7
+ section below is the single source of the rule it carries; `backlogrun.md` points here rather than
8
+ restating any of it.
10
9
 
11
10
  ## What one invocation approves
12
11
 
@@ -19,47 +18,36 @@ without anybody having labelled it.
19
18
 
20
19
  **Two repairs that would bound this the other way are prohibited** (joshuafolkken/kit#1675): dropping
21
20
  a run's own filings from the pool, and requiring a person's `auto-ok` on a child as well. What
22
- replaces them is "The brake that replaces the promise" below.
21
+ bounds it instead is "The brake that replaces the promise" below.
23
22
 
24
- That declaration is the authorization: **`backlogrun` declares "everything opted in", and a named
25
- epic declares "the children of that epic".** The declaration *is* the statement of what may be
26
- executed unattended, and that is the one thing a run must not leave ambiguous — which is why what runs
27
- is stated by the keyword and its arguments, never inferred from the shape of a request.
23
+ **`backlogrun` declares "everything opted in", and a named epic declares "the children of that
24
+ epic"** — what runs is stated by the keyword and its arguments, never inferred from the shape of a
25
+ request.
28
26
 
29
27
  **Which issues may be opted in stays a person's decision.** `auto-ok` is applied only by a person —
30
- this file is that rule's single source, now that `backlogrun` owns the opted-in pool
31
- (joshuafolkken/kit#1965) — and a run that labelled its own inputs would be widening its own
32
- authorization. The split is: **which ones carry the label, a person; in what order and how many at
33
- once, the run.** **What a run does move is the pool's contents**: an issue it files and bundles into an
34
- already opted-in epic is offered from the next ask onwards, and the brake below bounds that quantity
35
- rather than the membership.
36
-
37
- **The end-of-run retrospective is the one carve-out, revised here rather than worked around**
38
- (joshuafolkken/kit#2328). When a run drains its backlog, the retrospective files the improvements worth
39
- carrying into the next run and applies `auto-ok` to them, so the next run can pick them up without
40
- waiting for a person — the single path on which a run labels its own input. This is not the
41
- self-widening the rule guards against, because the brakes in "The brake that replaces the promise"
42
- below bound it with no exception: a retrospective's filings are counted against the ten-per-invocation
43
- ceiling, the WIP cap of 30, `--max`, the 150,000-token session budget and the 8-hour whole-run bound
44
- exactly as any other filing is. And a retrospective that judges nothing worth carrying files nothing,
45
- so the "file → drain → file again" loop converges the moment improvements run out. Every other filing
46
- route is unchanged: `auto-ok` stays a person's to apply.
28
+ this file is that rule's single source. **Which ones carry the label, a person; in what order and how
29
+ many at once, the run.** An issue the run files and bundles into an already opted-in epic is offered
30
+ from the next ask onwards, and the brake below bounds that quantity rather than the membership.
31
+
32
+ **The end-of-run retrospective is the one carve-out** (joshuafolkken/kit#2328): when a run drains its
33
+ backlog, the retrospective files the improvements worth carrying into the next run and applies
34
+ `auto-ok` to them. Its filings are counted against every brake in "The brake that replaces the
35
+ promise" below with no exception, and a retrospective that judges nothing worth carrying files
36
+ nothing. Every other filing route is unchanged: `auto-ok` stays a person's to apply.
47
37
 
48
38
  A Tier C action inside a child still stops that child, exactly as it does for any batch child.
49
39
 
50
40
  **A named issue is approved by the keyword and its number, not by `auto-ok`** (joshuafolkken/kit#1984).
51
- `backlogrun #N1 #N2 …` runs those issues whether or not they carry the label — typing them is the
52
- authorization, exactly as `queue` was explicit authorization to merge each of its issues. The pool
53
- that follows is unchanged: still `auto-ok`, still a person's to opt in. "Named issues run first, in
54
- order" below is the procedure.
41
+ `backlogrun #N1 #N2 …` runs those issues whether or not they carry the label. The pool that follows is
42
+ unchanged: still `auto-ok`, still a person's to opt in. "Named issues run first, in order" below is the
43
+ procedure.
44
+
45
+ Rationale: `docs/maintainers/backlogrun-steps-rationale.md` → "Why the authorization boundary is shaped this way"
55
46
 
56
47
  ### The brake that replaces the promise
57
48
 
58
- **The promise was an authorization boundary rather than a convenience**, so withdrawing it without
59
- putting something in its place would leave "file → run → file again" with no ceiling at all — the
60
- self-widening `split-assessment.md` refuses when it forbids a `fullrun` promoting itself to a batch.
61
- **What bounds it now is the invocation's own budget, and every part of that is counted in the carry
62
- record rather than in the conversation** ("The session cut is inside the invocation" below):
49
+ **Every bound is counted in the carry record rather than in the conversation** ("The session cut is
50
+ inside the invocation" below):
63
51
 
64
52
  | The bound | What it limits | Where it is counted |
65
53
  | --- | --- | --- |
@@ -68,22 +56,17 @@ record rather than in the conversation** ("The session cut is inside the invocat
68
56
  | **Ten filings per invocation** | how much one invocation may add to the pool at all, on **every** filing route (`SKILL.md` → §2d) | `run:carry --filed` |
69
57
  | The WIP cap of 30 open issues | how large the pool may become, across invocations | `prompts/collaboration-workflow/wip-cap.md` |
70
58
 
71
- **The last two are what actually replace the promise, and the first of them is why the loop
72
- terminates**: one invocation may add at most ten issues to the pool and merge at most `--max` of
73
- them, after which the run ends and the next one waits for a person to type the keyword. The old
74
- promise bounded the *kind* of work that could run; these bound the *amount*, which is the only thing
75
- left to bound once a run's own filings are admitted deliberately.
59
+ One invocation may add at most ten issues to the pool and merge at most `--max` of them, after which
60
+ the run ends and the next one waits for a person to type the keyword.
76
61
 
77
62
  **Two filing routes are exempt from the depth test (`SKILL.md` → §2i), and the ten-filings ceiling
78
63
  still covers them:**
79
64
 
80
- - **`route:tier-a` and `route:interrupt` stay exempt** — a filing the run cannot proceed without is
81
- citing its own blockage by construction. **Their number is still capped**: §2d's ten-filings
82
- ceiling is stated "at every entry point", and `run:carry --filed` counts it across session cuts.
83
- - **A review branch-2 filing stays exempt** because it has already cleared a bar the discretionary
84
- route has not — a confirmed defect reaching a runtime path, with a written failure scenario — and
85
- gating it on a citation as well would drop the one kind of finding both documents agree is never
86
- dropped.
65
+ - **`route:tier-a` and `route:interrupt` stay exempt.** **Their number is still capped**: §2d's
66
+ ten-filings ceiling is stated "at every entry point", and `run:carry --filed` counts it across
67
+ session cuts.
68
+ - **A review branch-2 filing stays exempt** — a confirmed defect reaching a runtime path, with a
69
+ written failure scenario.
87
70
 
88
71
  **This section is the single source of how the `epic:bundle` obligation and this authorization
89
72
  boundary meet.** `SKILL.md` → §2i points here rather than restating it.
@@ -93,13 +76,11 @@ boundary meet.** `SKILL.md` → §2i points here rather than restating it.
93
76
  **`backlogrun #N1 #N2 …` runs the named issues before it touches the pool** (joshuafolkken/kit#1984,
94
77
  folding in the old `queue` keyword). They run in the order they were typed, **one at a time — no
95
78
  lanes**, because the order is the point of naming them. Only once the named list is exhausted does the
96
- loop below drain the opted-in backlog. A bare `backlogrun` names none and starts straight on the loop,
97
- exactly as it always did.
79
+ loop below drain the opted-in backlog. A bare `backlogrun` names none and starts straight on the loop.
98
80
 
99
81
  **`pnpm josh backlog:plan #N1 #N2 …` reports the whole plan** — the named prefix first, in order, then
100
- the pool — so the person sees the shape of the run before anything starts. `scripts/backlog/backlog-named.ts`
101
- is the single source of that order and of the failure branch below; the plan renders it rather than
102
- restating it.
82
+ the pool. `scripts/backlog/backlog-named.ts` is the single source of that order and of the failure
83
+ branch below; the plan renders it rather than restating it.
103
84
 
104
85
  **Each named issue is a full `fullrun` in a delegated unit**, exactly as a backlog child is
105
86
  (`backlogrun-child.md` → "Each child runs in a delegated unit"), and the once-per-session steps — `josh latest`
@@ -108,10 +89,9 @@ on `required`, the progress watcher — run once for the whole invocation, never
108
89
  brings the previous merge into the checkout the next one implements in. The parent reads each issue's
109
90
  state back from GitHub — `pnpm josh issue:state <N>` — never the unit's summary.
110
91
 
111
- **A named issue that cannot finish parks, and the rest of the named list is skipped** — starting the
112
- issues after it would break the order they were named in. **The run does not stop there**: it goes on
113
- to drain the opted-in backlog, and the completion report lists the named issues it did not start.
114
- `backlog_named.after_failure` is the single source.
92
+ **A named issue that cannot finish parks, and the rest of the named list is skipped.** **The run does
93
+ not stop there**: it goes on to drain the opted-in backlog, and the completion report lists the named
94
+ issues it did not start. `backlog_named.after_failure` is the single source.
115
95
 
116
96
  - A named issue read back `CLOSED` — merged, or already `already-done` — is finished; continue with the
117
97
  next.
@@ -120,13 +100,13 @@ to drain the opted-in backlog, and the completion report lists the named issues
120
100
  - Everything else about running one — the verification gate, the hand-off, the guards — is
121
101
  this file's and is not restated here.
122
102
 
123
- **`--only` runs the named list and stops there, draining no pool** (joshuafolkken/kit#1984) — the old
124
- `queue`'s scope kept under one keyword. `backlogrun #N1 #N2 … --only` runs the named issues in order and
125
- ends, and the completion report says in one line that `--only` held it back from the opted-in backlog.
126
- A named issue that fails **under `--only` ends the run** rather than falling through to a pool, and the
127
- issues after it are skipped as ever. **`--only` with no named issues has nothing to run and is refused
128
- before anything starts.** `backlog_named.startup` is the single source of both, and
129
- `pnpm josh backlog:plan #N1 #N2 … --only` renders the named-only plan the run will take.
103
+ **`--only` runs the named list and stops there, draining no pool** (joshuafolkken/kit#1984).
104
+ `backlogrun #N1 #N2 … --only` runs the named issues in order and ends, and the completion report says
105
+ in one line that `--only` held it back from the opted-in backlog. A named issue that fails **under
106
+ `--only` ends the run** rather than falling through to a pool, and the issues after it are skipped as
107
+ ever. **`--only` with no named issues has nothing to run and is refused before anything starts.**
108
+ `backlog_named.startup` is the single source of both, and `pnpm josh backlog:plan #N1 #N2 … --only`
109
+ renders the named-only plan the run will take.
130
110
 
131
111
  ## The session cut is inside the invocation
132
112
 
@@ -138,35 +118,26 @@ record's `done` field rather than shrinking the string — a resumed session rea
138
118
  invocation string like every other token**, so a resumed session runs the remaining named issues and
139
119
  **stops** rather than draining the pool the person excluded.
140
120
 
141
- **Typing `backlogrun` once authorizes the declared budget, and a session cut is an execution detail of
142
- spending it** (joshuafolkken/kit#1714). `backlogrun-progress.md` → "The hand-off" stops the _session_ at the seam;
143
- under `backlogrun` that seam does not stop the _run_. The next session picks the same invocation up
144
- and carries on with the budget already partly spent.
121
+ **Typing `backlogrun` once authorizes the declared budget, and a session cut does not end the run.**
122
+ `backlogrun-progress.md` → "The hand-off" stops the _session_ at the seam; under `backlogrun` that
123
+ seam does not stop the _run_. The next session picks the same invocation up and carries on with the
124
+ budget already partly spent.
145
125
 
146
126
  **A resumed session's carried-over lanes are polled beside new work, never waited out** —
147
127
  `backlogrun-progress.md` → "A carried-over merge does not stand in front of the next lane" is the
148
128
  single source.
149
129
 
150
- **This is not an exception to the explicit-invocation rule above.** What §0 forbids is **inferring** a
151
- workflow from the shape of a request; it never required the keystroke to land in every session's
152
- transcript. A resumed session is a machine continuing an authorization a person gave, exactly as a
153
- delegated child runs `fullrun` in a unit where nobody typed the keyword.
154
-
155
- **The authorization boundary is carried by the budget, not by the keystroke.** So the budget has to
156
- survive the cut, and the conversation is the one place it cannot live:
130
+ **The authorization boundary is carried by the budget, not by the keystroke**, so the budget is
131
+ carried in a record rather than in the conversation:
157
132
 
158
133
  ```bash
159
134
  pnpm josh run:carry --begin "backlogrun --max 5 --idle 30" --owner "$PPID" # alias: josh rc
160
135
  pnpm josh run:carry --json # read it back in a resumed session
161
136
  ```
162
137
 
163
- **`--owner "$PPID"` is not decoration.** It names the parent loop's own long-lived process, which is
164
- what lets the command answer `busy` instead of letting a second parent count into a budget that is
165
- still being spent. Left off, the record declares no owner and every standing record reads as not
166
- provably live — refused rather than resumed, so nothing is lost silently, but the useful half of the
167
- answer is gone. When the sandbox cannot read a process-start token, a live PID is conservatively held
168
- as `busy`: stopping on a reused PID costs a decision, while replacing a genuinely live owner creates
169
- two parents on one budget.
138
+ **Always pass `--owner "$PPID"`** — the parent loop's own long-lived process. Left off, `busy` cannot
139
+ be answered and every standing record is refused rather than resumed. A live PID whose start token
140
+ the sandbox cannot read is held as `busy`.
170
141
 
171
142
  **Ask it before the plan, in the same turn as the first `git switch main && git pull`.** The contract
172
143
  is `docs/josh-commands.md` → "`josh run:carry`"; what this loop does with each answer is here:
@@ -175,66 +146,55 @@ is `docs/josh-commands.md` → "`josh run:carry`"; what this loop does with each
175
146
  | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
176
147
  | `began` | Nothing was carried. This is the invocation's first session: report the plan and run the decision pass as written below |
177
148
  | `resumed` | **This session is continuing a run that was cut** — a `--cut` handed the record off, or a `--resume` adopted it. Read the record with `--json` and take the budget figures from it, never from this session's own zero. Report the plan again — the pool has moved — and skip nothing else |
178
- | `busy` | The record's **owner process is still running and no cut handed it off**: another parent is spending this budget right now. **Stop; do not open a lane.** Counting into it would put two parents on one record. (A record a `--cut` _did_ hand off answers `resumed` here even with its owner still alive — joshuafolkken/kit#1935.) Nothing here is yours to end — either that run finishes and ends its own record, or a person decides it is over |
179
- | `standing` | A record is here that **no cut or supervisor handed off** — an unwatched crash, and the same command retyped over it. **Stop; do not open a lane**, and do not guess: the choice is the person's. Report the two commands the answer names — `pnpm josh run:carry --resume "<invocation>" --owner "$PPID"` to carry that budget on, or `pnpm josh run:carry --end` and begin again to discard it. The `--owner` belongs to the resume as much as to the begin: adopt without it and the record declares no owner, so `busy` degrades to `standing` for every parent after |
180
- | `mismatch` | A record is here for a **different** invocation — a run that never reached `--end`. **Stop; do not open a lane.** Resuming into it would spend that run's `--max` and its hours. End it deliberately, with `pnpm josh run:carry --end`, once you know that run is over |
149
+ | `busy` | The record's **owner process is still running and no cut handed it off**: another parent is spending this budget right now. **Stop; do not open a lane.** (A record a `--cut` _did_ hand off answers `resumed` here even with its owner still alive.) Nothing here is yours to end — either that run finishes and ends its own record, or a person decides it is over |
150
+ | `over` | This session is over the context-cut threshold, so it claims nothing (joshuafolkken/kit#2760). **End the conversation**; retype the invocation in a fresh session |
151
+ | `standing` | A record is here that **no cut or supervisor handed off** — an unwatched crash, and the same command retyped over it. **Stop; do not open a lane**, and do not guess: the choice is the person's. Report the two commands the answer names — `pnpm josh run:carry --resume "<invocation>" --owner "$PPID"` to carry that budget on, or `pnpm josh run:carry --end` and begin again to discard it. Pass `--owner` to the resume as to the begin |
152
+ | `mismatch` | A record is here for a **different** invocation — a run that never reached `--end`. **Stop; do not open a lane**, and never resume into it. End it deliberately, with `pnpm josh run:carry --end`, once you know that run is over |
181
153
  | `expired` | The 8-hour whole-run bound is spent. **Where a `--cut` handed the record off it is this run's own bound**, so this is the verdict on standard output and the run ends: report it and stop, and clear the record with `pnpm josh run:carry --end` once it is genuinely over. Where nothing handed it off it is printed on standard error ahead of a `began` instead — a person typing the keyword again over a spent record is starting a new run, and the record is replaced. A `--resume` over a spent record answers `expired` and adopts nothing |
182
- | `unreadable` | Report what it printed and **stop before opening a lane**. A budget that cannot be carried is a run that restarts it at the next cut, which is the whole defect |
154
+ | `unreadable` | Report what it printed and **stop before opening a lane** |
183
155
  | `unknown` | The same — the repository's git directory could not be read, so nothing was established |
184
156
 
185
- **The record is the parent loop's, and a lane never touches it.** The parent is what reads GitHub to
186
- verify a child merged (`backlogrun-child.md` → "Each child runs in a delegated unit"), so it is also what
187
- counts that merge — one sequential loop writing one record, with no two lanes writing it at once. A
188
- delegated unit is briefed with its child and nothing about the invocation's budget. **`--begin` claims
189
- the record exclusively and names the owning process, so a second parent is answered `busy`** rather
190
- than counting into a budget somebody else is spending.
157
+ **The record is the parent loop's, and a lane never touches it.** A delegated unit is briefed with its
158
+ child and nothing about the invocation's budget. **`--begin` claims the record exclusively and names
159
+ the owning process, so a second parent is answered `busy`.**
191
160
 
192
161
  **Count into the record rather than into your head** — `--merged 1 --owner "$PPID"` at every child's
193
162
  merge, `--filed 1 --owner "$PPID"` at every Issue this run files, and `--cut --owner "$PPID"`
194
- immediately before the cut. Every counter is an increment and the command owns the sum, because a run
195
- sending a total would be sending arithmetic done in its head. **`--owner "$PPID"` is not optional on a
196
- count**: a count that does not name the record's owning process is refused, so a session whose record a
197
- successor took over cannot advance a budget that is no longer its own.
163
+ immediately before the cut. Every counter is an increment. **`--owner "$PPID"` is not optional on a
164
+ count**: a count that does not name the record's owning process is refused.
198
165
 
199
166
  **`--cut` hands the record off, and so does `run:wake` for a dead owner** (joshuafolkken/kit#2437): the
200
167
  next `--begin` naming the same invocation answers `resumed` — **even while the cutting process is still
201
- running**, so a cut is handed to exactly one successor rather than stalling every resume with `busy`.
168
+ running**.
202
169
 
203
170
  **So `--cut` is the session's last write to the record**: count everything the session has, then cut.
204
- A `--merged` or `--filed` issued after the `--cut` is **refused**, so the hand-off is protected rather
205
- than silently spent.
171
+ A `--merged` or `--filed` issued after the `--cut` is **refused**.
206
172
 
207
173
  **`backlog:budget` is then fed from the record, never from a count kept in the conversation:**
208
- `--started` takes the record's `started_at` and `--merged` its `merged`. That one substitution is what
209
- makes `--max` and the 8-hour whole-run bound count **across** cuts, as one invocation's worth. **The
210
- 10-filings-per-run ceiling is counted the same way**, from `filed`. **`--idle` is the one budget that
211
- is not carried**, and a cut can only reach a watch that opened while this run's own children were
212
- still merging — the one shape where the two overlap. A resumed session then states its resume moment
213
- as `--active` and the watch begins again at its full budget, bounded by the 8 hours as everything
214
- else is: "The cost check is not asked during a watch" below.
174
+ `--started` takes the record's `started_at` and `--merged` its `merged`, so `--max` and the 8-hour
175
+ whole-run bound count **across** cuts, as one invocation's worth. **The 10-filings-per-run ceiling is
176
+ counted the same way**, from `filed`. **`--idle` is the one budget that is not carried**, and a cut can
177
+ only reach a watch that opened while this run's own children were still merging. A resumed session
178
+ then states its resume moment as `--active` and the watch begins again at its full budget, bounded by
179
+ the 8 hours as everything else is: "The cost check is not asked during a watch" below.
215
180
 
216
- **How many cuts the run crossed is named in the completion report**, read from the record's `cuts`. A
217
- run reporting only what it merged would hide that it spanned four sessions.
181
+ **How many cuts the run crossed is named in the completion report**, read from the record's `cuts`.
218
182
 
219
183
  **End the record when the run ends** — `pnpm josh run:carry --end`, **after the final report and never
220
- batched with it**: `run:report` scopes by the record `--end` removes (joshuafolkken/kit#2393). The next
221
- `backlogrun` then begins its own budget rather than resuming a spent one. **Where the run ends by _stopping_ rather than finishing** — `backlog:budget` or `epic:next`
222
- answered `stop` (a parked backlog, an unreadable listing, the maximum, the whole-run bound), or the
223
- consecutive-failure
224
- guard tripped — **end it with `pnpm josh run:carry --end --stopped "<one-line reason>"` instead**. That
225
- pushes one ⏸️ confirmation as it clears the record, so a person learns the run halted even when the
226
- session was cut and this parent headless (joshuafolkken/kit#2136); the reason is the stop verdict's, in
227
- the session language. A clean completion takes the bare `--end` and stays silent, having its own report;
228
- a parked _child_ is pushed by the child that parked it (`backlogrun-park.md`). Because `--end` removes
229
- the record, a re-run's second `--end` finds nothing and never re-sends that confirmation —
230
- `backlogrun-progress.md` →
184
+ batched with it**: `run:report` scopes by the record `--end` removes. The next `backlogrun` then begins
185
+ its own budget rather than resuming a spent one. **Where the run ends by _stopping_ rather than
186
+ finishing** — `backlog:budget` or `epic:next` answered `stop` (a parked backlog, an unreadable listing,
187
+ the maximum, the whole-run bound), or the consecutive-failure guard tripped — **end it with
188
+ `pnpm josh run:carry --end --stopped "<one-line reason>"` instead**. That pushes one ⏸️ confirmation as
189
+ it clears the record; the reason is the stop verdict's, in the session language. A clean completion
190
+ takes the bare `--end` and stays silent, having its own report; a parked _child_ is pushed by the child
191
+ that parked it (`backlogrun-park.md`). Because `--end` removes the record, a re-run's second `--end`
192
+ finds nothing and never re-sends that confirmation — `backlogrun-progress.md` →
231
193
  "Progress while the run is quiet" is the single source of the pull-versus-push split.
232
194
 
233
195
  **The record widens nothing.** It carries a budget and nothing else: `auto-ok` is still applied only
234
- by a person, so a cut adds no rule about which issues may be offered. **The pool itself may have
235
- grown across the cut** — an issue the first session filed, and `epic:bundle` placed under an already
236
- opted-in epic, is offered to the resumed one — and that is "What one invocation approves" rather than
237
- anything the record did. The ceiling on it is counted across cuts too, from the record's `filed`.
196
+ by a person. A pool grown across the cut by this run's own bundled filings is "What one invocation
197
+ approves", its ceiling counted from the record's `filed`.
238
198
 
239
199
  **Nothing waits for a person any more — `pnpm josh run:wake` supervises the driver**. After a cut or
240
200
  dead-owner recovery, it adopts the carry record and runs `backlog:drive` itself. The driver dispatches,
@@ -294,15 +254,15 @@ record. Telegram notifications retain their existing generation points (→ "It
294
254
  **The completion report names how many sessions were woken beside the record's `cuts`** — one wake per
295
255
  cut is the invariant, and what counts is a carry record actually claimed, never a process started.
296
256
 
297
- **The reading is scoped to `backlogrun`.** A `fullrun` cut still waits for a person's keystroke,
298
- because it declares no budget of the kind this section leans on — it ends at one issue and has
299
- nothing to carry.
257
+ **The reading is scoped to `backlogrun`.** A `fullrun` cut still waits for a person's keystroke.
258
+
259
+ Rationale: `docs/maintainers/backlogrun-steps-rationale.md` → "Why a session cut does not end the run"
300
260
 
301
261
  ## The plan, before the first child starts
302
262
 
303
263
  **A `backlogrun` reports its plan before it starts anything.** Nothing is dispatched, no lane is
304
264
  opened and no issue is picked up until the plan has been reported and the decision pass below has
305
- run (joshuafolkken/kit#1652).
265
+ run.
306
266
 
307
267
  **The plan is one command's output, not an assembly of several:**
308
268
 
@@ -311,12 +271,10 @@ pnpm josh backlog:plan # alias: josh blp
311
271
  ```
312
272
 
313
273
  **It renders the same classified pool `backlog:next` answers from**, so **the plan cannot promise an
314
- order the run does not take**. It is a separate command rather than a flag because `backlog:next`'s
315
- standard output is one bare token per line and a plan printed there would break the loop below. Its
316
- four sections are:
274
+ order the run does not take**. Its four sections are:
317
275
 
318
- - **Ready now** — the runnable children, grouped by repository. **The grouping is the parallelism**,
319
- not a presentational choice: a lane is per repository, so the bundles are how wide this run can go.
276
+ - **Ready now** — the runnable children, grouped by repository. **The grouping is the parallelism**:
277
+ a lane is per repository, so the bundles are how wide this run can go.
320
278
  - **Waiting** — every withheld child, each naming **what it is waiting on**: the blocking issue
321
279
  numbers, a run that already has it, or that it is ready but past the offer this ask could make.
322
280
  - **Waiting on a person** — the `needs-decision` children, which is the next subsection's input.
@@ -331,38 +289,41 @@ complete.
331
289
  ### Resolve what the plan can resolve, before starting
332
290
 
333
291
  **Every `needs-decision` issue the plan listed is settled in one pass at the start, rather than one
334
- stop at a time.** The point is to spend a person's attention once, up front, instead of interrupting
335
- an unattended run over and over.
292
+ stop at a time.**
336
293
 
337
294
  - **Decide everything decidable from the issue itself.** Read the issue's body **and its comments**
338
295
  (`SKILL.md` → §2g), and where the answer is already there, record it as an Issue comment and
339
- **remove the label** — `CLAUDE.md` → "Decision autonomy" already makes that Tier A, and a child
340
- whose decision is recorded while its label still stands is parked for no reason. **Read every one
296
+ **remove the label** — `CLAUDE.md` → "Decision autonomy" already makes that Tier A. **Read every one
341
297
  of them in one call** — `pnpm josh issue:read <N> <N> …`, the numbers the plan just listed under
342
- "Waiting on a person" — rather than a `gh api` pair per issue. This pass is where a parent reads the
343
- most issues in a row, and a parent's cost grows as n²/2 in its own request count
344
- (joshuafolkken/kit#1567), so a turn removed here is worth more than a turn removed inside a child.
298
+ "Waiting on a person" — rather than a `gh api` pair per issue.
345
299
  - **Never measure in order to decide.** A question needing a benchmark, a profile, or a run of the
346
- thing itself is not settled here: it stays labelled and the plan says so. This pass is a cheap
347
- read, not a research task, and turning it into one would put the whole backlog behind it.
300
+ thing itself is not settled here: it stays labelled and the plan says so.
348
301
  - **Label what you find.** An issue that turns out to need a person's judgement has `needs-decision`
349
302
  applied — the same label a parked child gets, applied the way `backlogrun-park.md` → "park and continue"
350
303
  applies it. **The next plan then classifies it by reading the label alone**, never by reading the
351
- body again, so the cost of this pass falls on every subsequent ask instead of repeating.
304
+ body again.
305
+ - **Order and isolate the pool in the same pass** (joshuafolkken/kit#2776), from the same
306
+ `issue:read`: an issue that must land first (it builds what another reuses, or both edit one code
307
+ path) becomes a native `blocked-by`; an issue the `wip-cap.md` → 「実行のしかた」 enumeration hits
308
+ gets `run:solo`, **every other one `run:lane`**. Comment the reason; the offer commands enforce all
309
+ three, answering `triage` while an issue has neither.
310
+ - **Report the order those labels produce — `pnpm josh backlog:plan --waves`** (joshuafolkken/kit#2778),
311
+ `backlog:next` played forward wave by wave. Report it with the plan, never an order derived by hand.
352
312
  - **Then start the loop.** Whatever is still labelled is reported as parked and left standing; the
353
313
  run does not wait on it.
354
314
 
355
- **`needs-decision` is the one workflow label a run may apply, and it is neither `auto-ok` nor
356
- `needs-human-review`.** Those two widen or withhold what may be *executed* and stay a person's alone
357
- (this file, `SKILL.md` → §2z). This one records only that a person's answer is needed, which is a
358
- finding rather than an authorization — which is why a run parks with it and a person clears it.
315
+ **`needs-decision`, `run:solo` and `run:lane` are the workflow labels a run may apply, and none is
316
+ `auto-ok` or `needs-human-review`.** Those two stay a person's alone (this file, `SKILL.md` → §2z); a
317
+ run parks with `needs-decision` and a person clears it.
318
+
319
+ Rationale: `docs/maintainers/backlogrun-steps-rationale.md` → "Why the plan and the decision pass come first"
359
320
 
360
321
  ## The loop
361
322
 
362
323
  **The detached `run:wake` supervisor runs `backlog:drive`** after the carry record is handed off.
363
324
  Its driver keeps the loop below running without a parent AI turn. A returned judgment branch includes
364
325
  the first-line verdict, affected issue and `resume:` flags in the AI prompt; `stop` reports and ends
365
- inside the driver. This uses the same command and decisions as the former parent-launched wait.
326
+ inside the driver.
366
327
 
367
328
  **The loop's head is one command — `pnpm josh backlog:offer`** (joshuafolkken/kit#2162). It runs
368
329
  `backlog:next`, maps its answer to the budget word the table below fixes, runs `backlog:budget`, and
@@ -384,20 +345,16 @@ this section's table.**
384
345
  **`backlog:next`'s output contract is `docs/josh-commands.md` → "`josh backlog:next`", and four parts
385
346
  of it decide how the mapping is written:**
386
347
 
387
- 1. **Standard output is one token per line, and everything else is standard error** — so `answers`
388
- holds something a loop can branch on.
348
+ 1. **Standard output is one token per line, and everything else is standard error.**
389
349
  2. **The tokens are bare issue numbers, scoped to the repository the command runs in.** A runnable
390
350
  child in *another* repository is reported on standard error with its repository and checkout, and
391
351
  is never a token; this repository having no candidate of its own is answered `wait`. **So
392
- `backlogrun` takes no `owner/repo#N` token.** A qualified token was implemented and withdrawn,
393
- because `--exclude` parses bare integers and feeding one back produces a usage error rather than
394
- an exclusion (joshuafolkken/kit#1630). Report the other repository's candidates in the run summary
395
- and leave them to a session running there (`backlogrun-lanes.md` → "Concurrency").
396
- 3. **The verdict words are `wait`, `stop`, `retry`, `error` and `none`** — `none` is `epic:next`'s
397
- `complete` under this command's spelling, and there is no `complete` here. **`retry` is the one
398
- with no `epic:next` counterpart**: it says GitHub did not answer, which is a statement about the
399
- connection and not about the graph (joshuafolkken/kit#1663).
400
- 4. **Exit code 0 covers all five verdicts, and 1 means the listing could not be read** — no answer at
352
+ `backlogrun` takes no `owner/repo#N` token.** Report the other repository's candidates in the run
353
+ summary and leave them to a session running there (`backlogrun-lanes.md` → "Concurrency").
354
+ 3. **The verdict words are `wait`, `stop`, `triage`, `retry`, `error` and `none`** — `none` is
355
+ `epic:next`'s `complete` under this command's spelling, and there is no `complete` here. **`retry`
356
+ is the one with no `epic:next` counterpart**: it says GitHub did not answer.
357
+ 4. **Exit code 0 covers all six verdicts, and 1 means the listing could not be read** — no answer at
401
358
  all. **`error` cannot be told apart by exit code, so read the token rather than the status.**
402
359
 
403
360
  **What the answer means is this table's; whether the run may act on it is `pnpm josh backlog:budget`'s**
@@ -411,52 +368,49 @@ subsection.
411
368
  | `wait`, with something of this run's own still in flight | Everything opted in is blocked or already running, so waiting can still change the answer. **Ask the command again on the wake the progress watcher's exit delivers** — the parent starts no sleep of its own, and the interval is a floor on the re-ask rather than a clock (`backlogrun-progress.md` → "Waiting, and never waiting forever" and "The parent keeps no clock of its own") | `blocked` |
412
369
  | `wait` this checkout can never resolve — the only candidates the command reported on standard error are in other repositories, and this run has nothing of its own in flight | Report those candidates with their checkouts. **Waiting cannot resolve them, but a person opting a new issue in here still can**, so the ending is the idle watch's rather than this row's | `exhausted` |
413
370
  | `stop` | Nothing can proceed without a person. Report the parked issues and finish | `parked` |
414
- | `retry`, on fewer than three consecutive asks | GitHub did not answer, so the graph was never read. Sleep the polling interval and **ask the command again** — **this is one of the states with no watcher-delivered wake**, because the outage that produced `retry` stops the watcher reading too and a declined watcher does not exit (`backlogrun-progress.md` → "The wake exists only while something is in flight"). This is the one answer re-asking is allowed on, and the count is consecutive: any other answer resets it to zero | `blocked` |
371
+ | `triage` | A candidate lacks `run:solo` / `run:lane` (joshuafolkken/kit#2779). Judge each one stderr names as the pre-start pass does, then **ask again**; running children continue | `untriaged` |
372
+ | `retry`, on fewer than three consecutive asks | GitHub did not answer, so the graph was never read. Sleep the polling interval and **ask the command again** — **this is one of the states with no watcher-delivered wake** (`backlogrun-progress.md` → "The wake exists only while something is in flight"). This is the one answer re-asking is allowed on, and the count is consecutive: any other answer resets it to zero | `blocked` |
415
373
  | `retry` for a third consecutive time | The outage is not a hiccup. Report what the command printed on standard error and finish | `unreadable` |
416
- | `error` | The graph could not be resolved — report what the command printed on standard error and finish. **Never re-ask hoping for a different answer**, and never fall back to picking an issue by hand: that would be the run choosing its own membership | `unreadable` |
374
+ | `error` | The graph could not be resolved — report what the command printed on standard error and finish. **Never re-ask hoping for a different answer**, and never fall back to picking an issue by hand | `unreadable` |
417
375
  | `none` | Nothing opted in is left | `exhausted` |
418
- | Exit 1, empty standard output | The listing could not be read. Report it and finish — **it is not `none`**, and reading it as one would report an empty backlog that was never seen | `unreadable` |
376
+ | Exit 1, empty standard output | The listing could not be read. Report it and finish — **it is not `none`** | `unreadable` |
419
377
 
420
- **Re-asking on `retry` is not the exception to the `error` rule** (joshuafolkken/kit#1663): `error`
421
- forbids a run answering its own question, while `retry` means the command never got an answer to read,
422
- so asking again is the same question rather than a second opinion. **Three consecutive `retry` answers
423
- end the run**, and any other answer resets the count to zero, so `backlog:budget` is told `blocked`
424
- while retries remain and `unreadable` on the third.
378
+ **Three consecutive `retry` answers end the run**, and any other answer resets the count to zero, so
379
+ `backlog:budget` is told `blocked` while retries remain and `unreadable` on the third.
425
380
 
426
- **Feed every issue this run has merged back through `--exclude`.** GitHub applies `closes #N`
427
- asynchronously, so a just-merged issue can still read as open on the next ask and be offered a second
428
- time. The flag takes a comma-separated list and may be repeated; it drops the issue from every bucket
429
- rather than only from the offer.
381
+ **Feed every issue this run has merged back through `--exclude`** — a just-merged issue can still read
382
+ as open on the next ask. The flag takes a comma-separated list and may be repeated; it drops the issue
383
+ from every bucket rather than only from the offer.
430
384
 
431
- **A short offer is not proof the backlog is empty.** The standalone half of the pool is capped at the
432
- same five rows the `🗒 Next issues` display shows, so a sixth opted-in issue simply appears on the
433
- next ask. An epic's children are not capped that way — they come through the epic's own graph.
385
+ **A short offer is not proof the backlog is empty**: the standalone half is capped at the five rows
386
+ `🗒 Next issues` shows, so a sixth opted-in issue appears on the next ask; an epic's children come
387
+ through its own graph, uncapped.
434
388
 
435
- **The epic side is found server-side by the `epic` label.** An epic that never received the label is
436
- invisible to the listing, so nothing knows it tracks anything at all (joshuafolkken/kit#1633). This is
437
- a known limit of the listing, recorded rather than worked around; do not assert the opposite anywhere.
389
+ **The epic side is found server-side by the `epic` label**, so an epic without it is invisible and
390
+ tracks nothing — a known limit of the listing (joshuafolkken/kit#1633).
438
391
 
439
392
  **A child is offered on its own `auto-ok` unless the epic tracking it is opted in**
440
- (joshuafolkken/kit#1668). An epic that **is** opted in owns its children entirely — it sequences them,
441
- so offering one beside it would hand the same issue over twice; ordering survives the standalone route
442
- on the children's own `blocked-by` relations, which `--ordered` records.
393
+ (joshuafolkken/kit#1668). An epic that **is** opted in owns its children entirely and sequences them;
394
+ ordering survives the standalone route on the children's own `blocked-by` relations, which `--ordered`
395
+ records.
443
396
 
444
397
  **An order between two epics holds too** (joshuafolkken/kit#1943). A `blocked-by` relation whose
445
398
  blocker sits in another opted-in epic — or is an opted-in standalone issue — makes the child wait on
446
- time, so one `backlogrun` runs both epics in that order without the person sequencing the commands. A
447
- blocker outside the opted-in backlog, or one inside it that itself waits on a person, makes the child
448
- wait on a person instead, and a cycle across epics
449
- answers `error`. The table is `docs/josh-commands.md` → "`josh epic:next`".
399
+ time, so one `backlogrun` runs both epics in that order. A blocker outside the opted-in backlog, or one
400
+ inside it that itself waits on a person, makes the child wait on a person instead, and a cycle across
401
+ epics answers `error`. The table is `docs/josh-commands.md` → "`josh epic:next`".
450
402
 
451
403
  **New work is picked up without restarting anything.** The command re-derives its pool from GitHub
452
404
  labels on every ask, so an issue filed and opted in while the run is going is offered on the next
453
- iteration.
405
+ iteration, once triaged.
406
+
407
+ Rationale: `docs/maintainers/backlogrun-steps-rationale.md` → "Why the loop's contract is shaped this way"
454
408
 
455
409
  ### The two budgets
456
410
 
457
411
  **A `backlogrun` may declare how long it will watch an empty backlog and how many issues it may
458
- take** (joshuafolkken/kit#1632). Both are written on the keyword, and **the idle watch is on by
459
- default while the maximum is not** (joshuafolkken/kit#1676):
412
+ take.** Both are written on the keyword, and **the idle watch is on by default while the maximum is
413
+ not**:
460
414
 
461
415
  ```
462
416
  backlogrun # watches for 30 minutes after the backlog empties
@@ -469,19 +423,15 @@ backlogrun --idle 0 --max 5
469
423
  | Idle watch | `--idle <minutes>` | **30 minutes** | After the candidates run out, keep polling this long for a new one. A candidate that appears restarts the watch from that moment |
470
424
  | Maximum issues | `--max <count>` | unlimited | How many issues this invocation may take. On reaching it the run reports and finishes |
471
425
 
472
- **`--idle 0` is how the watch is turned off, and it is the only way** (joshuafolkken/kit#1676):
473
- `backlogrun --idle 0` finishes at the first empty backlog, exactly as a bare `backlogrun` did before.
474
-
475
- **Why 30 minutes** (single source `scripts/backlog/backlog-budget.ts` → `DEFAULT_IDLE_MINUTES`): it is
476
- about the length of one child (12–28 minutes), so a run that has emptied its backlog waits roughly as
477
- long as one more issue would have taken. **A watch is polled every 5 minutes, not at the loop's
478
- 60-second interval** — it is waiting on a person, on human timescales, and the reason `backlog:budget`
479
- prints names the interval so the loop reads it rather than remembering it.
426
+ **`--idle 0` is how the watch is turned off, and it is the only way**: `backlogrun --idle 0` finishes
427
+ at the first empty backlog. The default's single source is `scripts/backlog/backlog-budget.ts` →
428
+ `DEFAULT_IDLE_MINUTES`. **A watch is polled every 5 minutes, not at the loop's 60-second interval** —
429
+ read the interval from the reason `backlog:budget` prints rather than remembering it.
480
430
 
481
- **Why an idle watch is safe.** To become a candidate an issue needs `auto-ok`, which only a person
482
- applies — except an issue this run filed and bundled under an already opted-in epic, which is admitted
483
- ("What one invocation approves") and bounded by the brake there, never the watch. No issue nobody
484
- opted in is ever picked up during a watch.
431
+ **To become a candidate an issue needs `auto-ok`, which only a person applies** — except an issue this
432
+ run filed and bundled under an already opted-in epic, which is admitted ("What one invocation
433
+ approves") and bounded by the brake there, never the watch. No issue nobody opted in is ever picked up
434
+ during a watch.
485
435
 
486
436
  **`backlog:offer` asks `backlog:budget` for you on every iteration** — it maps `backlog:next`'s answer
487
437
  to the word the table above fixes and hands it over, so the loop makes the one call and reads back the
@@ -493,10 +443,10 @@ offer=$(pnpm josh backlog:offer --started "$started" --active "$active" \
493
443
  ```
494
444
 
495
445
  **`--active` is required of every ask, because the watch is on unless it was turned off** — an
496
- invocation whose watch is on and that carries no `--active` is **refused** by `backlog:budget`, so an
497
- emptiness nobody watched cannot be reported as an ordinary `stop`. Only `--idle 0` excuses it.
498
- **`--running` also decides `wait` and `--retries` decides `retry`** — the two context branches the
499
- table above carries, now applied inside the command from the counts the loop already tracks.
446
+ invocation whose watch is on and that carries no `--active` is **refused** by `backlog:budget`. Only
447
+ `--idle 0` excuses it. **`--running` also decides `wait` and `--retries` decides `retry`** — the two
448
+ context branches the table above carries, applied inside the command from the counts the loop already
449
+ tracks.
500
450
 
501
451
  | Verdict | What the loop does |
502
452
  | --- | --- |
@@ -522,25 +472,25 @@ contract is `docs/josh-commands.md` → "`josh backlog:budget`".
522
472
  took, how many of them were picked up during an idle watch, and the termination reason — quoted from
523
473
  what `backlog:budget` printed rather than paraphrased.
524
474
 
475
+ Rationale: `docs/maintainers/backlogrun-steps-rationale.md` → "Why the idle watch defaults to 30 minutes"
476
+
525
477
  ### The cost check is not asked during a watch
526
478
 
527
- **A watch does not count towards the session cut** (joshuafolkken/kit#1676). The hand-off check —
528
- `pnpm josh cost --cut`, `backlogrun-progress.md` → "The hand-off" — is asked **at a child's merge**, and a
529
- watch has no merges, so a run that is only watching never reaches one. That is a decision rather than
530
- an omission: a watch holds nothing (the working tree's hold and every lane were released at the last
531
- child's merge), its cost is bounded before it starts (`--idle N` is at most `N / 5` asks), and when the
532
- watch picks something up the run has work again and the check is asked at that child's merge in the
533
- ordinary way.
479
+ **A watch does not count towards the session cut.** The hand-off check — `pnpm josh cost --cut`,
480
+ `backlogrun-progress.md` → "The hand-off" — is asked **at a child's merge**, and a watch has no merges,
481
+ so a run that is only watching never reaches one. When the watch picks something up, the check is
482
+ asked at that child's merge in the ordinary way.
534
483
 
535
- **So every cut is taken at a merge, which is what lets a resumed session state its own `--active`**: a
536
- woken session picks the run up seconds after the merge the cut was taken at. The one shape that puts a
537
- cut inside a watch — the backlog answering `exhausted` while this run's own children are still
538
- merging — is the same, because the woken session has just merged a child. A Codex parent's cut is the one
539
- taken at a dispatch instead (`backlogrun-progress.md` → "A parent without a completion callback").
484
+ **So every cut is taken at a merge.** The one shape that puts a cut inside a watch — the backlog
485
+ answering `exhausted` while this run's own children are still merging — is the same, because the woken
486
+ session has just merged a child. A Codex parent's cut is the one taken at a dispatch instead
487
+ (`backlogrun-progress.md` → "A parent without a completion callback").
540
488
 
541
489
  **The 8-hour whole-run bound is untouched and still outranks all of this** — it is measured from the
542
490
  record's `started_at` across every cut, so a run cannot watch its way past it in 30-minute pieces.
543
491
 
492
+ Rationale: `docs/maintainers/backlogrun-steps-rationale.md` → "Why the cost check skips a watch"
493
+
544
494
  ### Where the run stops
545
495
 
546
496
  Termination is decided by what the loop is told, never by a judgement that enough has been done:
@@ -556,8 +506,7 @@ Termination is decided by what the loop is told, never by a judgement that enoug
556
506
  watch still open.
557
507
  - **Every one of those budgets is counted across this run's own session cuts**, from the record
558
508
  `pnpm josh run:carry` keeps rather than from a count held in the current session — "The session cut
559
- is inside the invocation" above. A resumed run that started its budget over would stop 8 hours after
560
- the _last_ cut instead of after the invocation, which is no bound at all.
509
+ is inside the invocation" above.
561
510
  - **The guards in `backlogrun.md` → "Guards"** apply unchanged, counted over the whole `backlogrun`
562
511
  rather than per epic: children per run, Issues filed per run, and consecutive child failures. The
563
512
  maximum above is a person's declaration of scale and does not replace any of them — whichever binds
@@ -567,7 +516,7 @@ Termination is decided by what the loop is told, never by a judgement that enoug
567
516
  what happens to its lane.
568
517
  - **The hand-off check** — `pnpm josh cost --cut` at every child's merge, and the lane
569
518
  hand-over that follows an `over` — is `backlogrun-progress.md` → "The hand-off", unchanged. It is
570
- **not** asked during an idle watch: "The cost check is not asked during a watch" above is why.
519
+ **not** asked during an idle watch: "The cost check is not asked during a watch" above.
571
520
  - **Parking is not stopping.** A child that needs a decision, and a delegated unit that stopped
572
521
  without reporting, are **parked** and the run continues (`backlogrun-park.md` → "park and
573
522
  continue"); only the consecutive-failure guard above can turn repeated parks into a stop.
@@ -584,22 +533,17 @@ All of these are this file's, and are reached here in the same order and for the
584
533
  | `pnpm josh run:progress --wait` in the background, `--mark` at every real report | `backlogrun-progress.md` → "Progress while the run is quiet" |
585
534
  | `pnpm josh release:scope` once, after the last issue has merged and the last lane is closed | `followup-reference.md` → "When `pnpm josh release` runs" |
586
535
 
587
- **Two more run once per session and are this file's own, not this file's**:
536
+ **Two more run once per session and are this file's own**:
588
537
  `pnpm josh run:carry --begin "<the invocation, single-spaced>" --owner "$PPID"` before the plan and
589
538
  `pnpm josh run:carry --end` when the run finishes, and beside each of them
590
539
  `pnpm josh run:wake --start` and `pnpm josh run:wake --stop` — the supervisor that continues the run
591
540
  across a cut. Both pairs are "The session cut is inside the invocation" above.
592
541
 
593
542
  **Record the invocation in the form the supervisor can rebuild: one space between tokens, and a plain
594
- integer for each budget value** — `backlogrun --max 3`, never `backlogrun --max 03`. What the
595
- supervisor hands the next session is composed from constants and the validated integers rather than
596
- copied out of the record, and it refuses to wake on a record its own rebuild would rewrite, because the
597
- woken session hands that text straight back to `run:carry --begin` to be compared character for
598
- character. A record written some other way is not lost quietly — the first cut ends the run with a
599
- `warning` Telegram — but it ends the run, so write the canonical form rather than the keystrokes
600
- (joshuafolkken/kit#1719).
543
+ integer for each budget value** — `backlogrun --max 3`, never `backlogrun --max 03`. The supervisor
544
+ refuses to wake on a record its own rebuild would rewrite: the first cut ends the run with a `warning`
545
+ Telegram, so write the canonical form rather than the keystrokes.
601
546
 
602
547
  **`pnpm josh epic:audit` is not run.** There is no epic to audit — the run began from the backlog, the
603
- same reason this file skips it when it began from a bare Issue. The dependency graph the loop acts
604
- on is still audited in effect, because `backlog:next` reuses `epic:next`'s own read and classify and
605
- answers `error` rather than guessing when it cannot resolve one.
548
+ same reason this file skips it when it began from a bare Issue. `backlog:next` reuses `epic:next`'s
549
+ own read and classify and answers `error` rather than guessing when it cannot resolve one.