@joshuafolkken/kit 1.1455.0 → 1.1471.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 (211) hide show
  1. package/.claude/skills/dependency-update/SKILL.md +26 -55
  2. package/.claude/skills/epic-commands/SKILL.md +87 -148
  3. package/.claude/skills/epic-commands/epic-bundle.md +45 -77
  4. package/.claude/skills/epic-commands/execution-waves.md +44 -81
  5. package/.claude/skills/verify-ui/SKILL.md +25 -50
  6. package/.claude/skills/workflow-commands/background-commands.md +36 -47
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +72 -85
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +91 -121
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +73 -117
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -9
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +11 -20
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +43 -52
  13. package/.claude/skills/workflow-commands/chain-rule.md +24 -34
  14. package/.claude/skills/workflow-commands/followup.md +57 -76
  15. package/.claude/skills/workflow-commands/issue-comments.md +43 -60
  16. package/.claude/skills/workflow-commands/kickoff.md +65 -74
  17. package/.claude/skills/workflow-commands/observation-filing.md +62 -107
  18. package/.claude/skills/workflow-commands/observation-ledger.md +56 -111
  19. package/.claude/skills/workflow-commands/pre-gate-cut.md +89 -138
  20. package/.claude/skills/workflow-commands/prerequisite.md +10 -15
  21. package/.claude/skills/workflow-commands/progress-watcher.md +21 -26
  22. package/.claude/skills/workflow-commands/retrospective.md +31 -50
  23. package/.claude/skills/workflow-commands/split-assessment.md +34 -58
  24. package/.claude/skills/workflow-commands/working-tree-hold.md +54 -71
  25. package/CLAUDE.md +6 -5
  26. package/dist/CLAUDE.md +6 -5
  27. package/dist/hooks/batch-guard.js +6 -6
  28. package/dist/hooks/{chunk-7WQBPLUN.js → chunk-43ZHT22P.js} +31 -15
  29. package/dist/hooks/{chunk-LIAFCXL7.js → chunk-AS74OU3D.js} +23 -7
  30. package/dist/hooks/{chunk-3C4XRYG6.js → chunk-E7OIKX5Z.js} +17 -12
  31. package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-EUMACUK2.js} +18 -12
  32. package/dist/hooks/{chunk-WNLVGT7E.js → chunk-H5HVWYXK.js} +4 -4
  33. package/dist/hooks/{chunk-HHI6FO3R.js → chunk-IIGFYXKC.js} +345 -240
  34. package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-J47HU5H5.js} +14 -3
  35. package/dist/hooks/{chunk-OLMO72ON.js → chunk-K4EOONYM.js} +1 -1
  36. package/dist/hooks/{chunk-D5QBTOCZ.js → chunk-K4LMBQC2.js} +4 -4
  37. package/dist/hooks/{chunk-TBJFTSUY.js → chunk-KC5AK2GF.js} +8 -6
  38. package/dist/hooks/chunk-SHGPANFB.js +56 -0
  39. package/dist/hooks/{chunk-5H4SSDSD.js → chunk-YHBZAJSV.js} +3 -3
  40. package/dist/hooks/{chunk-HFGWYGUV.js → chunk-YYPI7SJZ.js} +5 -5
  41. package/dist/hooks/codex-hook-adapter.js +13 -13
  42. package/dist/hooks/duplicate-read-guard.js +7 -7
  43. package/dist/hooks/format-edited.js +3 -3
  44. package/dist/hooks/inputs.json +216 -214
  45. package/dist/hooks/investigation-guard.js +7 -7
  46. package/dist/hooks/pretool-guard.js +12 -12
  47. package/dist/hooks/session-lang.js +6 -5
  48. package/dist/hooks/stop-guard.js +18 -14
  49. package/dist/josh.js +5 -6
  50. package/dist/managed-marker/index.js +1 -2
  51. package/dist/version/index.js +15 -12
  52. package/eslint/base.js +2 -1
  53. package/eslint/rules/unicorn.js +16 -0
  54. package/package.json +3 -3
  55. package/prompts/coding-standards.md +12 -22
  56. package/prompts/collaboration-workflow/file-edits.md +26 -44
  57. package/prompts/collaboration-workflow/gh-rest.md +20 -36
  58. package/prompts/collaboration-workflow/glossary.md +5 -7
  59. package/prompts/collaboration-workflow/issue-citation.md +7 -13
  60. package/prompts/collaboration-workflow/issue-template.md +29 -29
  61. package/prompts/collaboration-workflow/operating-rules.md +35 -44
  62. package/prompts/collaboration-workflow/output-bounds.md +15 -15
  63. package/prompts/collaboration-workflow/overview.md +14 -19
  64. package/prompts/collaboration-workflow/plan-comment.md +13 -13
  65. package/prompts/collaboration-workflow/principles.md +50 -53
  66. package/prompts/collaboration-workflow/proposal-request.md +1 -9
  67. package/prompts/collaboration-workflow/report-format.md +32 -49
  68. package/prompts/collaboration-workflow/residency.md +10 -10
  69. package/prompts/collaboration-workflow/rule-delivery.md +9 -9
  70. package/prompts/collaboration-workflow/shell-body.md +10 -20
  71. package/prompts/collaboration-workflow/turn-batching.md +8 -14
  72. package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
  73. package/prompts/collaboration-workflow/wip-cap.md +24 -30
  74. package/prompts/collaboration-workflow.md +24 -25
  75. package/prompts/refactoring.md +33 -43
  76. package/prompts/review-rubric.md +84 -129
  77. package/prompts/review.md +111 -141
  78. package/prompts/sonar-hotspot-handling.md +22 -50
  79. package/prompts/testing-guide.md +48 -99
  80. package/scripts/adopt/adopt-toolkits.ts +2 -4
  81. package/scripts/agent/agent-exit-record.ts +23 -1
  82. package/scripts/agent/codex-agent-argv.ts +7 -2
  83. package/scripts/backlog/backlog-budget-cli.ts +1 -2
  84. package/scripts/backlog/backlog-drive-carried.ts +26 -0
  85. package/scripts/backlog/backlog-drive-cli.ts +10 -5
  86. package/scripts/backlog/backlog-drive-named.ts +2 -4
  87. package/scripts/backlog/backlog-drive-restore.ts +19 -1
  88. package/scripts/backlog/backlog-drive-seats.ts +27 -0
  89. package/scripts/backlog/backlog-drive.ts +11 -6
  90. package/scripts/backlog/backlog-ready.ts +13 -6
  91. package/scripts/behavior/behavior-rules.ts +3 -2
  92. package/scripts/cost-runtime/codex-usage.ts +4 -2
  93. package/scripts/cost-runtime/cost-cli.ts +1 -2
  94. package/scripts/delegation/investigation-guard.ts +1 -1
  95. package/scripts/delegation/investigation-reads.ts +1 -1
  96. package/scripts/discovery/repo-discovery.ts +4 -2
  97. package/scripts/discovery/repo-origin.ts +2 -4
  98. package/scripts/doctor/doctor-logic.ts +6 -3
  99. package/scripts/document/document-byte-budget.ts +27 -27
  100. package/scripts/document/document-byte-check.ts +1 -2
  101. package/scripts/document/entry-read-budget.ts +2 -2
  102. package/scripts/epic/epic-audit-rationale.ts +8 -2
  103. package/scripts/epic/epic-bundle-cli.ts +1 -2
  104. package/scripts/epic/epic-chains.ts +6 -2
  105. package/scripts/epic/epic-close-comment.ts +1 -2
  106. package/scripts/gate/verification-gate.ts +7 -2
  107. package/scripts/gh/git-pr.ts +6 -2
  108. package/scripts/git/git-common-directory.ts +3 -3
  109. package/scripts/git/git-diff-reads.ts +16 -9
  110. package/scripts/git/git-execa-error.ts +1 -2
  111. package/scripts/hooks/format-edited-file.ts +10 -4
  112. package/scripts/hooks/pre-push-unit.ts +15 -4
  113. package/scripts/hooks/pretool-guard.ts +23 -29
  114. package/scripts/hooks/step-zero-notice.ts +3 -2
  115. package/scripts/init/hook-command-rewrite.ts +6 -2
  116. package/scripts/init/init-logic-deploy-vps.ts +4 -2
  117. package/scripts/issue/behavior-change-lint.ts +12 -6
  118. package/scripts/issue/issue-scout-cli.ts +3 -2
  119. package/scripts/josh/file-map-stamp.ts +1 -3
  120. package/scripts/josh/hook-decision.ts +17 -2
  121. package/scripts/lane/lane-await.ts +3 -2
  122. package/scripts/lane/lane-child-invocation.ts +14 -1
  123. package/scripts/lane/lane-guard-policy.ts +4 -4
  124. package/scripts/lane/lane-ledger.ts +9 -0
  125. package/scripts/lane/lane-load.ts +9 -2
  126. package/scripts/lane/lane-measure-cli.ts +2 -0
  127. package/scripts/lane/lane-parked.ts +18 -0
  128. package/scripts/lane/lane-process-load.ts +125 -0
  129. package/scripts/lane/lane-process-stats.ts +93 -0
  130. package/scripts/lane/lane-registry.ts +8 -3
  131. package/scripts/lane/lane-session-broker.ts +29 -5
  132. package/scripts/lane/lane-spent.ts +14 -3
  133. package/scripts/lane/lane-stats.ts +1 -0
  134. package/scripts/managed-marker/managed-marker-logic.ts +1 -2
  135. package/scripts/overrides/lockfile-overrides.ts +1 -3
  136. package/scripts/propagate/propagate.ts +1 -2
  137. package/scripts/release/release-progress.ts +3 -2
  138. package/scripts/report/report-lint.ts +7 -4
  139. package/scripts/review/review-record-cli.ts +6 -2
  140. package/scripts/rules/destructive-command.ts +6 -9
  141. package/scripts/rules/direct-filing.ts +3 -7
  142. package/scripts/rules/early-heartbeat.ts +7 -18
  143. package/scripts/rules/file-body.ts +6 -13
  144. package/scripts/rules/filing-cap.ts +3 -6
  145. package/scripts/rules/git-force.ts +5 -9
  146. package/scripts/rules/implementation-cut.ts +37 -35
  147. package/scripts/rules/index-guard.ts +4 -7
  148. package/scripts/rules/issue-comments.ts +5 -15
  149. package/scripts/rules/josh-git-bare.ts +3 -6
  150. package/scripts/rules/lane-background.ts +10 -21
  151. package/scripts/rules/lane-carry-conflict.ts +7 -17
  152. package/scripts/rules/lane-interactive-ask.ts +6 -11
  153. package/scripts/rules/lane-park.ts +5 -13
  154. package/scripts/rules/lane-split-park.ts +2 -7
  155. package/scripts/rules/lane-switch-main.ts +9 -12
  156. package/scripts/rules/oracle-consulted.ts +4 -5
  157. package/scripts/rules/piped-verification.ts +81 -31
  158. package/scripts/rules/poll-loop.ts +7 -17
  159. package/scripts/rules/pre-gate-cut.ts +7 -21
  160. package/scripts/rules/prior-comment-read.ts +7 -5
  161. package/scripts/rules/protected-files.ts +5 -9
  162. package/scripts/rules/raw-field-body.ts +3 -7
  163. package/scripts/rules/rule-body-guard.ts +4 -11
  164. package/scripts/rules/run-tail-rule.ts +6 -22
  165. package/scripts/rules/shell-body-trigger.ts +4 -6
  166. package/scripts/rules/shell-segments.ts +7 -0
  167. package/scripts/rules/stop-rules.ts +16 -29
  168. package/scripts/rules/test-declared-commit.ts +4 -10
  169. package/scripts/rules/third-party-write.ts +5 -10
  170. package/scripts/rules/worktree-guard.ts +5 -12
  171. package/scripts/run/board/run-board-github.ts +11 -3
  172. package/scripts/run/board/run-board-idle.ts +31 -0
  173. package/scripts/run/board/run-board-read.ts +9 -12
  174. package/scripts/run/board/run-board-reason.ts +7 -3
  175. package/scripts/run/board/run-board-state.ts +4 -0
  176. package/scripts/run/board/run-board-status.ts +18 -3
  177. package/scripts/run/board/run-board-thanks.ts +7 -2
  178. package/scripts/run/board/run-board-tick.ts +32 -7
  179. package/scripts/run/carry/run-carry-args.ts +1 -3
  180. package/scripts/run/carry/run-carry-conversation.ts +7 -2
  181. package/scripts/run/cut/run-cut-cli.ts +15 -20
  182. package/scripts/run/cut/run-cut-decline.ts +72 -0
  183. package/scripts/run/cut/run-cut-ready.ts +74 -0
  184. package/scripts/run/cut/run-cut-report.ts +13 -0
  185. package/scripts/run/cut/run-cut.ts +10 -1
  186. package/scripts/run/hold/run-hold-cli.ts +14 -3
  187. package/scripts/run/hold/run-hold.ts +6 -3
  188. package/scripts/run/merge/run-merge-cli.ts +30 -7
  189. package/scripts/run/merge/run-merge-ship.ts +59 -1
  190. package/scripts/run/merge/run-merge.ts +3 -2
  191. package/scripts/run/run-ending.ts +31 -3
  192. package/scripts/run/run-parent-cut-hook.ts +5 -10
  193. package/scripts/run/run-preflight.ts +1 -2
  194. package/scripts/run/run-stop-notify.ts +1 -2
  195. package/scripts/run/run-watcher-guard.ts +6 -3
  196. package/scripts/run/ship/run-ship-review-steps.ts +13 -4
  197. package/scripts/run/ship/run-ship-review.ts +15 -2
  198. package/scripts/run/tidy/run-tidy-cli.ts +3 -3
  199. package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
  200. package/scripts/safe-chain/project-config.ts +7 -2
  201. package/scripts/sync/sync-hook-safety.ts +8 -2
  202. package/scripts/test/unit-worker-share.ts +3 -2
  203. package/scripts/time-runtime/time-batch-guard.ts +3 -3
  204. package/scripts/time-runtime/time-hook-transcript.ts +3 -2
  205. package/scripts/time-runtime/time-shell.ts +10 -0
  206. package/scripts/version/effective-upstream.ts +3 -4
  207. package/scripts/version/kit-version-config.ts +8 -2
  208. package/scripts/version/latest-corepack.ts +1 -2
  209. package/scripts/version/release-hold.ts +11 -4
  210. package/scripts/version/version-commands.ts +12 -4
  211. package/dist/hooks/chunk-UT7YC3N2.js +0 -56
