@joshuafolkken/kit 1.957.0 → 1.978.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 (102) 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 +4 -4
  9. package/.claude/skills/workflow-commands/halfrun.md +12 -7
  10. package/.claude/skills/workflow-commands/observation-filing.md +31 -25
  11. package/.claude/skills/workflow-commands/pre-gate-cut.md +96 -161
  12. package/.claude/skills/workflow-commands/working-tree-hold.md +16 -1
  13. package/.github/workflows/pr-classification.yml +12 -1
  14. package/CLAUDE.md +3 -3
  15. package/README.md +25 -21
  16. package/dist/CLAUDE.md +3 -3
  17. package/dist/hooks/batch-guard.js +4 -4
  18. package/dist/hooks/{chunk-XSL4SRMA.js → chunk-2K52BRWV.js} +2 -2
  19. package/dist/hooks/{chunk-M6K7EQDO.js → chunk-BNGGEY3Z.js} +38 -32
  20. package/dist/hooks/{chunk-KOCK5FXQ.js → chunk-BVMSPG3M.js} +3 -3
  21. package/dist/hooks/{chunk-3ON7HOYB.js → chunk-CRGJTD6E.js} +1 -1
  22. package/dist/hooks/{chunk-HJXQZ7V4.js → chunk-F6OM6SIG.js} +3 -3
  23. package/dist/hooks/{chunk-LDQDZYHX.js → chunk-KQG4776P.js} +1 -1
  24. package/dist/hooks/{chunk-244WYOEO.js → chunk-NXL3SBXY.js} +2 -2
  25. package/dist/hooks/{chunk-IJPBTDTB.js → chunk-Q7LE77CO.js} +88 -18
  26. package/dist/hooks/{chunk-BELI5XSS.js → chunk-ZPLXLJ7O.js} +15 -5
  27. package/dist/hooks/{chunk-GNOITLRR.js → chunk-ZYYSBNR6.js} +4 -4
  28. package/dist/hooks/codex-hook-adapter.js +10 -10
  29. package/dist/hooks/duplicate-read-guard.js +5 -5
  30. package/dist/hooks/format-edited.js +2 -2
  31. package/dist/hooks/investigation-guard.js +5 -5
  32. package/dist/hooks/pretool-guard.js +9 -9
  33. package/dist/hooks/stop-guard.js +206 -176
  34. package/dist/josh.js +8 -4
  35. package/package.json +4 -4
  36. package/prompts/collaboration-workflow/wip-cap.md +1 -1
  37. package/scripts/backlog/backlog-arrival.ts +11 -1
  38. package/scripts/backlog/backlog-budget.ts +25 -8
  39. package/scripts/backlog/backlog-next.ts +45 -2
  40. package/scripts/backlog/backlog-offer.ts +1 -0
  41. package/scripts/backlog/backlog-plan-cli.ts +66 -27
  42. package/scripts/backlog/backlog-plan.ts +30 -2
  43. package/scripts/backlog/backlog-stalled.ts +9 -0
  44. package/scripts/backlog/backlog-waves.ts +169 -0
  45. package/scripts/ci/pr-classification.ts +13 -12
  46. package/scripts/document/document-byte-budget.ts +1 -1
  47. package/scripts/document/entry-read-budget.ts +7 -5
  48. package/scripts/epic/epic-lane-offer.ts +69 -4
  49. package/scripts/epic/epic-next.ts +2 -2
  50. package/scripts/epic/epic-solo.ts +147 -0
  51. package/scripts/epic/epic-triage.ts +47 -0
  52. package/scripts/gate/hook-gate-reuse.ts +7 -8
  53. package/scripts/git/git-closes-keyword.ts +15 -0
  54. package/scripts/git/git-followup-flush.ts +5 -6
  55. package/scripts/git/git-followup-issue-close.ts +75 -0
  56. package/scripts/git/git-pr-followup-wrapup.ts +36 -6
  57. package/scripts/git/git-pr-followup.ts +9 -16
  58. package/scripts/git/git-staging.ts +33 -20
  59. package/scripts/git/issue-labels.ts +75 -5
  60. package/scripts/hooks/stop-guard.ts +1 -0
  61. package/scripts/init/init-bootstrap.ts +92 -0
  62. package/scripts/init/init-install.ts +67 -0
  63. package/scripts/init/init-logic-workspace.ts +1 -1
  64. package/scripts/init/init.ts +58 -13
  65. package/scripts/init/start-plan.ts +1 -1
  66. package/scripts/init/start-steps.ts +5 -28
  67. package/scripts/issue/issue-closing-pr.ts +93 -0
  68. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +2 -1
  69. package/scripts/josh/josh-commands-project.ts +5 -1
  70. package/scripts/josh/josh-commands-workflow.ts +3 -3
  71. package/scripts/observations/observation-ledger-prepare.ts +31 -0
  72. package/scripts/observations/observation-ledger.ts +5 -5
  73. package/scripts/observations/observations-flush.ts +11 -30
  74. package/scripts/repo/repository-labels.ts +91 -0
  75. package/scripts/rules/implementation-cut.ts +49 -16
  76. package/scripts/rules/lane-background.ts +25 -5
  77. package/scripts/rules/stop-rules.ts +16 -4
  78. package/scripts/run/run-carry-cli.ts +21 -0
  79. package/scripts/run/run-cut-cli.ts +92 -110
  80. package/scripts/run/run-cut-report.ts +167 -0
  81. package/scripts/run/run-cut.ts +23 -7
  82. package/scripts/run/run-entry-cli.ts +70 -3
  83. package/scripts/run/run-halfrun-resume.ts +104 -0
  84. package/scripts/run/run-headless.ts +1 -0
  85. package/scripts/run/run-hold-cli.ts +57 -13
  86. package/scripts/run/run-hold.ts +26 -4
  87. package/scripts/run/run-merge-cli.ts +28 -14
  88. package/scripts/run/run-merge-steps.ts +38 -2
  89. package/scripts/run/run-merge.ts +41 -3
  90. package/scripts/run/run-prep-cli.ts +8 -2
  91. package/scripts/run/run-prep-locate.ts +165 -0
  92. package/scripts/run/run-prep.ts +12 -1
  93. package/scripts/run/run-step-cli.ts +16 -0
  94. package/scripts/run/run-step.ts +28 -4
  95. package/scripts/run/run-tail-cli.ts +4 -2
  96. package/scripts/run/run-wake-work.ts +4 -0
  97. package/scripts/safe-chain/ci-installer-pin.ts +8 -4
  98. package/scripts/security/security-audit.ts +12 -1
  99. package/scripts/sync/sync.ts +6 -0
  100. package/scripts/time-runtime/time-background.ts +11 -0
  101. package/scripts/time-runtime/time-transcript-line.ts +4 -0
  102. package/templates/pnpm-workspace.static.yaml +2 -0
