@joshuafolkken/kit 1.957.0 → 1.972.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/workflow-commands/background-commands.md +3 -0
- package/.claude/skills/workflow-commands/backlogrun-child.md +75 -120
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +78 -115
- package/.claude/skills/workflow-commands/backlogrun-progress.md +82 -146
- package/.claude/skills/workflow-commands/backlogrun-steps.md +173 -229
- package/.claude/skills/workflow-commands/backlogrun.md +3 -1
- package/.claude/skills/workflow-commands/followup.md +3 -0
- package/.claude/skills/workflow-commands/fullrun.md +3 -4
- package/.claude/skills/workflow-commands/observation-filing.md +31 -25
- package/.claude/skills/workflow-commands/pre-gate-cut.md +96 -161
- package/.github/workflows/pr-classification.yml +12 -1
- package/CLAUDE.md +3 -3
- package/README.md +3 -3
- package/dist/CLAUDE.md +3 -3
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-XSL4SRMA.js → chunk-2K52BRWV.js} +2 -2
- package/dist/hooks/{chunk-M6K7EQDO.js → chunk-BNWIOJFU.js} +29 -16
- package/dist/hooks/{chunk-KOCK5FXQ.js → chunk-BVMSPG3M.js} +3 -3
- package/dist/hooks/{chunk-3ON7HOYB.js → chunk-CRGJTD6E.js} +1 -1
- package/dist/hooks/{chunk-HJXQZ7V4.js → chunk-F6OM6SIG.js} +3 -3
- package/dist/hooks/{chunk-LDQDZYHX.js → chunk-KQG4776P.js} +1 -1
- package/dist/hooks/{chunk-244WYOEO.js → chunk-NXL3SBXY.js} +2 -2
- package/dist/hooks/{chunk-IJPBTDTB.js → chunk-WWTMHHZU.js} +59 -18
- package/dist/hooks/{chunk-BELI5XSS.js → chunk-ZPLXLJ7O.js} +15 -5
- package/dist/hooks/{chunk-GNOITLRR.js → chunk-ZYYSBNR6.js} +4 -4
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +206 -176
- package/dist/josh.js +8 -4
- package/package.json +3 -3
- package/prompts/collaboration-workflow/wip-cap.md +1 -1
- package/scripts/backlog/backlog-arrival.ts +11 -1
- package/scripts/backlog/backlog-budget.ts +25 -8
- package/scripts/backlog/backlog-next.ts +45 -2
- package/scripts/backlog/backlog-offer.ts +1 -0
- package/scripts/backlog/backlog-plan-cli.ts +66 -27
- package/scripts/backlog/backlog-plan.ts +30 -2
- package/scripts/backlog/backlog-stalled.ts +9 -0
- package/scripts/backlog/backlog-waves.ts +169 -0
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/document/entry-read-budget.ts +7 -5
- package/scripts/epic/epic-lane-offer.ts +69 -4
- package/scripts/epic/epic-next.ts +2 -2
- package/scripts/epic/epic-solo.ts +147 -0
- package/scripts/epic/epic-triage.ts +47 -0
- package/scripts/gate/hook-gate-reuse.ts +7 -8
- package/scripts/git/git-closes-keyword.ts +15 -0
- package/scripts/git/git-followup-flush.ts +5 -6
- package/scripts/git/git-followup-issue-close.ts +75 -0
- package/scripts/git/git-pr-followup-wrapup.ts +36 -6
- package/scripts/git/git-pr-followup.ts +9 -16
- package/scripts/git/git-staging.ts +33 -20
- package/scripts/git/issue-labels.ts +23 -0
- package/scripts/hooks/stop-guard.ts +1 -0
- package/scripts/init/init-install.ts +64 -0
- package/scripts/init/init.ts +38 -11
- package/scripts/issue/issue-closing-pr.ts +93 -0
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +2 -1
- package/scripts/josh/josh-commands-project.ts +5 -1
- package/scripts/josh/josh-commands-workflow.ts +3 -3
- package/scripts/observations/observation-ledger-prepare.ts +31 -0
- package/scripts/observations/observation-ledger.ts +5 -5
- package/scripts/observations/observations-flush.ts +11 -30
- package/scripts/rules/implementation-cut.ts +49 -16
- package/scripts/rules/lane-background.ts +25 -5
- package/scripts/rules/stop-rules.ts +16 -4
- package/scripts/run/run-carry-cli.ts +21 -0
- package/scripts/run/run-cut-cli.ts +92 -110
- package/scripts/run/run-cut-report.ts +167 -0
- package/scripts/run/run-cut.ts +23 -7
- package/scripts/run/run-entry-cli.ts +36 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-hold-cli.ts +36 -15
- package/scripts/run/run-hold.ts +15 -4
- package/scripts/run/run-merge-cli.ts +28 -14
- package/scripts/run/run-merge-steps.ts +38 -2
- package/scripts/run/run-merge.ts +41 -3
- package/scripts/run/run-prep-cli.ts +8 -2
- package/scripts/run/run-prep-locate.ts +165 -0
- package/scripts/run/run-prep.ts +12 -1
- package/scripts/run/run-step-cli.ts +16 -0
- package/scripts/run/run-step.ts +28 -4
- package/scripts/run/run-tail-cli.ts +4 -2
- package/scripts/run/run-wake-work.ts +4 -0
- package/scripts/safe-chain/ci-installer-pin.ts +8 -4
- package/scripts/time-runtime/time-background.ts +11 -0
- package/scripts/time-runtime/time-transcript-line.ts +4 -0
- package/templates/pnpm-workspace.static.yaml +2 -0
|
@@ -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 —
|
|
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
|
|
237
|
+
return is_held_idle(context) && !context.notified
|
|
226
238
|
}
|
|
227
239
|
|
|
228
240
|
function needs_release(context: StopContext): boolean {
|
|
229
|
-
return context
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { run_cut, type CutState, type RunCut } from './run-cut'
|
|
2
|
+
|
|
3
|
+
// The verdict tokens `josh run:cut` answers with and the reports that print them, split out of
|
|
4
|
+
// `run-cut-cli.ts` so the CLI keeps to acting on the record. The contract is unchanged: **standard
|
|
5
|
+
// output carries exactly one token** and every explanation goes to standard error.
|
|
6
|
+
|
|
7
|
+
const SUCCESS_EXIT_CODE = 0
|
|
8
|
+
const FAILURE_EXIT_CODE = 1
|
|
9
|
+
|
|
10
|
+
const CUT_VERDICT = 'cut'
|
|
11
|
+
const RESUME_VERDICT = 'resume'
|
|
12
|
+
// An implementation-phase resume: the fresh process continues implementing rather than going to the
|
|
13
|
+
// gate (joshuafolkken/kit#1933). It is a distinct token so the resuming child branches on it without
|
|
14
|
+
// re-reading the record.
|
|
15
|
+
const RESUME_IMPL_VERDICT = 'resume-impl'
|
|
16
|
+
const FRESH_VERDICT = 'fresh'
|
|
17
|
+
// The recent-window context is under the shared cut threshold, so the pre-gate cut was not worth its
|
|
18
|
+
// resume and nothing was cut (joshuafolkken/kit#2312). A benign, non-failing answer: the run carries
|
|
19
|
+
// on to the gate itself, exactly as `not-a-lane` does.
|
|
20
|
+
const UNDER_THRESHOLD_VERDICT = 'under-threshold'
|
|
21
|
+
const STALE_VERDICT = 'stale'
|
|
22
|
+
// The record matched the tree but carried no instruction to resume on, so the run was refused rather
|
|
23
|
+
// than continued blind to what it was told to do (joshuafolkken/kit#2354).
|
|
24
|
+
const INCOMPLETE_VERDICT = 'incomplete'
|
|
25
|
+
// The `--handoff` path was given but could not be read as a handoff, or was too large for the record;
|
|
26
|
+
// the cut is refused rather than taken without the instruction it was meant to carry.
|
|
27
|
+
const BAD_HANDOFF_VERDICT = 'bad-handoff'
|
|
28
|
+
const HANDED_OFF_VERDICT = 'handed-off'
|
|
29
|
+
const BUSY_VERDICT = 'busy'
|
|
30
|
+
const NOT_A_LANE_VERDICT = 'not-a-lane'
|
|
31
|
+
const UNREADY_VERDICT = 'unready'
|
|
32
|
+
const FAILED_VERDICT = 'failed'
|
|
33
|
+
const ENDED_VERDICT = 'ended'
|
|
34
|
+
const UNREADABLE_VERDICT = 'unreadable'
|
|
35
|
+
const UNKNOWN_VERDICT = 'unknown'
|
|
36
|
+
// The asking session is over the shared context-cut threshold, so an implementation cut was not
|
|
37
|
+
// resumed into it (joshuafolkken/kit#2760) — the same refusal `run:carry` answers `over` with.
|
|
38
|
+
const OVER_VERDICT = 'over'
|
|
39
|
+
|
|
40
|
+
function report(verdict: string, code: number): number {
|
|
41
|
+
console.info(verdict)
|
|
42
|
+
|
|
43
|
+
return code
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function report_stale(cut_record: RunCut): number {
|
|
47
|
+
console.error(run_cut.stale_message(cut_record))
|
|
48
|
+
|
|
49
|
+
return report(STALE_VERDICT, FAILURE_EXIT_CODE)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function report_busy(cut_record: RunCut): number {
|
|
53
|
+
console.error(run_cut.busy_message(cut_record))
|
|
54
|
+
|
|
55
|
+
return report(BUSY_VERDICT, FAILURE_EXIT_CODE)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The record matched the tree but carried no instruction; the run is refused rather than continued
|
|
59
|
+
// without what it was told to do (joshuafolkken/kit#2354).
|
|
60
|
+
function report_incomplete(cut_record: RunCut): number {
|
|
61
|
+
console.error(run_cut.incomplete_message(cut_record))
|
|
62
|
+
|
|
63
|
+
return report(INCOMPLETE_VERDICT, FAILURE_EXIT_CODE)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The `--handoff` file parsed but the assembled record would not fit the byte bound — the scalar fields
|
|
67
|
+
// carry a handoff that fit alone past the cap (joshuafolkken/kit#2354).
|
|
68
|
+
const HANDOFF_OVERFLOW_NOTE = '--handoff <path> would grow the cut record past its byte bound'
|
|
69
|
+
// An implementation cut resumes into implementation, and that resume refuses a record with no
|
|
70
|
+
// instruction (`incomplete`) — so the cut is refused here instead of relaunching a successor that can
|
|
71
|
+
// only stop (joshuafolkken/kit#2484).
|
|
72
|
+
const HANDOFF_MISSING_NOTE = 'this cut resumes into implementation and needs --handoff <path>'
|
|
73
|
+
|
|
74
|
+
function report_bad_handoff(note: string): number {
|
|
75
|
+
console.error(`${note}. Nothing was cut; write the handoff file and reissue.`)
|
|
76
|
+
|
|
77
|
+
return report(BAD_HANDOFF_VERDICT, FAILURE_EXIT_CODE)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// A successor already resumed this cut; a process woken after its own hand-off is told to stop rather
|
|
81
|
+
// than to investigate (joshuafolkken/kit#1935). It is a benign, non-failing stop.
|
|
82
|
+
function report_handed_off(cut_record: RunCut): number {
|
|
83
|
+
console.error(run_cut.handed_off_message(cut_record))
|
|
84
|
+
|
|
85
|
+
return report(HANDED_OFF_VERDICT, SUCCESS_EXIT_CODE)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function report_unreadable(): number {
|
|
89
|
+
console.error(run_cut.unreadable_message())
|
|
90
|
+
|
|
91
|
+
return report(UNREADABLE_VERDICT, FAILURE_EXIT_CODE)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function report_unknown(): number {
|
|
95
|
+
console.error(run_cut.unknown_message())
|
|
96
|
+
|
|
97
|
+
return report(UNKNOWN_VERDICT, FAILURE_EXIT_CODE)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A cut is only meaningful on a lane branch whose implementation is uncommitted. On the default
|
|
101
|
+
// branch, or with a clean tree, there is nothing to carry across the boundary.
|
|
102
|
+
function refuse_unready(state: CutState): number {
|
|
103
|
+
console.error(
|
|
104
|
+
`Not ready to cut on ${state.branch} (dirty: ${String(state.is_dirty)}); a cut needs an uncommitted lane branch. Nothing was cut.`,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return report(UNREADY_VERDICT, FAILURE_EXIT_CODE)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Below the shared context threshold there is no accumulation a cut would drop, so the resume it would
|
|
111
|
+
// relaunch costs more than it saves; the current process carries the run on to the gate uncut.
|
|
112
|
+
function report_under_threshold(): number {
|
|
113
|
+
console.error(
|
|
114
|
+
'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.',
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return report(UNDER_THRESHOLD_VERDICT, SUCCESS_EXIT_CODE)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// **An implementation cut is never resumed by a session over the threshold** (joshuafolkken/kit#2760).
|
|
121
|
+
// The cut outside a lane leaves the session that took it open, so a person retyping `fullrun #N` there
|
|
122
|
+
// would be answered `resume-impl`, keep implementing over the threshold, and be cut again on the next
|
|
123
|
+
// edit — a loop that sheds no context. The record is left intact for a fresh session.
|
|
124
|
+
function report_over(): number {
|
|
125
|
+
console.error(
|
|
126
|
+
'This session is over the shared context-cut threshold (`pnpm josh cost --cut` answers `over`), so the implementation cut was not resumed here and its record is kept. End this conversation and type the invocation again in a fresh session.',
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return report(OVER_VERDICT, FAILURE_EXIT_CODE)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const run_cut_report = {
|
|
133
|
+
BAD_HANDOFF_VERDICT,
|
|
134
|
+
BUSY_VERDICT,
|
|
135
|
+
CUT_VERDICT,
|
|
136
|
+
ENDED_VERDICT,
|
|
137
|
+
FAILED_VERDICT,
|
|
138
|
+
FAILURE_EXIT_CODE,
|
|
139
|
+
FRESH_VERDICT,
|
|
140
|
+
HANDED_OFF_VERDICT,
|
|
141
|
+
HANDOFF_MISSING_NOTE,
|
|
142
|
+
HANDOFF_OVERFLOW_NOTE,
|
|
143
|
+
INCOMPLETE_VERDICT,
|
|
144
|
+
NOT_A_LANE_VERDICT,
|
|
145
|
+
OVER_VERDICT,
|
|
146
|
+
RESUME_IMPL_VERDICT,
|
|
147
|
+
RESUME_VERDICT,
|
|
148
|
+
STALE_VERDICT,
|
|
149
|
+
SUCCESS_EXIT_CODE,
|
|
150
|
+
UNDER_THRESHOLD_VERDICT,
|
|
151
|
+
UNKNOWN_VERDICT,
|
|
152
|
+
UNREADABLE_VERDICT,
|
|
153
|
+
UNREADY_VERDICT,
|
|
154
|
+
refuse_unready,
|
|
155
|
+
report,
|
|
156
|
+
report_bad_handoff,
|
|
157
|
+
report_busy,
|
|
158
|
+
report_handed_off,
|
|
159
|
+
report_incomplete,
|
|
160
|
+
report_over,
|
|
161
|
+
report_stale,
|
|
162
|
+
report_under_threshold,
|
|
163
|
+
report_unknown,
|
|
164
|
+
report_unreadable,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { run_cut_report }
|
package/scripts/run/run-cut.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { stamp_file } from '#scripts/josh/stamp-file'
|
|
|
9
9
|
import { lane_child_invocation } from '#scripts/lane/lane-child-invocation'
|
|
10
10
|
import { z } from 'zod'
|
|
11
11
|
import { run_cut_handoff, type Handoff } from './run-cut-handoff'
|
|
12
|
-
import { run_hold } from './run-hold'
|
|
12
|
+
import { run_hold, type RunHold } from './run-hold'
|
|
13
13
|
|
|
14
14
|
// joshuafolkken/kit#1839: a lane child is a detached `fullrun #<N>` process, and the thinking it
|
|
15
15
|
// accumulates while implementing rides on every later API call in the same session — measured at 176K
|
|
@@ -118,6 +118,8 @@ interface CutState {
|
|
|
118
118
|
branch: string
|
|
119
119
|
is_dirty: boolean
|
|
120
120
|
is_held: boolean
|
|
121
|
+
// The issue a `fullrun` hold names, absent when the tree is not held by one (joshuafolkken/kit#2760).
|
|
122
|
+
held_issue?: string | undefined
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
// The fields a cut is written from, bundled so `begin_cut` stays within the parameter limit. `phase`
|
|
@@ -432,22 +434,34 @@ function end_cut(target: string): void {
|
|
|
432
434
|
stamp_file.remove_stamp(target)
|
|
433
435
|
}
|
|
434
436
|
|
|
435
|
-
//
|
|
437
|
+
// The run hold still standing over this tree — present as `held` or as an aged-out `stale`, but
|
|
436
438
|
// present. A tree with no hold at all is not the one a cut was taken in, so the resume refuses it.
|
|
437
|
-
function
|
|
438
|
-
if (directory === undefined) return
|
|
439
|
+
function hold_of(directory: string | undefined): RunHold | undefined {
|
|
440
|
+
if (directory === undefined) return undefined
|
|
439
441
|
|
|
440
442
|
const read = run_hold.read_hold(run_hold.hold_path(directory))
|
|
441
443
|
|
|
442
|
-
return read.kind === 'held' || read.kind === 'stale'
|
|
444
|
+
return read.kind === 'held' || read.kind === 'stale' ? read.hold : undefined
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// The issue a `fullrun` holds this tree for — what tells a `fullrun` held in its own checkout apart from
|
|
448
|
+
// a person's tree (joshuafolkken/kit#2760). A `halfrun` or an in-session `backlogrun` child holds it
|
|
449
|
+
// too, but its cut would be resumed as `fullrun #N`, so only a hold `run:entry` marked names an issue.
|
|
450
|
+
function fullrun_issue(hold: RunHold | undefined): string | undefined {
|
|
451
|
+
return hold?.is_fullrun === true ? hold.issue : undefined
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// The same read taken synchronously, for the `PreToolUse` implementation-cut guard.
|
|
455
|
+
function held_issue_sync(): string | undefined {
|
|
456
|
+
return fullrun_issue(hold_of(worktree_git_directory_sync()))
|
|
443
457
|
}
|
|
444
458
|
|
|
445
459
|
async function current_state(): Promise<CutState> {
|
|
446
460
|
const branch = await git_command.branch()
|
|
447
461
|
const is_dirty = await run_hold.is_tree_dirty()
|
|
448
|
-
const
|
|
462
|
+
const hold = hold_of(await worktree_directory())
|
|
449
463
|
|
|
450
|
-
return { branch, is_dirty, is_held:
|
|
464
|
+
return { branch, is_dirty, is_held: hold !== undefined, held_issue: fullrun_issue(hold) }
|
|
451
465
|
}
|
|
452
466
|
|
|
453
467
|
function describe_cut(cut: RunCut): string {
|
|
@@ -508,8 +522,10 @@ const run_cut = {
|
|
|
508
522
|
describe_cut,
|
|
509
523
|
end_cut,
|
|
510
524
|
fresh_cut,
|
|
525
|
+
fullrun_issue,
|
|
511
526
|
handed_off_message,
|
|
512
527
|
has_required_handoff,
|
|
528
|
+
held_issue_sync,
|
|
513
529
|
incomplete_message,
|
|
514
530
|
invocation_for,
|
|
515
531
|
is_expired,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
4
4
|
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
5
|
+
import { run_cut_report } from './run-cut-report'
|
|
5
6
|
import { run_entry, type EntryParts } from './run-entry'
|
|
6
7
|
import { run_hold_cli } from './run-hold-cli'
|
|
7
8
|
import { run_next } from './run-next'
|
|
@@ -50,9 +51,13 @@ function first_line(out: string): string {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
// `run:hold <N>` prints one token — `hold`, `busy` or `unknown` — and forwards its explanation to
|
|
53
|
-
// stderr, so the composite branches on the token and the reader still sees why.
|
|
54
|
+
// stderr, so the composite branches on the token and the reader still sees why. `--fullrun` marks the
|
|
55
|
+
// hold as this entry's, since `run:entry` is `fullrun #N`'s alone (joshuafolkken/kit#2760).
|
|
54
56
|
async function claim_hold(issue_number: string): Promise<string> {
|
|
55
|
-
const held = await josh_command.josh_run(
|
|
57
|
+
const held = await josh_command.josh_run(
|
|
58
|
+
['run:hold', issue_number, '--fullrun'],
|
|
59
|
+
should_forward_stderr,
|
|
60
|
+
)
|
|
56
61
|
|
|
57
62
|
return first_line(held.out)
|
|
58
63
|
}
|
|
@@ -96,7 +101,36 @@ function stop_report(issue_number: string, hold: string, cost: string, report: s
|
|
|
96
101
|
return emit({ issue_number, hold, cost, verdict: run_entry.NO_VERDICT, report })
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
interface Resume {
|
|
105
|
+
token: string
|
|
106
|
+
code: number
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// **A resume is asked before the hold is claimed** (joshuafolkken/kit#2760). An implementation cut
|
|
110
|
+
// outside a lane keeps its hold, so the fresh session's `fullrun #N` would be refused `busy` by its own
|
|
111
|
+
// run's hold before it ever reached `run:cut --resume` — the hand-off the cut promised could not land.
|
|
112
|
+
async function ask_resume(issue_number: string): Promise<Resume> {
|
|
113
|
+
const asked = await josh_command.josh_run(
|
|
114
|
+
['run:cut', '--resume', issue_number],
|
|
115
|
+
should_forward_stderr,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return { token: first_line(asked.out), code: asked.code }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Anything but `fresh` is `run:cut --resume`'s answer to act on (`pre-gate-cut.md`), not a new run: the
|
|
122
|
+
// token and its exit code are passed through, and nothing is claimed or read.
|
|
123
|
+
function resume_report(issue_number: string, resume: Resume): number {
|
|
124
|
+
console.info(`entry #${issue_number} — resume: ${resume.token}`)
|
|
125
|
+
|
|
126
|
+
return resume.code
|
|
127
|
+
}
|
|
128
|
+
|
|
99
129
|
async function open_run(issue_number: string): Promise<number> {
|
|
130
|
+
const resume = await ask_resume(issue_number)
|
|
131
|
+
|
|
132
|
+
if (resume.token !== run_cut_report.FRESH_VERDICT) return resume_report(issue_number, resume)
|
|
133
|
+
|
|
100
134
|
const hold = await claim_hold(issue_number)
|
|
101
135
|
|
|
102
136
|
if (hold !== run_hold_cli.HOLD_VERDICT) {
|