@joshuafolkken/kit 1.953.0 → 1.972.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.claude/skills/workflow-commands/background-commands.md +3 -0
  2. package/.claude/skills/workflow-commands/backlogrun-child.md +75 -120
  3. package/.claude/skills/workflow-commands/backlogrun-lanes.md +78 -115
  4. package/.claude/skills/workflow-commands/backlogrun-progress.md +82 -146
  5. package/.claude/skills/workflow-commands/backlogrun-steps.md +173 -229
  6. package/.claude/skills/workflow-commands/backlogrun.md +3 -1
  7. package/.claude/skills/workflow-commands/followup.md +3 -0
  8. package/.claude/skills/workflow-commands/fullrun.md +3 -4
  9. package/.claude/skills/workflow-commands/observation-filing.md +31 -25
  10. package/.claude/skills/workflow-commands/pre-gate-cut.md +96 -161
  11. package/.github/workflows/pr-classification.yml +12 -1
  12. package/CLAUDE.md +3 -3
  13. package/README.md +5 -3
  14. package/dist/CLAUDE.md +3 -3
  15. package/dist/hooks/batch-guard.js +4 -4
  16. package/dist/hooks/{chunk-OM5VDM6F.js → chunk-2K52BRWV.js} +2 -2
  17. package/dist/hooks/{chunk-Z5TVZ4AP.js → chunk-BNWIOJFU.js} +29 -16
  18. package/dist/hooks/{chunk-NKVLBB65.js → chunk-BVMSPG3M.js} +3 -3
  19. package/dist/hooks/{chunk-IA7WC4NS.js → chunk-CRGJTD6E.js} +1 -1
  20. package/dist/hooks/{chunk-M4U4YNRE.js → chunk-F6OM6SIG.js} +3 -3
  21. package/dist/hooks/{chunk-O3PEDF3M.js → chunk-KQG4776P.js} +1 -1
  22. package/dist/hooks/{chunk-V7RH56HU.js → chunk-NXL3SBXY.js} +2 -2
  23. package/dist/hooks/{chunk-452OKRQX.js → chunk-WWTMHHZU.js} +120 -18
  24. package/dist/hooks/{chunk-EYFJ74MJ.js → chunk-ZPLXLJ7O.js} +21 -7
  25. package/dist/hooks/{chunk-IBFC25IX.js → chunk-ZYYSBNR6.js} +4 -4
  26. package/dist/hooks/codex-hook-adapter.js +10 -10
  27. package/dist/hooks/duplicate-read-guard.js +5 -5
  28. package/dist/hooks/format-edited.js +2 -2
  29. package/dist/hooks/investigation-guard.js +5 -5
  30. package/dist/hooks/pretool-guard.js +9 -9
  31. package/dist/hooks/stop-guard.js +206 -176
  32. package/dist/josh.js +14 -6
  33. package/package.json +3 -3
  34. package/prompts/collaboration-workflow/wip-cap.md +1 -1
  35. package/scripts/backlog/backlog-arrival.ts +11 -1
  36. package/scripts/backlog/backlog-budget.ts +25 -8
  37. package/scripts/backlog/backlog-drive-cli.ts +5 -1
  38. package/scripts/backlog/backlog-drive-retrospective.ts +20 -0
  39. package/scripts/backlog/backlog-drive.ts +7 -3
  40. package/scripts/backlog/backlog-next.ts +45 -2
  41. package/scripts/backlog/backlog-offer.ts +1 -0
  42. package/scripts/backlog/backlog-plan-cli.ts +66 -27
  43. package/scripts/backlog/backlog-plan.ts +30 -2
  44. package/scripts/backlog/backlog-stalled.ts +9 -0
  45. package/scripts/backlog/backlog-waves.ts +169 -0
  46. package/scripts/document/document-byte-budget.ts +1 -1
  47. package/scripts/document/entry-read-budget.ts +7 -5
  48. package/scripts/epic/epic-lane-offer.ts +69 -4
  49. package/scripts/epic/epic-next.ts +2 -2
  50. package/scripts/epic/epic-solo.ts +147 -0
  51. package/scripts/epic/epic-triage.ts +47 -0
  52. package/scripts/gate/hook-gate-reuse.ts +7 -8
  53. package/scripts/git/git-closes-keyword.ts +15 -0
  54. package/scripts/git/git-followup-flush.ts +5 -6
  55. package/scripts/git/git-followup-issue-close.ts +75 -0
  56. package/scripts/git/git-pr-followup-wrapup.ts +36 -6
  57. package/scripts/git/git-pr-followup.ts +9 -16
  58. package/scripts/git/git-staging.ts +33 -20
  59. package/scripts/git/issue-labels.ts +42 -0
  60. package/scripts/hooks/stop-guard.ts +1 -0
  61. package/scripts/init/init-install.ts +64 -0
  62. package/scripts/init/init.ts +38 -11
  63. package/scripts/init/project-profile.ts +9 -5
  64. package/scripts/init/start-plan.ts +145 -0
  65. package/scripts/init/start-prompt.ts +55 -0
  66. package/scripts/init/start-steps.ts +167 -0
  67. package/scripts/init/start.ts +92 -16
  68. package/scripts/issue/issue-closing-pr.ts +93 -0
  69. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +4 -2
  70. package/scripts/josh/josh-commands-project.ts +11 -3
  71. package/scripts/josh/josh-commands-workflow.ts +3 -3
  72. package/scripts/observations/observation-ledger-prepare.ts +31 -0
  73. package/scripts/observations/observation-ledger.ts +5 -5
  74. package/scripts/observations/observations-flush.ts +11 -30
  75. package/scripts/rules/implementation-cut.ts +49 -16
  76. package/scripts/rules/lane-background.ts +25 -5
  77. package/scripts/rules/stop-rules.ts +16 -4
  78. package/scripts/run/run-carry-cli.ts +21 -0
  79. package/scripts/run/run-cut-cli.ts +92 -110
  80. package/scripts/run/run-cut-report.ts +167 -0
  81. package/scripts/run/run-cut.ts +23 -7
  82. package/scripts/run/run-entry-cli.ts +36 -2
  83. package/scripts/run/run-headless.ts +1 -0
  84. package/scripts/run/run-hold-cli.ts +36 -15
  85. package/scripts/run/run-hold.ts +15 -4
  86. package/scripts/run/run-merge-cli.ts +28 -14
  87. package/scripts/run/run-merge-steps.ts +38 -2
  88. package/scripts/run/run-merge.ts +41 -3
  89. package/scripts/run/run-prep-cli.ts +8 -2
  90. package/scripts/run/run-prep-locate.ts +165 -0
  91. package/scripts/run/run-prep.ts +12 -1
  92. package/scripts/run/run-retrospective.ts +34 -0
  93. package/scripts/run/run-step-cli.ts +19 -5
  94. package/scripts/run/run-step.ts +31 -19
  95. package/scripts/run/run-tail-cli.ts +4 -2
  96. package/scripts/run/run-wake-work.ts +4 -0
  97. package/scripts/safe-chain/ci-installer-pin.ts +8 -4
  98. package/scripts/time-runtime/time-background.ts +11 -0
  99. package/scripts/time-runtime/time-transcript-line.ts +4 -0
  100. package/templates/pnpm-workspace.static.yaml +2 -0