@@ -2,15 +2,13 @@
2
2
 
3
3
  **Read this file in full before the first lane opens** — in the turn that reaches
4
4
  `pnpm josh lane:open`. It is a point-of-use document, never an entry read: the entry procedure is
5
- `backlogrun.md`, which points here at that step (joshuafolkken/kit#2010). This file is the single
6
- source of the per-repository lane ceiling, the lane lifecycle, and how a merge conflict between lanes
7
- is resolved.
5
+ `backlogrun.md`, which points here at that step. This file is the single source of the per-repository
6
+ lane ceiling, the lane lifecycle, and how a merge conflict between lanes is resolved.
8
7
 
9
8
  ## Concurrency: as many children per repository as it has free lanes
10
9
 
11
- Execution state lives on GitHub and nowhere else, so **an `backlogrun` need not be a single session.** One
12
- session per repository; each calls `josh epic:next <E> --repo <owner/repo>` and runs only its own
13
- repository's children.
10
+ **An `backlogrun` need not be a single session.** One session per repository; each calls
11
+ `josh epic:next <E> --repo <owner/repo>` and runs only its own repository's children.
14
12
 
15
13
  ```bash
16
14
  # In the kit checkout
@@ -26,7 +24,7 @@ kit's issue does not publish kit, so such a dependency resolves only once the bl
26
24
  its release has appeared in the registry — and while the blocker is still open the registry is never
27
25
  consulted. **Unless that repository publishes nothing** — no `package.json` on its default branch, or
28
26
  one declaring `private` — in which case a closed blocker there resolves. The answer is read from the
29
- blocker repository's manifest, never from the registry (a 404 also means "this token may not see it").
27
+ blocker repository's manifest, never from the registry.
30
28
 
31
29
  **The lane count is per repository, and `epic:next` is what applies it.** When it has a child to offer,
32
30
  it first asks that repository **how many of its lanes are already running something**: every open issue
@@ -34,29 +32,23 @@ carrying `in-progress` and not parked counts for one — whichever epic it belon
34
32
  `JOSH_LANE_LIMIT` (**default 6**) is what gets offered, and at zero the answer is `wait`. It is asked
35
33
  **only when there is a candidate**. A **parked** issue does not hold a lane: `needs-decision` outranks
36
34
  `in-progress` here exactly as in the classification. A child stopped by `needs-human-review` is
37
- deliberately not parked and goes on holding its lane, because its uncommitted work is still in that
38
- checkout.
35
+ deliberately not parked and goes on holding its lane.
39
36
 
40
37
  **It is advisory and not atomic.** The label is applied when a child is dispatched — the parent claims
41
38
  it in `lane:dispatch` before the child process starts, and a standalone `fullrun` / `halfrun` claims it
42
- the moment `run:hold` answers `hold` — *after* this read, so two sessions starting in the same instant
43
- can both read the same free lane. What the check closes is the window that actually occurs — a
44
- dispatched lane holds the label from the launch onward, not the tens of minutes it took when the
45
- child's own `fullrun` had to reach its own apply first. It is a guard that makes the invariant
46
- mechanical, not a mutex.
39
+ the moment `run:hold` answers `hold` — *after* this read. It is a guard, not a mutex.
47
40
 
48
41
  **It is scoped to the resource, not the epic.** A lane is its own checkout with its own branch and
49
42
  ports, so the repository-wide number is a **ceiling on how many lanes run at once**. How a session
50
43
  drives more than one child at a time is "Lanes — running more than one child at a time" below.
51
44
 
52
- **A stale label holds a lane, so the stale rule reaches past this epic's own children** — "`in-progress`
53
- is removed by whoever finds it stale" below applies to **any** open issue in the repository. `epic:next`
45
+ **A stale label holds a lane, so the stale rule reaches past this epic's own children** — `backlogrun-park.md` →
46
+ "`in-progress` is removed by whoever finds it stale" applies to **any** open issue in the repository. `epic:next`
54
47
  names the holders on standard error, and the 90-minute stale window bounds the wait.
55
48
 
56
49
  **A listing it could not read is not an idle repository.** `epic:next` answers `wait` there rather than
57
- offering the child — reading a failed read as "nothing is running" is the one direction this guard may
58
- not fail in, because that answer *starts* work. **A listing that was *cut short* is the same answer**: a
59
- short one with no visible holder is `wait`, with its own message.
50
+ offering the child. **A listing that was *cut short* is the same answer**: a short one with no visible
51
+ holder is `wait`, with its own message.
60
52
 
61
53
  **Two children of one repository may run at once, and the section below is how.** The guard is a
62
54
  ceiling rather than a prohibition, scoped to the lane rather than to the epic. **Do not read this as
@@ -66,6 +58,9 @@ switching the guard off is not one of the ways to get parallelism.
66
58
  Parallelism only helps children that do not depend on each other. When app-kit's child needs kit's new
67
59
  feature, that is recorded as `blocked-by` and `epic:next` makes it wait.
68
60
 
61
+ Rationale: `docs/maintainers/backlogrun-lanes-rationale.md` → "Why the lane ceiling is shaped the way
62
+ it is".
63
+
69
64
  ## Lanes — running more than one child at a time
70
65
 
71
66
  `pnpm josh epic:next <E> --repo <owner/repo> --lanes` answers with **one issue number per line**, up to
@@ -85,41 +80,37 @@ number (`pre-gate-cut.md` → "The dispatch mark" and "The stage is passed to th
85
80
  verification path itself.** It runs alone, and the batch resumes only once it has merged. **Decide it
86
81
  from the enumeration, never from how serious it looks** — does the defect reach the verification gate
87
82
  (lint / type check / spell check / unit tests), the code review, the pre-push hook, or the merge
88
- checks? One of those, and the offered children wait; none, and it fills a lane like any other child.
89
- **Ask it of what `epic:next --lanes` just offered, before opening a second lane**; the rule is
83
+ checks? One of those, and the issue carries `run:solo`; none, and it carries `run:lane` and fills a
84
+ lane like any other child. `backlog:next` and `epic:next --lanes` enforce both (joshuafolkken/kit#2776,
85
+ #2779). The rule is
90
86
  `prompts/collaboration-workflow/wip-cap.md` → 「実行のしかた」, its single source.
91
87
 
92
88
  **A lane's review does not inherit the lane, and `pnpm josh review:brief` is what closes that.**
93
- `/code-review` is forked by the harness into the **session's** working directory, which during a lane
94
- run is a different tree — usually the default branch. Reading that, the review finds nothing wrong and
95
- **the failure arrives as approval**: the child counts the round as clean and commits a diff nobody
96
- read. **So the path is no longer something a brief has to remember to carry**: `pnpm josh review:brief`
97
- prints the lane's absolute root, branch and HEAD, hands over targets written `git -C <root> …`, and
98
- prints a nonce the review attests with `pnpm josh review:attest <nonce>` from the checkout it actually
99
- read. **The child asks `pnpm josh review:attest --check` before it counts a round**, and
100
- `pnpm josh followup` asks again before it merges; `missing` and `mismatch` are both refusals. A clean
101
- round is therefore the case to check hardest, not the case to skip the check on.
89
+ `pnpm josh review:brief` prints the lane's absolute root, branch and HEAD, hands over targets written
90
+ `git -C <root> …`, and prints a nonce the review attests with `pnpm josh review:attest <nonce>` from
91
+ the checkout it actually read. **The child asks `pnpm josh review:attest --check` before it counts a
92
+ round**, and `pnpm josh followup` asks again before it merges; `missing` and `mismatch` are both
93
+ refusals. A clean round is the case to check hardest, not the case to skip the check on. Rationale:
94
+ `docs/maintainers/backlogrun-lanes-rationale.md` → "Why a lane's review needs a brief and an
95
+ attestation".
102
96
 
103
97
  **A lane's branch is `<N>-lane`, and the issue number leads it so that the commit path accepts it.**
104
- `pnpm josh git` refuses to commit from a branch that is neither the default branch nor one sharing the
105
- child's `<N>-` prefix (`scripts/git/git-branch.ts` → `has_same_issue_prefix`, `/^\d+-/`). **Switching
106
- the lane to another branch is not the way round it**: `lane-registry.ts` → `branch_issue` identifies a
107
- lane **by** that branch name, so a switched lane drops out of `list_lanes()` — losing its seat, its
108
- listing and its isolation. So `epic:next` is asked **with** `--lanes`, and everything below runs as
109
- written.
110
-
111
- **Running unattended gets harder, not easier, and that is the honest trade.** Many lanes make the
112
- overlap between children real, and every overlap that becomes a conflict costs the child that loses the
113
- race a resolution, a re-run gate and a review ("Conflicts are not predicted" below), and parks it
114
- outright under that section's four conditions. Report the throughput gain only with its merge-race
115
- cost.
98
+ **Never switch the lane to another branch** — a switched lane loses its seat, its listing and its
99
+ isolation. So `epic:next` is asked **with** `--lanes`, and everything below runs as written. Rationale:
100
+ `docs/maintainers/backlogrun-lanes-rationale.md` → "Why the lane branch is `<N>-lane` and never
101
+ switched".
102
+
103
+ **Report the throughput gain only with its merge-race cost** — every overlap that becomes a conflict
104
+ costs a resolution, a re-run gate and a review ("Conflicts are not predicted" below). Rationale:
105
+ `docs/maintainers/backlogrun-lanes-rationale.md` → "Why lanes cost as well as buy throughput".
116
106
 
117
107
  ### Once per repository, before the first lane opens
118
108
 
119
109
  In the **primary checkout**, in this order, and never again per lane:
120
110
 
121
111
  1. `git switch main && git pull` — every lane branches from this ref.
122
- 2. `pnpm josh latest:scope`, and the update on `required` — "`josh latest` runs once per session" above.
112
+ 2. `pnpm josh latest:scope`, and the update on `required` — `backlogrun-child.md` → "`josh latest`
113
+ runs once per session, not once per child".
123
114
  3. `pnpm josh lane:prune` — closes the lanes an interruption left registered without a work tree.
124
115
  4. `pnpm josh run:tidy` — sweeps merged lanes and stashes (as `run:hold` does).
125
116
 
@@ -141,10 +132,9 @@ is a repository-wide stack every lane shares, so a positional pop would take whi
141
132
 
142
133
  ### Opening one lane and dispatching its child
143
134
 
144
- **One command opens the lane, prepares it, and dispatches the child — `pnpm josh lane:launch`**
145
- (joshuafolkken/kit#2162). It runs `lane:open`, then — only when `--stash` is given, which is the first
146
- lane alone — pops that stash into the lane and re-installs against the lock it brought in, then
147
- `lane:dispatch`. Four to six turns of the parent folded into one:
135
+ **One command opens the lane, prepares it, and dispatches the child — `pnpm josh lane:launch`.** It
136
+ runs `lane:open`, then — only when `--stash` is given, which is the first lane alone — pops that stash
137
+ into the lane and re-installs against the lock it brought in, then `lane:dispatch`:
148
138
 
149
139
  ```bash
150
140
  pid=$(pnpm josh lane:launch "$n") || exit 1 # the child's pid on stdout, nothing else; alias: josh lnla
@@ -153,6 +143,8 @@ pid=$(pnpm josh lane:launch "$n" --stash "backlogrun: josh latest before lanes")
153
143
 
154
144
  `lane:launch` is a thin layer over `lane:open`, `stash:pop` and `lane:dispatch`, so each of their
155
145
  guards, refusals and messages holds unchanged; the bullets below describe the steps it runs.
146
+ Rationale: `docs/maintainers/backlogrun-lanes-rationale.md` → "Why `lane:launch` folds the lane steps
147
+ into one".
156
148
 
157
149
  - **A refusal is an empty capture beside a non-zero exit**, with the reason on standard error: `full`,
158
150
  `already-open`, and a **failed install** — which leaves a real work tree behind holding its seat, so
@@ -162,21 +154,15 @@ guards, refusals and messages holds unchanged; the bullets below describe the st
162
154
  dispatched**.
163
155
  - **`lane:open` installs; the `--stash` lane re-installs, and only that lane needs it.** A failed
164
156
  install fails `lane:open`, so `lane:launch` reaching `lane:dispatch` is the guarantee the lane runs.
165
- - **What the pop changes is the lock, which is why that one lane installs twice.** The pop brings in
166
- the `pnpm-lock.yaml` that `josh latest` rewrote, which this child's gate must build against. **A pop
167
- that fails stops the lane** rather than re-installing anyway.
157
+ **A pop that fails stops the lane** rather than re-installing anyway.
168
158
  - **A `<N>-lane` that already exists is attached to, and `lane:open` says so on standard error** — the
169
159
  way back to a child parked after it pushed: a local branch of that name gets the work tree put on it,
170
160
  and where there is none the **remote** is asked (`ls-remote`, fetched first) and the lane cut from
171
161
  `origin/<N>-lane` where the branch survives — otherwise from the default branch. **A reused branch is
172
162
  not a fresh lane.** Nothing deletes a branch to open a lane, so a reopen cannot cost the pushed work.
173
- - **Nothing switches the lane's branch** — the registry identifies a lane by that branch, so a switch
174
- costs it its seat, listing and isolation, and `<N>-lane` is already a name `pnpm josh git` commits
175
- from.
176
- - **`pnpm josh run:hold`'s preflight check is not asked in a lane; `lane:open`'s own answer replaces it.** A fresh
177
- lane is clean by construction, preflight's `reclaim` arm (HEAD != default branch) is true of every
178
- lane, and its recovery (`git switch <default>`) cannot run in a linked work tree. What a leftover
179
- looks like here is `lane:open` answering `already-open`.
163
+ - **Nothing switches the lane's branch.**
164
+ - **`pnpm josh run:hold`'s preflight check is not asked in a lane; `lane:open`'s own answer replaces it.**
165
+ What a leftover looks like here is `lane:open` answering `already-open`.
180
166
  - **`pnpm josh run:hold` is unchanged, and is claimed inside the lane** — it keys on
181
167
  `git rev-parse --absolute-git-dir` (`.git/worktrees/<name>`), so each lane holds independently.
182
168
 
@@ -191,27 +177,25 @@ final step, the `lane:dispatch` it runs after the lane is opened and prepared, w
191
177
  The bullets below describe that dispatch:
192
178
 
193
179
  - **It records where the child writes as it starts it** — the dispatch's own
194
- `<temp>/josh-lane-dispatch-<N>.log` — so "this lane records no path" is no longer a state the hand-off
195
- has to except.
180
+ `<temp>/josh-lane-dispatch-<N>.log`.
196
181
  - **A refusal is an empty capture beside a non-zero exit**, and every refusal also sends a `warning`
197
182
  Telegram — no lane open, a path that could not be recorded, a launch that failed. **Park that child
198
183
  and name the log the message carries**; do not re-dispatch into the same lane without reading it.
199
184
  - **A child that started with nowhere to write exits zero and warns.** The launcher starts the session
200
185
  even when it could not open the log, so the child really is running. **Poll that lane on the process
201
186
  trace alone**, because its log will never grow.
202
- - **The brief the child used to be handed is now the invocation itself.** The child is started as
203
- `fullrun #<N>` with the lane as its working directory. The batch a person approved by typing
204
- `backlogrun` covers the child.
187
+ - **The child is started as `fullrun #<N>` with the lane as its working directory.** The batch a
188
+ person approved by typing `backlogrun` covers the child.
205
189
  - **It does not pass `--dangerously-skip-permissions`.** What the child may do is the checkout's own
206
190
  `.claude/settings.json` to decide; a headless child reaches `gh` and merges through `pnpm josh
207
191
  followup` without the flag, and the run reports what stopped rather than loosening it.
208
192
 
209
- **Start each child without blocking on it, and poll them all.** That is "A delegated unit that stopped
210
- without reporting" above applied N times, and `pnpm josh run:liveness <N> --output <path> --process
211
- alive` is read **in that child's lane**. **Run `pgrep -laf "(fullrun|run-ship-cli\.ts .*) #<N>$"`
212
- first and pass what it found** — `alive` where the child is there, `none` where it is not, and never
213
- `alive` merely because the child was dispatched. The command line carries no path; the `$` anchor
214
- keeps `#12` from matching a running `#123`.
193
+ **Start each child without blocking on it, and poll them all.** That is `backlogrun-child.md` → "A
194
+ delegated unit that stopped without reporting" applied N times, and `pnpm josh run:liveness <N>
195
+ --output <path> --process alive` is read **in that child's lane**. **Run
196
+ `pgrep -laf "(fullrun|run-ship-cli\.ts .*) #<N>$"` first and pass what it found** — `alive` where the
197
+ child is there, `none` where it is not, and never `alive` merely because the child was dispatched. The
198
+ command line carries no path; the `$` anchor keeps `#12` from matching a running `#123`.
215
199
 
216
200
  **`git switch main && git pull` is the parent's now, not the child's.** No lane can switch to the
217
201
  default branch, so the refresh moves to the primary checkout **before each `lane:open`** — the ref the
@@ -231,37 +215,28 @@ reports it: a pull request that conflicts with its base comes back `mergeStateSt
231
215
  ends that child with a named conflict in about ten seconds.
232
216
 
233
217
  **That child does not stop: it resolves the conflict in its own lane.** It is **not** counted against
234
- the consecutive-failure guard either way — a lost merge race is an ordinary outcome of running many
235
- lanes.
218
+ the consecutive-failure guard either way.
236
219
 
237
- **Nothing is at risk while it resolves, because the work is already committed and pushed.** The pull
238
- request holds the branch, `git merge --abort` puts the tree back, and no step below rewrites a pushed
239
- commit. **The merge direction is `origin/main` into the lane's branch, never a rebase** — a rebase would
240
- rewrite pushed commits and need a force push, which this package denies.
220
+ **The merge direction is `origin/main` into the lane's branch, never a rebase**, and no step below
221
+ rewrites a pushed commit. Rationale: `docs/maintainers/backlogrun-lanes-rationale.md` → "Why a conflict
222
+ is resolved in the lane rather than handed to a person".
241
223
 
242
224
  1. **Resolve in the lane.** `git fetch origin main`, merge `origin/main` into the lane's branch, resolve
243
- in place. Do not close the lane, open another, or switch its branch. **`pnpm josh lane:open` can now
244
- reattach to a pushed branch**, so a lane closed by mistake here is recoverable; staying put is still
245
- what keeps the run clear of the question.
225
+ in place. Do not close the lane, open another, or switch its branch.
246
226
  2. **Re-run the whole gate.** The tree changed, so the green recorded before the conflict is void:
247
227
  `pnpm josh lint:related` and `pnpm josh test:related`, then `pnpm josh gate`.
248
228
  3. **Review the resolution, one round.** Brief it with `pnpm josh review:brief` and run `/code-review`
249
229
  over the resolution diff, then require `pnpm josh review:attest --check` to answer `ok`. That round
250
230
  is a different subject and does not spend one of the two the cap allows — `prompts/review.md` →
251
231
  "Review round cap" carries the exception.
252
- 4. **Conclude the merge and push it, with `pnpm josh git -y`.** Without this step nothing changes on
253
- `origin`: GitHub still returns `mergeStateStatus: DIRTY`, step 5 reports the same conflict, and
254
- condition 3 reads that as a second one and parks the child for good. The distributed
255
- `.claude/settings.json` denies `Bash(git commit*)` and `Bash(git add*)`, so the node script is the
256
- only sanctioned way. **Record the resolution on the Issue in this same step** — a comment naming the
257
- conflicting paths — because condition 3 counts resolutions off the Issue comments, not off memory.
232
+ 4. **Conclude the merge and push it, with `pnpm josh git -y`** — the only sanctioned way; without it
233
+ nothing changes on `origin` and condition 3 parks the child for good. **Record the resolution on the
234
+ Issue in this same step** — a comment naming the conflicting paths — because condition 3 counts
235
+ resolutions off the Issue comments, not off memory.
258
236
  5. **Merge**, by re-running `pnpm josh followup` exactly as before.
259
237
 
260
- **The safeguard is the re-run verification, not who holds the pen.** What is dangerous is unreviewed
261
- code merging onto a branch whose review has converged, and that happens identically whichever hand did
262
- the work — so routing the decision to a person does not address it, while re-running the gate and the
263
- review does. **In this repository routing it to a person does not even resolve it**: the user does not
264
- read code, so a code-level conflict handed over is a deferral rather than a decision.
238
+ **The safeguard is the re-run verification, not who holds the pen** — a conflict the run can resolve is
239
+ not routed to a person.
265
240
 
266
241
  **The run steps back under these four conditions, and under no others. The list is exhaustive and
267
242
  carries no judgement.**
@@ -292,38 +267,27 @@ to abort.
292
267
  | The child **merged** | `pnpm josh lane:close <N>` | `followup` released the hold and the branch is on `main`; nothing in that tree is wanted |
293
268
  | The child was **parked before its commit** | `git -C <dir> stash push -u -m "backlogrun: parked #<N>"`, record it on the Issue, then `pnpm josh lane:close <N>` | The stash is a repository-level ref, so it outlives the work tree — and `lane:close` is a **forced** removal that would otherwise take the work. `epic:next` counts a parked child's lane as released, so a lane left open holds a seat the count believes is free |
294
269
  | The child was **parked after its commit and push** | **Left open**, its directory and held seat recorded on the Issue | The stash step is a **no-op** (tree clean because committed). What `lane:close` would take is the **local branch**, which is the resume path |
295
- | The child hit a **merge conflict** | **Left open** — the resolution happens in it | The child resolves in place ("Conflicts are not predicted"). Closing it would delete the local branch the pull request carries — **`lane:open` does now put that branch back**, so a mistaken close is recoverable. A park under that section's four conditions takes the row above |
270
+ | The child hit a **merge conflict** | **Left open** — the resolution happens in it | The child resolves in place ("Conflicts are not predicted"). A park under that section's four conditions takes the row above |
296
271
  | The child stopped on **`needs-human-review`** | **Left open and untouched** | The uncommitted work *is* the artifact a person has to look at. Name the lane directory in the stop report and in the Telegram |
297
272
  | The child **failed** | Whichever of the two parked rows applies, plus the consecutive-failure count | Same reasoning; only the counter differs. **A merge conflict is not this row** — it takes the row above, and it is not counted |
298
273
  | **`lane:open` failed on the install** | `pnpm josh lane:close <N>`, then park the child | A lane exists that no `pnpm josh …` runs in and the next `lane:open` answers `already-open`. Closing frees the seat; parking is right because the cause is one a person fixes. Carry pnpm's reason into the park note |
299
274
  | The run was **interrupted** | Nothing to do — the work tree survives on disk | The next session's `pnpm josh lane:prune` closes what git no longer has a tree for. **Park that child**, naming the lane and `pnpm josh lane:close <N>` as the way out |
300
275
 
301
- **A committed child's lane is kept because it is the cheapest resume, not because closing it is final.**
302
- `lane:close` follows the work-tree removal with `git branch -D <N>-lane` (the force flag is there so an
303
- unmerged lane branch goes too), and resume is a re-run of `pnpm josh followup`, which reads the branch
304
- **locally** (a `git diff` against the merge base) — so with no local branch the gate cannot be
305
- re-evaluated. Keeping the lane is worth a seat: the tree, the branch and its `node_modules` are still
306
- there, and the resume is one command.
307
-
308
- **What is no longer true is that closing it is unrecoverable.** `pnpm josh lane:open <N>` now attaches
309
- to a `<N>-lane` that exists locally and creates one from `origin/<N>-lane` where only the remote has it,
310
- saying on standard error which it reused. So a lane closed after a push is reopened rather than lost.
311
- **The branch still has to have reached the remote for that to hold** — a child parked before its commit
312
- has nothing on `origin`, which is why that row stashes first and this one does not.
276
+ **A lane closed after a push is reopened by `pnpm josh lane:open <N>`**, which attaches to a local
277
+ `<N>-lane` or creates one from `origin/<N>-lane`, saying on standard error which it reused. **The branch
278
+ still has to have reached the remote for that to hold** — a child parked before its commit has nothing
279
+ on `origin`, which is why that row stashes first and the after-push row does not. Rationale:
280
+ `docs/maintainers/backlogrun-lanes-rationale.md` → "Why a committed child's lane is kept".
313
281
 
314
282
  **Which row a child takes is decided by what `followup` printed, not by reading the situation.** **What
315
283
  it prints is `PR checks failed (merge conflict)`** — that string is what reaches you;
316
284
  `mergeStateStatus: DIRTY` is the internal spelling `git-pr-checks-eval.ts` compares against, never an
317
285
  output line to search for. That string sends the child to resolve the conflict in the lane it is
318
286
  standing in. Do not grep the output for `mergeable_state`: that is the REST field name, normalized away
319
- before anything prints it. **Every after-commit park now keeps its lane, with no exception left in this
320
- table.**
287
+ before anything prints it.
321
288
 
322
- **A kept lane holds its seat, and that is the price rather than an oversight.** Seats are read back out
323
- of the live work trees, so `lane:open` answers `full` rather than handing out ports twice. The optimism
324
- is `epic:next`'s — it counts a parked child as having released its lane — and it costs one child offered
325
- that cannot get a lane, which is visible and recoverable. **Say it in the park comment**: the lane
326
- directory, that its seat is held, and `pnpm josh lane:close <N>` as the way to give it back.
289
+ **A kept lane holds its seat. Say it in the park comment**: the lane directory, that its seat is held,
290
+ and `pnpm josh lane:close <N>` as the way to give it back.
327
291
 
328
292
  **Release the hold on either parked arm, and leave the lane before closing it.** `pnpm josh followup`
329
293
  releases the working-tree hold at the **merge**, so a parked child's is still held: run
@@ -334,13 +298,12 @@ so removing the tree strands it. And `cd` out of the lane **before** `pnpm josh
334
298
  close removes the directory the shell is sitting in.
335
299
 
336
300
  **A `needs-human-review` stop ends the run, and the lanes already in flight are allowed to finish.** No
337
- new lane is opened, but killing units mid-gate would strand as many trees as there are lanes. When the
338
- others have merged or parked, report and stop.
301
+ new lane is opened, and no unit is killed mid-gate. When the others have merged or parked, report and
302
+ stop.
339
303
 
340
304
  ### CI concurrency
341
305
 
342
- Past a GitHub account's concurrency entitlement, jobs **queue** rather than fail, which could cancel out
343
- what the lanes bought; **`JOSH_LANE_LIMIT` lowers the ceiling with no code**. `ci.yml`'s concurrency
344
- group is keyed on `${{ github.ref }}`, so N lanes on N branches are N independent groups and no lane
345
- cancels another's run. Whether queueing bites is read from the multi-day backlog timing report — a
346
- `checks-wait` that grows with the lane count is the queueing.
306
+ **`JOSH_LANE_LIMIT` lowers the ceiling with no code** when CI jobs start to queue. Whether queueing
307
+ bites is read from the multi-day backlog timing report — a `checks-wait` that grows with the lane count
308
+ is the queueing. Rationale: `docs/maintainers/backlogrun-lanes-rationale.md` → "Why CI concurrency is
309
+ capped by the lane limit".