@@ -103,16 +103,11 @@ function writes_third_party(
103
103
  // to do instead. The command that computes the party is handed over, because the whole point of this
104
104
  // Issue is that the test is computed rather than judged.
105
105
  const THIRD_PARTY_WRITE_REASON =
106
- '⛔ third-party write: this `gh api` call writes to a repository whose owner is not this session — ' +
107
- 'a tracker we do not own is Tier C, and every write there (Issue, comment, PR, Discussion, review) ' +
108
- 'needs explicit current-turn user instruction; typing the target, or holding a correct diagnosis, ' +
109
- 'is not that instruction (`CLAUDE.md` → "Third-party repositories are Tier C", ' +
110
- '`prompts/collaboration-workflow/upstream-interrupt.md`). Confirm the party with ' +
111
- '`pnpm josh repo:party <owner/repo>` — owner equality, computed not judged. A read (`-X GET`, or no ' +
112
- 'field flag) passes untouched; only a write (`-f`/`-F`/`--field`/`--raw-field`/`--input`, or ' +
113
- '`-X POST|PATCH|PUT|DELETE`) is refused. Record the finding on our own Issue and draft the artifact ' +
114
- 'for a person to post — do not publish it yourself. **This rule fires on every occurrence, not once ' +
115
- 'per run.**'
106
+ '⛔ third-party write: this `gh api` call writes to a repository whose owner is not this session, ' +
107
+ 'which is Tier C and needs explicit current-turn user instruction. Check with ' +
108
+ '`pnpm josh repo:party <owner/repo>`, then record the finding on our own Issue and draft the ' +
109
+ 'artifact for a person to post. This rule fires on every occurrence ' +
110
+ '(`prompts/collaboration-workflow/upstream-interrupt.md`).'
116
111
 
117
112
  // `decide` returns true so it refuses every occurrence; it declares no `keeps` — not writing to
118
113
  // another owner's tracker is the absence of a call, not a call, so the row is reported unmeasured
@@ -124,18 +124,11 @@ function is_unauthorized_worktree_change(command: string): boolean {
124
124
  // and — for a stash — the message form that is allowed. `pnpm josh git` and `pnpm josh stash:pop` are
125
125
  // invisible to this trigger, so a run that uses them is never refused.
126
126
  const WORKTREE_MUTATION_REASON =
127
- '⛔ unauthorized working-tree change: `git checkout -- <path>`, `git restore <path>`, a forced ' +
128
- '`git clean` and an unauthorized `git stash` discard work or mutate the repository-wide stash every work tree ' +
129
- 'shares, and none is on the `.claude/settings.json` deny list (`operating-rules.md`). Do not run ' +
130
- 'them on your own judgement. A commit goes through `pnpm josh git`; a stash pop goes through `pnpm ' +
131
- 'josh stash:pop "<message>"`, never a positional `git stash pop` a shared stack lets another lane ' +
132
- 'divert. A stash push is authorized only inside a documented flow and only with a findable message ' +
133
- '— `git stash push -u -m "<message>"` — which this row allows; a bare `git stash`, a message-less ' +
134
- 'push, or `pop` / `apply` / `drop` is refused. A plan that restores a handed-over stash with `git ' +
135
- 'stash apply` / `git stash pop` is read as `pnpm josh stash:pop "<message>"`: apply it that way, ' +
136
- 'continue, and record the substitution on the Issue — Tier A, never a `needs-decision` park ' +
137
- 'unless the plan says why the owner keeps the stash: then stop and confirm (`operating-rules.md` ' +
138
- '→ "no-self-staging"). **This rule fires on every occurrence, not once per run.**'
127
+ '⛔ unauthorized working-tree change: `git checkout --`, `git restore`, `git clean -f`, ' +
128
+ '`git stash`. A documented flow stashes with `git stash push -u -m "<message>"`; a handed-over ' +
129
+ 'stash is `pnpm josh stash:pop "<message>"`; record the substitution on the Issue (Tier A) unless ' +
130
+ 'the plan says why the owner keeps the stash: then stop and confirm. It fires on every occurrence ' +
131
+ '(`operating-rules.md`).'
139
132
 
140
133
  const ROW = {
141
134
  id: 'worktree-mutation',
@@ -2,6 +2,7 @@ import { run_progress } from '#scripts/run/progress/run-progress'
2
2
  import { run_progress_cli } from '#scripts/run/progress/run-progress-cli'
3
3
  import type { ClosedAnswer } from './run-board-closed'
4
4
  import { run_board_fetch, type Fetch } from './run-board-fetch'
5
+ import { run_board_idle } from './run-board-idle'
5
6
  import { run_board_layout, type BoardPlan } from './run-board-layout'
6
7
  import type { LocalRead } from './run-board-read'
7
8
  import { run_board_state, type BoardPorts, type BoardState, type Pace } from './run-board-state'
@@ -72,8 +73,15 @@ function fold_plan(state: BoardState): BoardState {
72
73
  }
73
74
  }
74
75
 
75
- function statuses_for(plan: BoardPlan, local: LocalRead): ReadonlyMap<number, ItemStatus> {
76
- return run_board_status.statuses_of(local.events, local.lanes, run_board_layout.numbers_of(plan))
76
+ function statuses_for(
77
+ state: BoardState,
78
+ plan: BoardPlan,
79
+ local: LocalRead,
80
+ ): ReadonlyMap<number, ItemStatus> {
81
+ const in_run = run_board_layout.numbers_of(plan)
82
+ const stopped = run_board_idle.stopped_of(state.idle_since, state.read_ms)
83
+
84
+ return run_board_status.statuses_of(local.events, local.lanes, in_run, stopped)
77
85
  }
78
86
 
79
87
  // The children the run touched that the open listing no longer holds and the board has not read yet.
@@ -83,7 +91,7 @@ function unread_closed(state: BoardState, local: LocalRead): Array<number> {
83
91
 
84
92
  if (open === undefined || state.plan === undefined) return []
85
93
 
86
- const touched = [...statuses_for(state.plan, local).keys()]
94
+ const touched = [...statuses_for(state, state.plan, local).keys()]
87
95
 
88
96
  return touched.filter((issue) => !open.has(issue) && !state.closed.has(issue))
89
97
  }
@@ -0,0 +1,31 @@
1
+ import { lane_await } from '#scripts/lane/lane-await'
2
+
3
+ // A lane is drawn stopped only once it has read idle for `lane_await.RECONFIRM_MS`: a boundary cut drops
4
+ // the child's process for a moment before its resume takes over, and one bare probe cannot tell that
5
+ // handoff from a child that died — `lane:await` re-confirms a disappearance over the same window. A live
6
+ // board carries when each lane was first read idle across its reads; a single frame has no earlier read,
7
+ // so it reads the lanes again after the window (`run-board-tick.ts`).
8
+
9
+ /** When each lane the last read found idle was first read so. */
10
+ type IdleSince = ReadonlyMap<string, number>
11
+
12
+ const NO_IDLE: IdleSince = new Map()
13
+
14
+ // The lanes read idle now, each keeping the moment it was first read so; a lane read working drops out.
15
+ function since_of(before: IdleSince, idle: ReadonlyArray<string>, now_ms: number): IdleSince {
16
+ return new Map(idle.map((lane) => [lane, before.get(lane) ?? now_ms]))
17
+ }
18
+
19
+ // The lanes idle for the whole window as of the read at `read_ms`; none before any read.
20
+ function stopped_of(since: IdleSince, read_ms: number | undefined): Array<string> {
21
+ if (read_ms === undefined) return []
22
+
23
+ const lasted = [...since].filter(([, at]) => read_ms - at >= lane_await.RECONFIRM_MS)
24
+
25
+ return lasted.map(([lane]) => lane)
26
+ }
27
+
28
+ const run_board_idle = { NO_IDLE, since_of, stopped_of }
29
+
30
+ export type { IdleSince }
31
+ export { run_board_idle }
@@ -1,7 +1,8 @@
1
1
  import type { NamedPlan } from '#scripts/backlog/backlog-plan'
2
+ import { backlog_ready } from '#scripts/backlog/backlog-ready'
2
3
  import { backlogrun_launch } from '#scripts/backlog/backlogrun-launch'
3
- import { lane_capacity } from '#scripts/lane/lane-capacity'
4
4
  import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
5
+ import { lane_spent } from '#scripts/lane/lane-spent'
5
6
  import { run_carry } from '#scripts/run/carry/run-carry'
6
7
  import { run_carry_ended } from '#scripts/run/carry/run-carry-ended'
7
8
  import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
@@ -33,21 +34,16 @@ interface LocalRead {
33
34
  is_stranded: boolean
34
35
  // The newest operation of each session of the run, read off the end of its log.
35
36
  operations?: SessionOperations | undefined
36
- // How many more lanes the limit allows beside the live ones; `undefined` where the limit could not
37
- // be read, so nothing is said about it.
37
+ // The open lanes no child is working in at this read (`lane-spent.ts`); one is drawn stopped once it
38
+ // has stayed so (`run-board-idle.ts`).
39
+ idle_lanes?: ReadonlyArray<string> | undefined
40
+ // How many more lanes the limit allows beside the live ones, as `lane:limit` counts them;
41
+ // `undefined` where the limit could not be read, so nothing is said about it.
38
42
  free_lanes?: number | undefined
39
43
  // How long each phase usually takes, from the repository's lane ledger.
40
44
  usual?: UsualTimes | undefined
41
45
  }
42
46
 
43
- async function free_lanes_of(lanes: ReadonlyArray<LaneInfo>): Promise<number | undefined> {
44
- const choice = await lane_capacity.lane_limit()
45
-
46
- if (choice.kind !== 'limit') return undefined
47
-
48
- return lane_capacity.free_lanes(choice.limit, lane_registry.live_count(lanes))
49
- }
50
-
51
47
  const NO_SPAN: SpanHere = { span: undefined, resume: undefined, transcript: undefined }
52
48
 
53
49
  interface SpanHere {
@@ -104,7 +100,8 @@ async function read_local(): Promise<LocalRead | undefined> {
104
100
  resume,
105
101
  is_stranded: run_stranded_detect.is_stranded_at(repository),
106
102
  operations: run_board_operation_read.read(sources_of(repository, lanes, transcript)),
107
- free_lanes: await free_lanes_of(lanes),
103
+ idle_lanes: lane_spent.idle_lanes(lanes),
104
+ free_lanes: await backlog_ready.free_lanes_beside(lanes),
108
105
  usual: run_board_usual.read(repository),
109
106
  }
110
107
  }
@@ -5,6 +5,7 @@ import { run_event_setup } from '#scripts/run/event/run-event-setup'
5
5
  import type { RunEvent } from '#scripts/run/event/run-event-stream'
6
6
  import { run_board_layout, type Overlay, type ReasonOf, type RowReason } from './run-board-layout'
7
7
  import { run_board_setup } from './run-board-setup'
8
+ import type { ItemStatus } from './run-board-status'
8
9
 
9
10
  // Why each waiting row waits — pure, so the words are tested apart from the screen. A row whose lane is
10
11
  // being opened says the stage that lane is at; any other says what `backlog:next` would answer for it,
@@ -63,10 +64,13 @@ function is_untriaged_candidate(child: EpicChild, read: ReasonRead): boolean {
63
64
  return !epic_triage.is_triaged(child) && !run_board_layout.is_blocked(child, read)
64
65
  }
65
66
 
67
+ // A child whose lane stands with no one in it still holds the paths it declared, as its open lane does.
68
+ function holds_paths(status: ItemStatus): boolean {
69
+ return status.state === 'running' || status.lane !== undefined
70
+ }
71
+
66
72
  function running_of(read: ReasonRead): Array<number> {
67
- return [...read.statuses]
68
- .filter(([, status]) => status.state === 'running')
69
- .map(([issue]) => issue)
73
+ return [...read.statuses].filter(([, status]) => holds_paths(status)).map(([issue]) => issue)
70
74
  }
71
75
 
72
76
  function pool_of(read: ReasonRead): Pool {
@@ -4,6 +4,7 @@ import type { ClosedAnswer, ClosedIssue } from './run-board-closed'
4
4
  import type { Fetch } from './run-board-fetch'
5
5
  import type { TerminalSize } from './run-board-fit'
6
6
  import type { BoardForm } from './run-board-header'
7
+ import { run_board_idle, type IdleSince } from './run-board-idle'
7
8
  import type { BoardPlan } from './run-board-layout'
8
9
  import type { Link } from './run-board-link'
9
10
  import type { MachineGauges, MachineMark } from './run-board-machine'
@@ -56,6 +57,8 @@ interface BoardState {
56
57
  // The last local read, drawn until the next is due; `undefined` when no run had started.
57
58
  local: LocalRead | undefined
58
59
  read_ms: number | undefined
60
+ // When each lane the reads found idle was first read so (`run-board-idle.ts`).
61
+ idle_since: IdleSince
59
62
  // The start of the run the plan state was built for, so a run that begins draws nothing of the last.
60
63
  run_started_ms: number | undefined
61
64
  plan: BoardPlan | undefined
@@ -91,6 +94,7 @@ interface BoardState {
91
94
  const FRESH_STATE: BoardState = {
92
95
  local: undefined,
93
96
  read_ms: undefined,
97
+ idle_since: run_board_idle.NO_IDLE,
94
98
  run_started_ms: undefined,
95
99
  plan: undefined,
96
100
  plan_fetch: undefined,
@@ -66,6 +66,9 @@ const ENDING_STATES: Readonly<Partial<Record<string, ItemState>>> = {
66
66
  const CONTINUING_KINDS: ReadonlySet<string> = new Set([KIND.CUT, KIND.RESUME])
67
67
  // A person's restart starts the child over in a fresh session, so its track starts over too.
68
68
  const LAUNCH_KINDS: ReadonlySet<string> = new Set([KIND.CHILD_LAUNCH, KIND.RESTART])
69
+ // What the GitHub read may still settle: a running child, and one whose lane has no child left in it —
70
+ // a child that merged and then died is drawn merged, not stopped.
71
+ const UNSETTLED_STATES: ReadonlySet<ItemState> = new Set(['running', 'stopped'])
69
72
 
70
73
  function moment(event: RunEvent): number {
71
74
  return Date.parse(event.at)
@@ -175,13 +178,25 @@ function is_running_lane(
175
178
  return in_run.has(issue) && !run_board_setup.is_lane_pending(events, issue)
176
179
  }
177
180
 
181
+ // A lane no child is working in is stopped whatever the stream last said: a child that died writes no
182
+ // ending, so its row would otherwise run — and its phase time grow — for as long as the lane stays.
183
+ function lane_status(status: ItemStatus | undefined, lane: string, is_idle: boolean): ItemStatus {
184
+ const held = { lane: `lane ${lane}` }
185
+
186
+ return is_idle
187
+ ? { ...settled_as(status, 'stopped'), ...held }
188
+ : { state: 'running', ...status, ...held }
189
+ }
190
+
178
191
  // Every child the run has touched, with the lane each running child holds. A lane the stream never saw
179
192
  // launched — a run restored after the stream rolled — still reads as running while its issue is in the
180
- // run's plan (`in_run`); one the run is still setting up for stays waiting until its launch.
193
+ // run's plan (`in_run`); one the run is still setting up for stays waiting until its launch. A lane in
194
+ // `idle` has no child working in it (`lane-spent.ts`).
181
195
  function statuses_of(
182
196
  events: ReadonlyArray<RunEvent>,
183
197
  lanes: ReadonlyArray<string>,
184
198
  in_run: ReadonlySet<number>,
199
+ idle: ReadonlyArray<string> = [],
185
200
  ): ReadonlyMap<number, ItemStatus> {
186
201
  const statuses = fold_events(events)
187
202
 
@@ -190,7 +205,7 @@ function statuses_of(
190
205
  const status = statuses.get(issue)
191
206
 
192
207
  if (is_running_lane(issue, status, events, in_run)) {
193
- statuses.set(issue, { state: 'running', ...status, lane: `lane ${lane}` })
208
+ statuses.set(issue, lane_status(status, lane, idle.includes(lane)))
194
209
  }
195
210
  }
196
211
 
@@ -260,7 +275,7 @@ function closed_status(
260
275
  }
261
276
 
262
277
  function settled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus {
263
- if (status.state !== 'running') return status
278
+ if (!UNSETTLED_STATES.has(status.state)) return status
264
279
 
265
280
  const from_closed = closed_status(status, read.closed.get(issue))
266
281
 
@@ -17,8 +17,13 @@ type ThanksState = Pick<BoardState, 'thanks' | 'thanks_fetch' | 'thanks_attempte
17
17
  function launch_thanks(state: BoardState, ports: BoardPorts, now_ms: number): BoardState {
18
18
  const { read_thanks } = ports
19
19
 
20
- if (read_thanks === undefined || state.thanks_fetch !== undefined) return state
21
- if (!is_due(state.thanks_attempted_ms, TTL_MS, now_ms)) return state
20
+ if (
21
+ read_thanks === undefined ||
22
+ state.thanks_fetch !== undefined ||
23
+ !is_due(state.thanks_attempted_ms, TTL_MS, now_ms)
24
+ ) {
25
+ return state
26
+ }
22
27
 
23
28
  const thanks_fetch: Fetch<Thanks> = run_board_fetch.launch(read_thanks, state.thanks)
24
29
 
@@ -1,7 +1,9 @@
1
+ import { lane_await } from '#scripts/lane/lane-await'
1
2
  import { run_progress } from '#scripts/run/progress/run-progress'
2
3
  import type { ClosedIssue } from './run-board-closed'
3
4
  import { run_board_github } from './run-board-github'
4
5
  import { run_board_header, type BoardHeader } from './run-board-header'
6
+ import { run_board_idle } from './run-board-idle'
5
7
  import { run_board_labels } from './run-board-labels'
6
8
  import { run_board_layout, type BoardLayout, type BoardPlan } from './run-board-layout'
7
9
  import { run_board_link } from './run-board-link'
@@ -34,10 +36,30 @@ const { FRESH_STATE, is_due } = run_board_state
34
36
  const { PLAN_RETRY_MS, gather_github, statuses_for } = run_board_github
35
37
  const { gather_thanks } = run_board_thanks
36
38
 
37
- async function reread(state: BoardState, ports: BoardPorts, now_ms: number): Promise<BoardState> {
39
+ async function read_at(state: BoardState, ports: BoardPorts, now_ms: number): Promise<BoardState> {
40
+ const local = await ports.read_local()
41
+ const idle_since = run_board_idle.since_of(state.idle_since, local?.idle_lanes ?? [], now_ms)
42
+
43
+ return { ...state, local, read_ms: now_ms, idle_since }
44
+ }
45
+
46
+ // A single frame has no earlier read to confirm an idle lane against (`run-board-idle.ts`).
47
+ function is_unconfirmed(before: BoardState, read: BoardState, pace: Pace): boolean {
48
+ return pace === 'settled' && before.read_ms === undefined && read.idle_since.size > 0
49
+ }
50
+
51
+ async function reread(state: BoardState, ports: BoardPorts, pace: Pace): Promise<BoardState> {
52
+ const now_ms = ports.now()
53
+
38
54
  if (!is_due(state.read_ms, LOCAL_READ_MS, now_ms)) return state
39
55
 
40
- return { ...state, local: await ports.read_local(), read_ms: now_ms }
56
+ const read = await read_at(state, ports, now_ms)
57
+
58
+ if (!is_unconfirmed(state, read, pace)) return read
59
+
60
+ await ports.sleep(lane_await.RECONFIRM_MS)
61
+
62
+ return await read_at(read, ports, ports.now())
41
63
  }
42
64
 
43
65
  // Each lane's usage, read on the machine's second; a reading that failed draws no column.
@@ -69,9 +91,9 @@ async function resample(state: BoardState, ports: BoardPorts, now_ms: number): P
69
91
  function state_for(state: BoardState, local: LocalRead): BoardState {
70
92
  if (state.run_started_ms === local.started_ms) return state
71
93
 
72
- const { local: kept, read_ms } = state
94
+ const { local: kept, read_ms, idle_since } = state
73
95
 
74
- return { ...FRESH_STATE, local: kept, read_ms, run_started_ms: local.started_ms }
96
+ return { ...FRESH_STATE, local: kept, read_ms, idle_since, run_started_ms: local.started_ms }
75
97
  }
76
98
 
77
99
  // The plan with a closed child's title and labels under the listing's own.
@@ -108,7 +130,7 @@ function planned_layout(state: BoardState, plan: BoardPlan, local: LocalRead): B
108
130
  closed,
109
131
  labels: plan.labels,
110
132
  }
111
- const statuses = run_board_status.settle_closed(statuses_for(plan, local), read)
133
+ const statuses = run_board_status.settle_closed(statuses_for(state, plan, local), read)
112
134
  const reason_of = run_board_reason.reader({
113
135
  plan,
114
136
  statuses,
@@ -125,8 +147,10 @@ function planned_layout(state: BoardState, plan: BoardPlan, local: LocalRead): B
125
147
  function layout_for(state: BoardState, local: LocalRead): BoardLayout {
126
148
  if (state.plan !== undefined) return planned_layout(state, state.plan, local)
127
149
 
150
+ const stopped = run_board_idle.stopped_of(state.idle_since, state.read_ms)
151
+
128
152
  return run_board_layout.local_layout_of(
129
- run_board_status.statuses_of(local.events, local.lanes, new Set()),
153
+ run_board_status.statuses_of(local.events, local.lanes, new Set(), stopped),
130
154
  )
131
155
  }
132
156
 
@@ -220,8 +244,9 @@ async function tick(
220
244
  ports: BoardPorts,
221
245
  pace: Pace = 'settled',
222
246
  ): Promise<BoardState> {
247
+ const read = await reread(state, ports, pace)
248
+ // Taken after the read, which a single frame may have waited on to confirm an idle lane.
223
249
  const now_ms = ports.now()
224
- const read = await reread(state, ports, now_ms)
225
250
  const { local } = read
226
251
 
227
252
  if (local === undefined) {
@@ -269,9 +269,7 @@ function to_named_request(values: ParsedValues, owner: CarryOwner): Request | un
269
269
  }
270
270
 
271
271
  function to_request(values: ParsedValues): Request | undefined {
272
- if (group_count(values) > ONE_GROUP) return undefined
273
-
274
- if (!is_summary_paired(values)) return undefined
272
+ if (group_count(values) > ONE_GROUP || !is_summary_paired(values)) return undefined
275
273
 
276
274
  const owner = to_owner(values.owner)
277
275
 
@@ -76,8 +76,13 @@ function is_same_conversation(carry: RunCarry, owner: CarryOwner): boolean {
76
76
  // a new one, so the record goes on naming a live process once the conversation falls quiet. A record a
77
77
  // cut handed off is the successor's to take, never the cutting conversation's to take back.
78
78
  function reclaim_owner(carry: RunCarry, owner: CarryOwner): RunCarry {
79
- if (carry.is_handed_off === true || !is_same_conversation(carry, owner)) return carry
80
- if (owner.pid === carry.owner_pid) return carry
79
+ if (
80
+ carry.is_handed_off === true ||
81
+ !is_same_conversation(carry, owner) ||
82
+ owner.pid === carry.owner_pid
83
+ ) {
84
+ return carry
85
+ }
81
86
 
82
87
  return { ...carry, owner_pid: owner.pid, owner_start: owner.start }
83
88
  }
@@ -3,7 +3,6 @@ import { fileURLToPath } from 'node:url'
3
3
  import { agent_session_role } from '#scripts/agent/agent-session-role'
4
4
  import { cost_cli } from '#scripts/cost-runtime/cost-cli'
5
5
  import { cost_verdict } from '#scripts/cost-runtime/cost-verdict'
6
- import { git_command } from '#scripts/git/git-command'
7
6
  import { issue_cite } from '#scripts/issue/issue-cite'
8
7
  import { session_cite } from '#scripts/issue/session-cite'
9
8
  import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
@@ -12,9 +11,11 @@ import { openai_lane_supervisor } from '#scripts/lane/openai-lane-supervisor'
12
11
  import { run_event_stream } from '#scripts/run/event/run-event-stream'
13
12
  import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
14
13
  import { run_cli_fault } from '#scripts/run/run-cli-fault'
15
- import { run_cut, type CutState, type RunCut } from './run-cut'
14
+ import { run_cut, type RunCut } from './run-cut'
16
15
  import { run_cut_args, type Request } from './run-cut-args'
16
+ import { run_cut_decline } from './run-cut-decline'
17
17
  import { run_cut_handoff } from './run-cut-handoff'
18
+ import { run_cut_ready } from './run-cut-ready'
18
19
  import { run_cut_report } from './run-cut-report'
19
20
 
20
21
  // `josh run:cut` — the record that lets a lane child end its process before the gate and a fresh one
@@ -57,6 +58,7 @@ const {
57
58
  report_busy,
58
59
  report_fault,
59
60
  report_handed_off,
61
+ report_held_cut,
60
62
  report_incomplete,
61
63
  report_over,
62
64
  report_reviewer,
@@ -125,11 +127,8 @@ function relaunch(target: string, lane: LaneInfo, phase: string): number {
125
127
  return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
126
128
  }
127
129
 
128
- async function is_lane_branch(state: CutState): Promise<boolean> {
129
- const default_branch = await git_command.get_default_branch()
130
-
131
- return state.branch !== default_branch && state.is_dirty
132
- }
130
+ const { is_lane_branch } = run_cut_ready
131
+ const { declined } = run_cut_decline
133
132
 
134
133
  // An expired record is a fresh process that never started; it is cleared so the next cut can begin
135
134
  // rather than being wedged behind a stale file the exclusive create would refuse forever.
@@ -204,16 +203,6 @@ async function finish_cut(target: string, lane: LaneInfo, request: CutRequest):
204
203
  : relaunch(target, lane, request.phase)
205
204
  }
206
205
 
207
- // The cut outside a lane is taken, and nothing is relaunched: the session that took it is the one a
208
- // person is watching, so the run is handed on the way `fullrun`'s entry `over` stop hands it on.
209
- function report_held_cut(issue: string): number {
210
- console.error(
211
- `The cut is recorded for ${session_cite.issue(issue)} outside a lane, so nothing was relaunched. Keep the hold, send a \`confirmation\` Telegram whose body names the resume command \`fullrun ${issue_cite.plain(issue)}\`, and end the turn; the fresh session's \`pnpm josh run:cut --resume ${issue}\` answers \`resume-impl\` with the handoff.`,
212
- )
213
-
214
- return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
215
- }
216
-
217
206
  // **A `fullrun` held in its own checkout takes the implementation cut too**, so a run a person started
218
207
  // has a bound on its context mid-implementation. The hold naming this issue is what marks the run — a
219
208
  // tree nobody holds for it, and the pre-gate phase (which a held run's gate reaches in the same
@@ -228,8 +217,11 @@ async function cut_outside_lane(
228
217
 
229
218
  const state = await run_cut.current_state()
230
219
 
231
- if (state.held_issue !== request.issue) return report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE)
232
- if (!(await is_lane_branch(state))) return refuse_unready(state)
220
+ if (state.held_issue !== request.issue) {
221
+ return await declined(request, state, report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE))
222
+ }
223
+
224
+ if (!(await is_lane_branch(state))) return await declined(request, state, refuse_unready(state))
233
225
 
234
226
  clear_expired(target)
235
227
 
@@ -250,7 +242,10 @@ async function cut(
250
242
 
251
243
  const state = await run_cut.current_state()
252
244
 
253
- if (!(await is_lane_branch(state))) return refuse_unready(state)
245
+ if (!(await is_lane_branch(state))) {
246
+ return await declined({ issue, phase }, state, refuse_unready(state))
247
+ }
248
+
254
249
  if (skips_pre_gate_cut(phase)) return report_under_threshold()
255
250
 
256
251
  clear_expired(target)
@@ -0,0 +1,72 @@
1
+ import { stamp_file } from '#scripts/josh/stamp-file'
2
+ import { json_value } from '#scripts/lib/json-value'
3
+ import { z } from 'zod'
4
+ import { run_cut, type CutState } from './run-cut'
5
+
6
+ // The implementation cut a run asked for and was refused — `not-a-lane` or `unready` — recorded so the
7
+ // implementation-cut guard stops asking for it (joshuafolkken/kit#3881).
8
+ //
9
+ // **The reissue window alone re-asked every 90 seconds.** A run whose `run:cut --impl` can only answer
10
+ // `unready` was refused edit after edit, each refusal a round trip that cut nothing. Neither answer
11
+ // changes while the same hold stands over the tree, so the record names the issue and the hold it
12
+ // answered, and the guard stays silent while both still match; a fresh hold — a new run — asks again.
13
+
14
+ const DECLINE_PREFIX = 'josh-run-cut-decline-'
15
+
16
+ const decline_schema = z.object({ issue: z.string(), hold_taken_at: z.string().optional() })
17
+
18
+ type CutDecline = z.infer<typeof decline_schema>
19
+
20
+ function decline_path(git_directory: string): string {
21
+ return stamp_file.stamp_path(DECLINE_PREFIX, git_directory)
22
+ }
23
+
24
+ function record(git_directory: string, decline: CutDecline): void {
25
+ stamp_file.replace_stamp(decline_path(git_directory), decline)
26
+ }
27
+
28
+ function read(git_directory: string): CutDecline | undefined {
29
+ const raw = stamp_file.read_stamp_text(decline_path(git_directory))
30
+
31
+ return raw === undefined ? undefined : json_value.parse_with(raw, decline_schema)
32
+ }
33
+
34
+ function matches(recorded: CutDecline | undefined, current: CutDecline): boolean {
35
+ return recorded?.issue === current.issue && recorded.hold_taken_at === current.hold_taken_at
36
+ }
37
+
38
+ // The same read taken synchronously, for the `PreToolUse` guard. A checkout git cannot describe has no
39
+ // record, so the guard keeps speaking there.
40
+ function is_declined_sync(
41
+ issue: string,
42
+ directory: string | undefined = run_cut.worktree_git_directory_sync(),
43
+ ): boolean {
44
+ if (directory === undefined) return false
45
+
46
+ const hold_taken_at = run_cut.hold_of(directory)?.taken_at
47
+
48
+ return matches(read(directory), { issue, hold_taken_at })
49
+ }
50
+
51
+ // **An implementation cut `run:cut` refuses is recorded for the guard that asked for it**, then its
52
+ // exit code is passed back unchanged. The pre-gate phase has its own guard and records nothing.
53
+ async function declined(
54
+ request: { issue: string; phase: string },
55
+ state: CutState,
56
+ code: number,
57
+ ): Promise<number> {
58
+ if (request.phase !== run_cut.IMPLEMENTATION_PHASE) return code
59
+
60
+ const directory = await run_cut.worktree_directory()
61
+
62
+ if (directory !== undefined) {
63
+ record(directory, { issue: request.issue, hold_taken_at: state.hold_taken_at })
64
+ }
65
+
66
+ return code
67
+ }
68
+
69
+ const run_cut_decline = { decline_path, declined, is_declined_sync, read, record }
70
+
71
+ export type { CutDecline }
72
+ export { run_cut_decline }
@@ -0,0 +1,74 @@
1
+ import { UNTRACKED_FILES_FLAG } from '#scripts/git/constants'
2
+ import { git_command } from '#scripts/git/git-command'
3
+ import { git_diff_reads } from '#scripts/git/git-diff-reads'
4
+ import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
5
+ import type { CutState } from './run-cut'
6
+
7
+ // Whether a cut can be taken on this tree — the one predicate `run:cut` refuses `unready` on and the
8
+ // implementation-cut guard asks before it refuses an edit (joshuafolkken/kit#3881).
9
+ //
10
+ // **The guard and the command must not disagree.** The guard once fired on every over-threshold edit
11
+ // of a run held on the default branch, where `run:cut --impl` can only ever answer `unready`; each
12
+ // refusal cost a round trip that cut nothing. Reading the same predicate keeps the guard silent
13
+ // exactly where the command would refuse.
14
+
15
+ interface CutTree {
16
+ branch: string
17
+ default_branch: string
18
+ is_dirty: boolean
19
+ }
20
+
21
+ // A cut needs an uncommitted implementation on a branch other than the default one: the resume
22
+ // verifies both, so a tree missing either has nothing a fresh process could resume.
23
+ function is_ready(tree: CutTree): boolean {
24
+ return tree.branch !== tree.default_branch && tree.is_dirty
25
+ }
26
+
27
+ // The same predicate over the state `run:cut` reads, against the default branch asked asynchronously.
28
+ async function is_lane_branch(state: CutState): Promise<boolean> {
29
+ const default_branch = await git_command.get_default_branch()
30
+
31
+ return is_ready({ branch: state.branch, default_branch, is_dirty: state.is_dirty })
32
+ }
33
+
34
+ // `--branch` puts the branch on a `# branch.head` header line, so one call answers both halves; every
35
+ // other line is a change.
36
+ const STATUS_ARGUMENTS = ['status', '--porcelain=v2', '--branch', UNTRACKED_FILES_FLAG] as const
37
+ const HEADER_PREFIX = '#'
38
+ const BRANCH_HEADER = '# branch.head '
39
+
40
+ function tree_of(status: string, default_branch: string): CutTree | undefined {
41
+ const lines = status.split('\n').filter((line) => line !== '')
42
+ const header = lines.find((line) => line.startsWith(BRANCH_HEADER))
43
+
44
+ if (header === undefined) return undefined
45
+
46
+ const branch = header.slice(BRANCH_HEADER.length)
47
+ const is_dirty = lines.some((line) => !line.startsWith(HEADER_PREFIX))
48
+
49
+ return { branch, default_branch, is_dirty }
50
+ }
51
+
52
+ // The tree read synchronously, for the `PreToolUse` guard — `undefined` where git cannot answer.
53
+ function read_tree_sync(): CutTree | undefined {
54
+ const status = git_spawn_sync.read(STATUS_ARGUMENTS)
55
+
56
+ if (status === undefined) return undefined
57
+
58
+ const reference = git_spawn_sync.read(git_diff_reads.DEFAULT_BRANCH_ARGUMENTS)
59
+
60
+ return tree_of(status, git_diff_reads.default_branch_of(reference))
61
+ }
62
+
63
+ // **A tree git cannot describe reads as ready**, so the guard keeps speaking rather than going quiet
64
+ // on a read fault — the same direction `run_cut.carried_cut_sync` takes for an unreadable record.
65
+ function is_ready_sync(read: () => CutTree | undefined = read_tree_sync): boolean {
66
+ const tree = read()
67
+
68
+ return tree === undefined || is_ready(tree)
69
+ }
70
+
71
+ const run_cut_ready = { is_lane_branch, is_ready, is_ready_sync, tree_of }
72
+
73
+ export type { CutTree }
74
+ export { run_cut_ready }