@@ -63,6 +63,9 @@ interface LaneCostState {
63
63
  directory: string
64
64
  source: MarkerSource
65
65
  carried: (now?: Date) => RunCut | undefined
66
+ // The issue whose run hold stands over this checkout (joshuafolkken/kit#2760) — how a `fullrun`
67
+ // held outside a lane is told apart from a person's tree.
68
+ held_issue: () => string | undefined
66
69
  verdict: () => CostVerdict
67
70
  }
68
71
 
@@ -71,6 +74,7 @@ function current_state(): LaneCostState {
71
74
  directory: process.cwd(),
72
75
  source: process.env,
73
76
  carried: run_cut.carried_cut_sync,
77
+ held_issue: run_cut.held_issue_sync,
74
78
  // **Read through the short reuse window** (joshuafolkken/kit#2385). The row now fires per
75
79
  // threshold crossing rather than once per run, so this predicate is a candidate on every edit;
76
80
  // the window collapses one turn's burst of edits to a single whole-transcript price.
@@ -79,22 +83,49 @@ function current_state(): LaneCostState {
79
83
  }
80
84
  }
81
85
 
82
- // A dispatched lane child for this checkout with no cut already carried. The dispatch mark is what
83
- // tells a child apart from a person (joshuafolkken/kit#1904), read from the environment against this
84
- // lane's own issue so a leaked mark for another issue reads as a person. **The carried-cut half keeps
85
- // the guard silent between a cut and its resume**, exactly as `pre-gate-cut.ts`'s does: a record naming
86
- // this issue means a cut is already in flight, and `begin_cut`'s exclusive create would refuse a second
87
- // one anyway.
88
- function uncut_lane_child_issue(state: LaneCostState): string | undefined {
86
+ // A dispatched lane child's issue for this checkout. The dispatch mark is what tells a child apart from
87
+ // a person (joshuafolkken/kit#1904), read from the environment against this lane's own issue so a
88
+ // leaked mark for another issue reads as a person.
89
+ function lane_child_issue(state: LaneCostState): string | undefined {
89
90
  if (!lane_child_marker.is_child_of(state.directory, state.source)) return undefined
90
91
 
91
- const issue = lane_paths.lane_issue_of(state.directory, { ...state.source })
92
+ return lane_paths.lane_issue_of(state.directory, { ...state.source })
93
+ }
94
+
95
+ // The issue a run holds this checkout for, read only outside a lane: a lane's hold is its dispatched
96
+ // child's, so a person finishing that lane's work carries no mark and still sees no refusal.
97
+ function held_run_issue(state: LaneCostState): string | undefined {
98
+ if (lane_paths.lane_issue_of(state.directory, { ...state.source }) !== undefined) return undefined
99
+
100
+ return state.held_issue()
101
+ }
102
+
103
+ // The run this checkout's edits belong to, with no cut already carried — a dispatched lane child, or
104
+ // **a `fullrun` held in its own checkout** (joshuafolkken/kit#2760): the cut was lane-only, so a run a
105
+ // person started grew without a bound mid-implementation. The run hold naming an issue is what marks
106
+ // such a run, and a person's tree holds nothing, so it still sees no refusal. **The carried-cut half
107
+ // keeps the guard silent between a cut and its resume**, exactly as `pre-gate-cut.ts`'s does: a record
108
+ // naming this issue means a cut is already in flight, and `begin_cut`'s exclusive create would refuse
109
+ // a second one anyway.
110
+ function uncut_run_issue(state: LaneCostState): string | undefined {
111
+ const issue = lane_child_issue(state) ?? held_run_issue(state)
92
112
 
93
113
  if (issue === undefined) return undefined
94
114
 
95
115
  return state.carried()?.issue === issue ? undefined : issue
96
116
  }
97
117
 
118
+ // **An unmeasurable session is cut in a lane and left alone outside one.** A lane child's safety net is
119
+ // the pre-gate direction below; a held run outside a lane is a person's own session, so only a measured
120
+ // `over` interrupts it — a provider with no transcript would otherwise refuse every edit it makes.
121
+ function warrants_the_cut(state: LaneCostState): boolean {
122
+ const verdict = state.verdict()
123
+
124
+ if (lane_child_issue(state) === undefined) return verdict === cost_verdict.OVER_VERDICT
125
+
126
+ return verdict !== cost_verdict.UNDER_VERDICT
127
+ }
128
+
98
129
  // **The tool-name test comes first and the world is consulted second, the cost measurement last.** This
99
130
  // predicate is asked of every call in the run, so the `Edit` / `Write` string match keeps the lane read
100
131
  // off all but the edits, and the lane read keeps the transcript-priced verdict off every edit but those
@@ -109,9 +140,9 @@ function is_over_threshold_edit(
109
140
  call: GuardedCall,
110
141
  state: LaneCostState = current_state(),
111
142
  ): boolean {
112
- if (!is_edit_tool(call.name) || uncut_lane_child_issue(state) === undefined) return false
143
+ if (!is_edit_tool(call.name) || uncut_run_issue(state) === undefined) return false
113
144
 
114
- return state.verdict() !== cost_verdict.UNDER_VERDICT
145
+ return warrants_the_cut(state)
115
146
  }
116
147
 
117
148
  // **The threshold in the refusal text is assembled from the constant, never spelled** (joshuafolkken/kit#2385).
@@ -125,7 +156,8 @@ const THRESHOLD_TEXT = `${cost_format.format_tokens(CONTEXT_CUT_THRESHOLD)}-toke
125
156
  // because `cut` is the only one that ends the turn and a run told merely to "take the cut" would have to
126
157
  // read which of the others leave it implementing — the same reason `pre-gate-cut.ts` spells them out.
127
158
  const IMPLEMENTATION_CUT_REASON =
128
- '⛔ implementation-phase cut: this checkout is a lane dispatched for this issue and its recent-context ' +
159
+ '⛔ implementation-phase cut: this checkout is a lane dispatched for this issue, or a run holds it for ' +
160
+ 'this issue (joshuafolkken/kit#2760), and its recent-context ' +
129
161
  `cost has crossed the shared ${THRESHOLD_TEXT} threshold mid-implementation, so the thinking accumulated ` +
130
162
  'so far is now re-read on every later request. Take the cut before this edit. ' +
131
163
  'First write a handoff file with the Write tool — the user’s instruction verbatim, what you have ' +
@@ -139,11 +171,12 @@ const IMPLEMENTATION_CUT_REASON =
139
171
  'because the verdict was only ever read at session entry, where the context has not yet grown. The ' +
140
172
  "verdict is `pnpm josh cost --cut`'s exactly — the same per-request billed-input statistic against the " +
141
173
  'same threshold, never a second measurement. Issue `pnpm josh run:cut --impl <N> --handoff <path>` now and read the ' +
142
- 'verdict: on `cut`, **end the turn immediately** — the fresh process owns the run and continues ' +
143
- 'implementing, so it must not be waited for; on `not-a-lane`, `unready`, `busy`, `failed` or ' +
174
+ 'verdict: on `cut`, **end the turn immediately** — in a lane the fresh process owns the run and ' +
175
+ 'continues implementing, so it must not be waited for; outside a lane nothing is relaunched, so first ' +
176
+ 'send the `confirmation` Telegram naming the resume command `fullrun #<N>` and keep the hold; on `not-a-lane`, `unready`, `busy`, `failed` or ' +
144
177
  '`unknown`, this process carries the run on and the edit is simply the next call. Never relaunch a ' +
145
- 'second process after `busy`. This fired because the dispatch mark names this lane; a person working ' +
146
- 'here carries no mark and sees no refusal, so there is no human-or-child judgement left to make. The ' +
178
+ 'second process after `busy`. This fired because the dispatch mark names this lane or the run hold ' +
179
+ 'names this issue; a person working here carries no mark and holds no run, so sees no refusal, so there is no human-or-child judgement left to make. The ' +
147
180
  'procedure is `.claude/skills/workflow-commands/pre-gate-cut.md`. Reissue this edit once the cut has ' +
148
181
  'answered — an edit reissued right after this refusal passes, so `busy` / `failed` cannot wedge the ' +
149
182
  'run edit after edit; this fires again on the next threshold crossing rather than once per run.'
@@ -195,7 +228,7 @@ const implementation_cut = {
195
228
  is_over_threshold_edit,
196
229
  is_reissued_refusal,
197
230
  takes_the_impl_cut,
198
- uncut_lane_child_issue,
231
+ uncut_run_issue,
199
232
  }
200
233
 
201
234
  export type { LaneCostState }
@@ -3,7 +3,10 @@ import { lane_child_marker } from '#scripts/lane/lane-child-marker'
3
3
  import { json_value } from '#scripts/lib/json-value'
4
4
  import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
5
5
  import { time_shell } from '#scripts/time-runtime/time-shell'
6
- import { time_transcript_line } from '#scripts/time-runtime/time-transcript-line'
6
+ import {
7
+ time_transcript_line,
8
+ type Block as TranscriptBlock,
9
+ } from '#scripts/time-runtime/time-transcript-line'
7
10
  import { run_tail } from './run-tail'
8
11
 
9
12
  // A dispatched lane child is stopped from backgrounding a long-running josh command (joshuafolkken/kit#2704).
@@ -60,16 +63,32 @@ function is_background_long_run(
60
63
  return is_candidate(call) && is_lane_child()
61
64
  }
62
65
 
63
- // The background tasks the transcript tail launched and never saw finish. A launch is read off its own
64
- // tool result and a finish off the harness notice, both by `time_transcript_line`.
65
- function pending_background_ids(tail: string): ReadonlyArray<string> {
66
+ // The launches `launched_ids` reads off the transcript tail that never saw their finish. A launch is
67
+ // read off its own tool result and a finish off the harness notice, both by `time_transcript_line`.
68
+ function unfinished_ids(
69
+ tail: string,
70
+ launched_ids: (block: TranscriptBlock) => ReadonlyArray<string>,
71
+ ): ReadonlyArray<string> {
66
72
  const lines = tail.split('\n').map((line) => time_transcript_line.parse_line(line))
67
73
  const finished = new Set(lines.map((line) => line?.finished_background ?? ''))
68
- const launched = lines.flatMap((line) => line?.blocks.map((block) => block.background_id) ?? [])
74
+ const blocks = lines.flatMap((line) => line?.blocks ?? [])
75
+ const launched = blocks.flatMap((block) => launched_ids(block))
69
76
 
70
77
  return launched.filter((id) => id !== '' && !finished.has(id))
71
78
  }
72
79
 
80
+ // The background tasks the tail launched and never saw finish — commands and subagents alike
81
+ // (joshuafolkken/kit#2774), since a lane child's turn-end kills either.
82
+ function pending_background_ids(tail: string): ReadonlyArray<string> {
83
+ return unfinished_ids(tail, (block) => [block.background_id, block.agent_id])
84
+ }
85
+
86
+ // The backgrounded subagents alone. A subagent always ends and its notice re-invokes the session; a
87
+ // command may be a server that never exits, so only this set says the run is certain to resume.
88
+ function pending_agent_ids(tail: string): ReadonlyArray<string> {
89
+ return unfinished_ids(tail, (block) => [block.agent_id])
90
+ }
91
+
73
92
  const LANE_BACKGROUND_REASON =
74
93
  '⛔ lane background: this session is a dispatched lane child (`JOSH_LANE_CHILD`), a headless ' +
75
94
  '`claude -p` process whose background Bash tasks are killed when the turn ends — so a backgrounded ' +
@@ -105,6 +124,7 @@ const lane_background = {
105
124
  LANE_BACKGROUND_STOP_REASON,
106
125
  ROW,
107
126
  is_background_long_run,
127
+ pending_agent_ids,
108
128
  pending_background_ids,
109
129
  }
110
130
 
@@ -67,9 +67,12 @@ interface StopContext {
67
67
  headless_refusals: number
68
68
  // This session is a dispatched lane child for this checkout — `lane_child_marker.is_child_of`.
69
69
  lane_child: boolean
70
- // A background task this run launched has not finished — `lane_background.pending_background_ids`
71
- // over the transcript tail (joshuafolkken/kit#2704).
70
+ // A background task this run launched — a command or a subagent — has not finished —
71
+ // `lane_background.pending_background_ids` over the transcript tail (joshuafolkken/kit#2704).
72
72
  background_pending: boolean
73
+ // A backgrounded subagent this run launched has not finished — `lane_background.pending_agent_ids`
74
+ // (joshuafolkken/kit#2774). Apart from `background_pending`, since only a subagent is sure to end.
75
+ agent_pending: boolean
73
76
  // The resolved `JOSH_SESSION_LANG` — `session_language.resolve_session_lang` (joshuafolkken/kit#2470).
74
77
  session_lang: string
75
78
  }
@@ -221,12 +224,21 @@ function needs_filing(context: StopContext): boolean {
221
224
  return filing_offer.is_first_party_target(context.message, context.session_owner)
222
225
  }
223
226
 
227
+ // **A running background subagent stands both hold rules down** (joshuafolkken/kit#2774). Its completion
228
+ // notice re-invokes the session, so the turn-end is a wait on the run's own work, not on a person — a
229
+ // notify there says "nothing needed" and dilutes the ones that do. A backgrounded command does not
230
+ // count: a dev server never ends, and a stop behind it would leave a person silently waiting. A lane
231
+ // child never reaches this with a task running: `needs_background_wait` answers it first.
232
+ function is_held_idle(context: StopContext): boolean {
233
+ return context.hold_present && !context.agent_pending
234
+ }
235
+
224
236
  function needs_notify(context: StopContext): boolean {
225
- return context.hold_present && !context.notified
237
+ return is_held_idle(context) && !context.notified
226
238
  }
227
239
 
228
240
  function needs_release(context: StopContext): boolean {
229
- return context.hold_present && context.tree_clean
241
+ return is_held_idle(context) && context.tree_clean
230
242
  }
231
243
 
232
244
  function citation_reason(message: string): string | undefined {
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
+ import { cost_cli } from '#scripts/cost-runtime/cost-cli'
4
+ import { cost_verdict } from '#scripts/cost-runtime/cost-verdict'
3
5
  import {
4
6
  run_carry,
5
7
  type CarryClaim,
@@ -44,6 +46,9 @@ const UNKNOWN_VERDICT = 'unknown'
44
46
  // the run carries on uncut rather than paying a cold preamble the accumulation it would shed no longer
45
47
  // covers, exactly as an under-threshold pre-gate cut carries on to the gate.
46
48
  const CAPPED_VERDICT = 'capped'
49
+ // The asking session is over the shared context-cut threshold, so nothing was claimed
50
+ // (joshuafolkken/kit#2760).
51
+ const OVER_VERDICT = 'over'
47
52
  const NO_CUTS = 0
48
53
 
49
54
  const CLAIM_VERDICTS: Record<CarryClaim, string> = {
@@ -235,7 +240,22 @@ function adopt(target: string, request: CarryClaimRequest, is_json: boolean): nu
235
240
  return report_claim(target, read.carry, request, is_json)
236
241
  }
237
242
 
243
+ // **A session already over the shared cut threshold claims nothing** (joshuafolkken/kit#2760). A claim
244
+ // assumed the asking session was fresh, so a `backlogrun` parent that had taken its cut at 233k could
245
+ // retype the keyword in the same conversation and be answered `resumed` by its own hand-off — measured
246
+ // twice, at 232k and 258k. The verdict is `pnpm josh cost --cut`'s, so no second threshold exists; only a
247
+ // measured `over` refuses, because a provider with no transcript cannot be priced at all.
248
+ function report_over(is_json: boolean): number {
249
+ console.error(
250
+ 'This session is over the shared context-cut threshold (`pnpm josh cost --cut` answers `over`), so no run was begun or resumed here. End this conversation and type the invocation again in a fresh session.',
251
+ )
252
+
253
+ return report(OVER_VERDICT, undefined, is_json, FAILURE_EXIT_CODE)
254
+ }
255
+
238
256
  function claim_record(target: string, request: CarryClaimRequest, is_json: boolean): number {
257
+ if (cost_cli.session_verdict() === cost_verdict.OVER_VERDICT) return report_over(is_json)
258
+
239
259
  return request.is_adoption ? adopt(target, request, is_json) : begin(target, request, is_json)
240
260
  }
241
261
 
@@ -425,6 +445,7 @@ const run_carry_cli = {
425
445
  EXPIRED_VERDICT,
426
446
  MISMATCH_VERDICT,
427
447
  NONE_VERDICT,
448
+ OVER_VERDICT,
428
449
  RESUMED_VERDICT,
429
450
  STANDING_VERDICT,
430
451
  UNKNOWN_VERDICT,
@@ -9,6 +9,7 @@ import { openai_lane_supervisor } from '#scripts/lane/openai-lane-supervisor'
9
9
  import { run_cut, type CutState, type RunCut } from './run-cut'
10
10
  import { run_cut_args, type Request } from './run-cut-args'
11
11
  import { run_cut_handoff } from './run-cut-handoff'
12
+ import { run_cut_report } from './run-cut-report'
12
13
  import { run_event_stream } from './run-event-stream'
13
14
  import { run_event_stream_emit } from './run-event-stream-emit'
14
15
 
@@ -24,115 +25,41 @@ import { run_event_stream_emit } from './run-event-stream-emit'
24
25
  // session.
25
26
 
26
27
  const ARGV_OFFSET = 2
27
- const SUCCESS_EXIT_CODE = 0
28
- const FAILURE_EXIT_CODE = 1
29
-
30
- const CUT_VERDICT = 'cut'
31
- const RESUME_VERDICT = 'resume'
32
- // An implementation-phase resume: the fresh process continues implementing rather than going to the
33
- // gate (joshuafolkken/kit#1933). It is a distinct token so the resuming child branches on it without
34
- // re-reading the record.
35
- const RESUME_IMPL_VERDICT = 'resume-impl'
36
- const FRESH_VERDICT = 'fresh'
37
- // The recent-window context is under the shared cut threshold, so the pre-gate cut was not worth its
38
- // resume and nothing was cut (joshuafolkken/kit#2312). A benign, non-failing answer: the run carries
39
- // on to the gate itself, exactly as `not-a-lane` does.
40
- const UNDER_THRESHOLD_VERDICT = 'under-threshold'
41
- const STALE_VERDICT = 'stale'
42
- // The record matched the tree but carried no instruction to resume on, so the run was refused rather
43
- // than continued blind to what it was told to do (joshuafolkken/kit#2354).
44
- const INCOMPLETE_VERDICT = 'incomplete'
45
- // The `--handoff` path was given but could not be read as a handoff, or was too large for the record;
46
- // the cut is refused rather than taken without the instruction it was meant to carry.
47
- const BAD_HANDOFF_VERDICT = 'bad-handoff'
48
- const HANDED_OFF_VERDICT = 'handed-off'
49
- const BUSY_VERDICT = 'busy'
50
- const NOT_A_LANE_VERDICT = 'not-a-lane'
51
- const UNREADY_VERDICT = 'unready'
52
- const FAILED_VERDICT = 'failed'
53
- const ENDED_VERDICT = 'ended'
54
- const UNREADABLE_VERDICT = 'unreadable'
55
- const UNKNOWN_VERDICT = 'unknown'
56
-
57
- function report(verdict: string, code: number): number {
58
- console.info(verdict)
59
-
60
- return code
61
- }
62
-
63
- function report_stale(cut_record: RunCut): number {
64
- console.error(run_cut.stale_message(cut_record))
65
-
66
- return report(STALE_VERDICT, FAILURE_EXIT_CODE)
67
- }
68
-
69
- function report_busy(cut_record: RunCut): number {
70
- console.error(run_cut.busy_message(cut_record))
71
28
 
72
- return report(BUSY_VERDICT, FAILURE_EXIT_CODE)
73
- }
74
-
75
- // The record matched the tree but carried no instruction; the run is refused rather than continued
76
- // without what it was told to do (joshuafolkken/kit#2354).
77
- function report_incomplete(cut_record: RunCut): number {
78
- console.error(run_cut.incomplete_message(cut_record))
79
-
80
- return report(INCOMPLETE_VERDICT, FAILURE_EXIT_CODE)
81
- }
82
-
83
- // The `--handoff` file parsed but the assembled record would not fit the byte bound — the scalar fields
84
- // carry a handoff that fit alone past the cap (joshuafolkken/kit#2354).
85
- const HANDOFF_OVERFLOW_NOTE = '--handoff <path> would grow the cut record past its byte bound'
86
- // An implementation cut resumes into implementation, and that resume refuses a record with no
87
- // instruction (`incomplete`) — so the cut is refused here instead of relaunching a successor that can
88
- // only stop (joshuafolkken/kit#2484).
89
- const HANDOFF_MISSING_NOTE = 'this cut resumes into implementation and needs --handoff <path>'
90
-
91
- function report_bad_handoff(note: string): number {
92
- console.error(`${note}. Nothing was cut; write the handoff file and reissue.`)
93
-
94
- return report(BAD_HANDOFF_VERDICT, FAILURE_EXIT_CODE)
95
- }
96
-
97
- // A successor already resumed this cut; a process woken after its own hand-off is told to stop rather
98
- // than to investigate (joshuafolkken/kit#1935). It is a benign, non-failing stop.
99
- function report_handed_off(cut_record: RunCut): number {
100
- console.error(run_cut.handed_off_message(cut_record))
101
-
102
- return report(HANDED_OFF_VERDICT, SUCCESS_EXIT_CODE)
103
- }
104
-
105
- function report_unreadable(): number {
106
- console.error(run_cut.unreadable_message())
107
-
108
- return report(UNREADABLE_VERDICT, FAILURE_EXIT_CODE)
109
- }
110
-
111
- function report_unknown(): number {
112
- console.error(run_cut.unknown_message())
113
-
114
- return report(UNKNOWN_VERDICT, FAILURE_EXIT_CODE)
115
- }
116
-
117
- // A cut is only meaningful on a lane branch whose implementation is uncommitted. On the default
118
- // branch, or with a clean tree, there is nothing to carry across the boundary.
119
- function refuse_unready(state: CutState): number {
120
- console.error(
121
- `Not ready to cut on ${state.branch} (dirty: ${String(state.is_dirty)}); a cut needs an uncommitted lane branch. Nothing was cut.`,
122
- )
123
-
124
- return report(UNREADY_VERDICT, FAILURE_EXIT_CODE)
125
- }
126
-
127
- // Below the shared context threshold there is no accumulation a cut would drop, so the resume it would
128
- // relaunch costs more than it saves; the current process carries the run on to the gate uncut.
129
- function report_under_threshold(): number {
130
- console.error(
131
- 'The recent-window context is under the shared cut threshold, so nothing was cut; a resume would cost more than the accumulation it would drop. Continue to the gate.',
132
- )
133
-
134
- return report(UNDER_THRESHOLD_VERDICT, SUCCESS_EXIT_CODE)
135
- }
29
+ const {
30
+ BAD_HANDOFF_VERDICT,
31
+ BUSY_VERDICT,
32
+ CUT_VERDICT,
33
+ ENDED_VERDICT,
34
+ FAILED_VERDICT,
35
+ FAILURE_EXIT_CODE,
36
+ FRESH_VERDICT,
37
+ HANDED_OFF_VERDICT,
38
+ HANDOFF_MISSING_NOTE,
39
+ HANDOFF_OVERFLOW_NOTE,
40
+ INCOMPLETE_VERDICT,
41
+ NOT_A_LANE_VERDICT,
42
+ OVER_VERDICT,
43
+ RESUME_IMPL_VERDICT,
44
+ RESUME_VERDICT,
45
+ STALE_VERDICT,
46
+ SUCCESS_EXIT_CODE,
47
+ UNDER_THRESHOLD_VERDICT,
48
+ UNKNOWN_VERDICT,
49
+ UNREADABLE_VERDICT,
50
+ UNREADY_VERDICT,
51
+ refuse_unready,
52
+ report,
53
+ report_bad_handoff,
54
+ report_busy,
55
+ report_handed_off,
56
+ report_incomplete,
57
+ report_over,
58
+ report_stale,
59
+ report_under_threshold,
60
+ report_unknown,
61
+ report_unreadable,
62
+ } = run_cut_report
136
63
 
137
64
  // **The pre-gate cut is conditional on the same statistic the implementation-phase cut reads**
138
65
  // (joshuafolkken/kit#2312). `cost_cli.session_verdict` prices the recent `RECENT_REQUEST_WINDOW`
@@ -240,7 +167,9 @@ function spec_refusal(spec: Parameters<typeof run_cut.begin_cut>[1]): string | u
240
167
  return undefined
241
168
  }
242
169
 
243
- async function finish_cut(target: string, lane: LaneInfo, request: CutRequest): Promise<number> {
170
+ // Writes the record and appends the `cut` event, or answers the refusal that stopped it — the half of a
171
+ // cut every checkout shares; what follows the write (a relaunch or a stop) is the caller's.
172
+ async function write_cut(target: string, request: CutRequest): Promise<number | undefined> {
244
173
  const loaded = run_cut_handoff.load_handoff(request.handoff_path, run_cut.MAX_HANDOFF_BYTES)
245
174
  if (loaded.kind === 'bad') return report_bad_handoff(loaded.note)
246
175
 
@@ -258,11 +187,53 @@ async function finish_cut(target: string, lane: LaneInfo, request: CutRequest):
258
187
 
259
188
  await emit_cut_event(request)
260
189
 
190
+ return undefined
191
+ }
192
+
193
+ async function finish_cut(target: string, lane: LaneInfo, request: CutRequest): Promise<number> {
194
+ const refused = await write_cut(target, request)
195
+
196
+ if (refused !== undefined) return refused
197
+
261
198
  return lane_relaunch.is_openai_lane(lane)
262
199
  ? report(CUT_VERDICT, SUCCESS_EXIT_CODE)
263
200
  : relaunch(target, lane, request.phase)
264
201
  }
265
202
 
203
+ // The cut outside a lane is taken, and nothing is relaunched: the session that took it is the one a
204
+ // person is watching, so the run is handed on the way `fullrun`'s entry `over` stop hands it on.
205
+ function report_held_cut(issue: string): number {
206
+ console.error(
207
+ `The cut is recorded for #${issue} outside a lane, so nothing was relaunched. Keep the hold, send a \`confirmation\` Telegram whose body names the resume command \`fullrun #${issue}\`, and end the turn; the fresh session's \`pnpm josh run:cut --resume ${issue}\` answers \`resume-impl\` with the handoff.`,
208
+ )
209
+
210
+ return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
211
+ }
212
+
213
+ // **A `fullrun` held in its own checkout takes the implementation cut too** (joshuafolkken/kit#2760).
214
+ // The cut was lane-only, so a run a person started had no bound on its context mid-implementation. The
215
+ // hold naming this issue is what marks the run — a tree nobody holds for it, and the pre-gate phase
216
+ // (which a held run's gate reaches in the same session), stay `not-a-lane` exactly as before.
217
+ async function cut_outside_lane(
218
+ target: string,
219
+ request: Omit<CutRequest, 'branch'>,
220
+ ): Promise<number> {
221
+ if (request.phase !== run_cut.IMPLEMENTATION_PHASE) {
222
+ return report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE)
223
+ }
224
+
225
+ const state = await run_cut.current_state()
226
+
227
+ if (state.held_issue !== request.issue) return report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE)
228
+ if (!(await is_lane_branch(state))) return refuse_unready(state)
229
+
230
+ clear_expired(target)
231
+
232
+ const refused = await write_cut(target, { ...request, branch: state.branch })
233
+
234
+ return refused ?? report_held_cut(request.issue)
235
+ }
236
+
266
237
  async function cut(
267
238
  target: string,
268
239
  issue: string,
@@ -271,7 +242,7 @@ async function cut(
271
242
  ): Promise<number> {
272
243
  const lane = await lane_registry.find_open_lane(issue)
273
244
 
274
- if (lane === undefined) return report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE)
245
+ if (lane === undefined) return await cut_outside_lane(target, { issue, phase, handoff_path })
275
246
 
276
247
  const state = await run_cut.current_state()
277
248
 
@@ -331,6 +302,14 @@ function adopt(target: string, cut_record: RunCut): number {
331
302
  return report(resume_verdict_for(cut_record), SUCCESS_EXIT_CODE)
332
303
  }
333
304
 
305
+ // A relaunched lane child and a fresh session both measure `under`, so only the session that took an
306
+ // implementation cut resumes over the threshold (joshuafolkken/kit#2760); unmeasurable is not refused.
307
+ function is_over_threshold_resume(cut_record: RunCut): boolean {
308
+ if (!run_cut.resumes_into_implementation(cut_record.phase)) return false
309
+
310
+ return cost_cli.session_verdict() === cost_verdict.OVER_VERDICT
311
+ }
312
+
334
313
  async function verify_and_adopt(
335
314
  target: string,
336
315
  cut_record: RunCut,
@@ -350,6 +329,8 @@ async function verify_and_adopt(
350
329
 
351
330
  if (verdict === INCOMPLETE_VERDICT) return report_incomplete(cut_record)
352
331
 
332
+ if (is_over_threshold_resume(cut_record)) return report_over()
333
+
353
334
  return adopt(target, cut_record)
354
335
  }
355
336
 
@@ -444,6 +425,7 @@ const run_cut_cli = {
444
425
  HANDED_OFF_VERDICT,
445
426
  INCOMPLETE_VERDICT,
446
427
  NOT_A_LANE_VERDICT,
428
+ OVER_VERDICT,
447
429
  RESUME_IMPL_VERDICT,
448
430
  RESUME_VERDICT,
449
431
  STALE_VERDICT,