@joshuafolkken/kit 1.1379.0 → 1.1387.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/backlogrun-progress.md +2 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -4
- package/.claude/skills/workflow-commands/retrospective.md +1 -2
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-RW4XMF47.js → chunk-2YSW3CXH.js} +133 -234
- package/dist/hooks/{chunk-4RWRP3FF.js → chunk-DCY2GWHC.js} +5 -5
- package/dist/hooks/{chunk-DHSKBAQ2.js → chunk-EAQEGME7.js} +1 -1
- package/dist/hooks/{chunk-E7LZL5AN.js → chunk-EBSHN25W.js} +3 -3
- package/dist/hooks/{chunk-HGA4EG5S.js → chunk-GJ7JPIHN.js} +15 -15
- package/dist/hooks/{chunk-TG2AB3W6.js → chunk-GNDJ7NJ7.js} +88 -501
- package/dist/hooks/{chunk-546X4PX3.js → chunk-IYVOLN6A.js} +3 -3
- package/dist/hooks/{chunk-NBEAGPS4.js → chunk-J3KSS7II.js} +1 -1
- package/dist/hooks/{chunk-7ZM464OW.js → chunk-LMWBRJIP.js} +3 -3
- package/dist/hooks/{chunk-TORRSNDJ.js → chunk-SL4GQ2XP.js} +3 -3
- package/dist/hooks/{chunk-PV3FWDSQ.js → chunk-STDSEPWU.js} +2 -2
- package/dist/hooks/{chunk-YSDAH73S.js → chunk-U32VSRNG.js} +420 -4
- package/dist/hooks/{chunk-QARPP36N.js → chunk-WNFJLBXI.js} +3 -3
- package/dist/hooks/codex-hook-adapter.js +13 -13
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +3 -3
- package/dist/hooks/inputs.json +213 -212
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +65 -9
- package/dist/hooks/stop-guard.js +104 -32
- package/dist/josh.js +2 -2
- package/package.json +1 -1
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/scripts/backlog/backlog-budget-cli.ts +2 -20
- package/scripts/backlog/backlog-budget.ts +72 -109
- package/scripts/backlog/backlog-drive-cli.ts +12 -7
- package/scripts/backlog/backlog-drive-launch.ts +2 -0
- package/scripts/backlog/backlog-drive-owner.ts +72 -5
- package/scripts/backlog/backlog-drive.ts +5 -2
- package/scripts/backlog/backlog-offer-cli.ts +1 -15
- package/scripts/backlog/backlogrun-launch.ts +56 -5
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/epic/epic-candidate-confirm.ts +1 -1
- package/scripts/epic/epic-cross-repo.ts +4 -4
- package/scripts/epic/epic-next.ts +1 -1
- package/scripts/hooks/stop-guard.ts +20 -9
- package/scripts/init/start-plan.ts +15 -4
- package/scripts/init/start-steps.ts +31 -3
- package/scripts/init/start.ts +20 -13
- package/scripts/josh/josh-commands-ai.ts +1 -1
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/lane/lane-cli.ts +1 -3
- package/scripts/run/board/run-board-header.ts +137 -88
- package/scripts/run/board/run-board-labels.ts +18 -17
- package/scripts/run/board/run-board-machine.ts +21 -8
- package/scripts/run/board/run-board-read.ts +9 -2
- package/scripts/run/board/run-board-render.ts +1 -5
- package/scripts/run/board/run-board-setup.ts +73 -0
- package/scripts/run/board/run-board-span.ts +49 -8
- package/scripts/run/board/run-board-status.ts +10 -23
- package/scripts/run/board/run-board-tick.ts +11 -2
- package/scripts/run/carry/run-carry-added.ts +1 -1
- package/scripts/run/carry/run-carry-change.ts +1 -1
- package/scripts/run/carry/run-carry-cli.ts +36 -60
- package/scripts/run/carry/run-carry-ended.ts +10 -2
- package/scripts/run/carry/run-carry.ts +38 -68
- package/scripts/run/cut/run-cut.ts +4 -4
- package/scripts/run/event/run-event-render.ts +1 -1
- package/scripts/run/event/run-event-scope.ts +4 -8
- package/scripts/run/event/run-event-setup.ts +47 -0
- package/scripts/run/event/run-event-stream-emit.ts +0 -25
- package/scripts/run/event/run-event-stream.ts +41 -28
- package/scripts/run/merge/run-merge-collect.ts +1 -1
- package/scripts/run/merge/run-merge-steps.ts +1 -1
- package/scripts/run/progress/run-progress-read.ts +1 -1
- package/scripts/run/run-headless.ts +2 -2
- package/scripts/run/run-status-cli.ts +1 -5
- package/scripts/run/run-step.ts +2 -9
- package/scripts/run/run-stop-notify.ts +1 -1
- package/scripts/run/run-stranded-detect.ts +61 -15
- package/scripts/run/run-stranded.ts +25 -17
- package/scripts/run/wake/run-wake-cli.ts +58 -15
- package/scripts/run/wake/run-wake-describe.ts +5 -5
- package/scripts/run/wake/run-wake-driver.ts +13 -4
- package/scripts/run/wake/run-wake-handoff.ts +1 -1
- package/scripts/run/wake/run-wake-loop.ts +4 -21
- package/scripts/run/wake/run-wake-restart.ts +1 -1
- package/scripts/run/wake/run-wake.ts +6 -7
- package/scripts/backlog/backlog-idle.ts +0 -88
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PendingLaunch } from '#scripts/backlog/backlogrun-launch'
|
|
1
2
|
import type { CarryRead } from '#scripts/run/carry/run-carry'
|
|
2
3
|
import type { EndedRun } from '#scripts/run/carry/run-carry-ended'
|
|
3
4
|
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
@@ -27,10 +28,40 @@ function ended_span(ended: EndedRun | undefined): BoardSpan | undefined {
|
|
|
27
28
|
return { invocation: ended.invocation, started_ms, ended_ms }
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// A run `josh backlogrun` launched and its parent has not claimed yet: it has no carry record, so the
|
|
32
|
+
// launch record is the only one that says a run is here and since when.
|
|
33
|
+
function launch_span(launch: PendingLaunch | undefined): BoardSpan | undefined {
|
|
34
|
+
const started_ms = launch === undefined ? undefined : moment_of(launch.started_at)
|
|
35
|
+
|
|
36
|
+
if (launch === undefined || started_ms === undefined) return undefined
|
|
37
|
+
|
|
38
|
+
return { invocation: launch.invocation, started_ms, ended_ms: undefined }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// With no carry record: the run on its way, else the one that last ended. **A launch older than that
|
|
42
|
+
// end is the ended run's own** — its parent outlives `run:carry --end` for a moment — so it never
|
|
43
|
+
// redraws a finished run as one starting.
|
|
44
|
+
function idle_span(
|
|
45
|
+
ended: EndedRun | undefined,
|
|
46
|
+
launch: PendingLaunch | undefined,
|
|
47
|
+
): BoardSpan | undefined {
|
|
48
|
+
const last = ended_span(ended)
|
|
49
|
+
const coming = launch_span(launch)
|
|
50
|
+
|
|
51
|
+
if (coming === undefined) return last
|
|
52
|
+
|
|
53
|
+
return (last?.ended_ms ?? 0) >= coming.started_ms ? last : coming
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
// The running run wins over the ended one, so a run that begins replaces the finished board at once.
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
// **The launch's start is the carry's**: `run:carry --begin` inherits it for the parent the launch
|
|
58
|
+
// started, so the clock does not restart when the record appears.
|
|
59
|
+
function span_of(
|
|
60
|
+
read: CarryRead,
|
|
61
|
+
ended: EndedRun | undefined,
|
|
62
|
+
launch?: PendingLaunch,
|
|
63
|
+
): BoardSpan | undefined {
|
|
64
|
+
if (read.kind !== 'carried') return read.kind === 'none' ? idle_span(ended, launch) : undefined
|
|
34
65
|
|
|
35
66
|
const started_ms = moment_of(read.carry.started_at)
|
|
36
67
|
|
|
@@ -39,12 +70,22 @@ function span_of(read: CarryRead, ended: EndedRun | undefined): BoardSpan | unde
|
|
|
39
70
|
return { invocation: read.carry.invocation, started_ms, ended_ms: undefined }
|
|
40
71
|
}
|
|
41
72
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
73
|
+
// Whether the span drawn with no carry record is the ended run's, rather than a launch's.
|
|
74
|
+
function is_ended_drawn(ended: EndedRun | undefined, launch: PendingLaunch | undefined): boolean {
|
|
75
|
+
return idle_span(ended, launch)?.ended_ms !== undefined
|
|
76
|
+
}
|
|
46
77
|
|
|
47
|
-
|
|
78
|
+
// The session a person resumes a stopped run from: only while the ended run is
|
|
79
|
+
// the one drawn, and only when it stopped rather than finished. A run that begins — or is on its way —
|
|
80
|
+
// hides it at once.
|
|
81
|
+
function resume_of(
|
|
82
|
+
read: CarryRead,
|
|
83
|
+
ended: EndedRun | undefined,
|
|
84
|
+
launch?: PendingLaunch,
|
|
85
|
+
): string | undefined {
|
|
86
|
+
if (read.kind !== 'none' || !is_ended_drawn(ended, launch)) return undefined
|
|
87
|
+
|
|
88
|
+
return ended?.stopped === undefined ? undefined : ended.session
|
|
48
89
|
}
|
|
49
90
|
|
|
50
91
|
function is_within(event: RunEvent, span: BoardSpan): boolean {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { backlog_idle, type IdleWindow } from '#scripts/backlog/backlog-idle'
|
|
2
1
|
import {
|
|
3
2
|
has_label_name,
|
|
4
3
|
IN_PROGRESS_LABEL,
|
|
@@ -8,6 +7,7 @@ import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
|
8
7
|
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
9
8
|
import type { ClosedIssue } from './run-board-closed'
|
|
10
9
|
import { run_board_phase, type Phase } from './run-board-phase'
|
|
10
|
+
import { run_board_setup, type SetupRead } from './run-board-setup'
|
|
11
11
|
|
|
12
12
|
// What each issue of a run is doing now, read from the run's own event stream and its open lanes.
|
|
13
13
|
// Local reads only — no `gh` call — so `run:board` can redraw it every few seconds; the plan these
|
|
@@ -27,11 +27,10 @@ interface ItemStatus {
|
|
|
27
27
|
track?: ReadonlyArray<Phase> | undefined
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// The run as a whole, read off the same stream.
|
|
31
|
-
|
|
30
|
+
// The run as a whole, read off the same stream. The setup half is `run-board-setup.ts`'s, absent
|
|
31
|
+
// where a stream carries no stage.
|
|
32
|
+
interface RunActivity extends Partial<SetupRead> {
|
|
32
33
|
last_event_ms: number | undefined
|
|
33
|
-
// The idle watch's window, when the newest one opened after the newest launch.
|
|
34
|
-
idle: IdleWindow | undefined
|
|
35
34
|
// The run's last position is a `stop` that names no child — the run itself ended.
|
|
36
35
|
is_stopped: boolean
|
|
37
36
|
}
|
|
@@ -128,16 +127,17 @@ function fold_events(events: ReadonlyArray<RunEvent>): Map<number, ItemStatus> {
|
|
|
128
127
|
function is_running_lane(
|
|
129
128
|
issue: number,
|
|
130
129
|
status: ItemStatus | undefined,
|
|
130
|
+
events: ReadonlyArray<RunEvent>,
|
|
131
131
|
in_run: ReadonlySet<number>,
|
|
132
132
|
): boolean {
|
|
133
|
-
if (status
|
|
133
|
+
if (status !== undefined) return status.state === 'running'
|
|
134
134
|
|
|
135
|
-
return
|
|
135
|
+
return in_run.has(issue) && !run_board_setup.is_lane_pending(events, issue)
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// Every child the run has touched, with the lane each running child holds. A lane the stream never saw
|
|
139
139
|
// launched — a run restored after the stream rolled — still reads as running while its issue is in the
|
|
140
|
-
// run's plan (`in_run`).
|
|
140
|
+
// run's plan (`in_run`); one the run is still setting up for stays waiting until its launch.
|
|
141
141
|
function statuses_of(
|
|
142
142
|
events: ReadonlyArray<RunEvent>,
|
|
143
143
|
lanes: ReadonlyArray<string>,
|
|
@@ -149,7 +149,7 @@ function statuses_of(
|
|
|
149
149
|
const issue = Number(lane)
|
|
150
150
|
const status = statuses.get(issue)
|
|
151
151
|
|
|
152
|
-
if (is_running_lane(issue, status, in_run)) {
|
|
152
|
+
if (is_running_lane(issue, status, events, in_run)) {
|
|
153
153
|
statuses.set(issue, { state: 'running', ...status, lane: `lane ${lane}` })
|
|
154
154
|
}
|
|
155
155
|
}
|
|
@@ -243,19 +243,6 @@ function settle_closed(
|
|
|
243
243
|
return new Map([...statuses].map(([issue, status]) => [issue, settled(issue, status, read)]))
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
function newest_of(events: ReadonlyArray<RunEvent>, kind: string): RunEvent | undefined {
|
|
247
|
-
return events.findLast((event) => event.kind === kind)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function idle_of(events: ReadonlyArray<RunEvent>): IdleWindow | undefined {
|
|
251
|
-
const idle = newest_of(events, KIND.IDLE)
|
|
252
|
-
const launch = newest_of(events, KIND.CHILD_LAUNCH)
|
|
253
|
-
|
|
254
|
-
if (idle === undefined || (launch !== undefined && launch.pos > idle.pos)) return undefined
|
|
255
|
-
|
|
256
|
-
return backlog_idle.parse(idle.text)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
246
|
function is_run_stop(event: RunEvent | undefined): boolean {
|
|
260
247
|
return event?.kind === KIND.STOP && run_event_scope.issue_named(event) === undefined
|
|
261
248
|
}
|
|
@@ -267,8 +254,8 @@ function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActi
|
|
|
267
254
|
const position = events.findLast((event) => !run_event_stream.TRACE_KINDS.has(event.kind))
|
|
268
255
|
|
|
269
256
|
return {
|
|
257
|
+
...run_board_setup.read(events),
|
|
270
258
|
last_event_ms: last === undefined ? undefined : moment(last),
|
|
271
|
-
idle: idle_of(events),
|
|
272
259
|
is_stopped: is_ended || is_run_stop(position),
|
|
273
260
|
}
|
|
274
261
|
}
|
|
@@ -11,7 +11,7 @@ import type { LocalRead } from './run-board-read'
|
|
|
11
11
|
import { run_board_render, type FrameBounds } from './run-board-render'
|
|
12
12
|
import { run_board_spin, type Spun } from './run-board-spin'
|
|
13
13
|
import { run_board_state, type BoardPorts, type BoardState, type Pace } from './run-board-state'
|
|
14
|
-
import { run_board_status } from './run-board-status'
|
|
14
|
+
import { run_board_status, type RunActivity } from './run-board-status'
|
|
15
15
|
import { run_board_usage } from './run-board-usage'
|
|
16
16
|
|
|
17
17
|
// One `run:board` redraw. **Three speeds**: the frame is redrawn once a second, on the second, so
|
|
@@ -116,6 +116,14 @@ interface Redraw {
|
|
|
116
116
|
now_ms: number
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// A run `run:carry --end` closed and one nobody drives any more are both drawn as stopped.
|
|
120
|
+
function activity_here(local: LocalRead): RunActivity {
|
|
121
|
+
return run_board_status.activity_of(
|
|
122
|
+
local.events,
|
|
123
|
+
local.ended_ms !== undefined || local.is_stranded,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
119
127
|
function header_of(
|
|
120
128
|
state: BoardState,
|
|
121
129
|
local: LocalRead,
|
|
@@ -126,7 +134,7 @@ function header_of(
|
|
|
126
134
|
now_ms: redraw.now_ms,
|
|
127
135
|
started_ms: local.started_ms,
|
|
128
136
|
ended_ms: local.ended_ms,
|
|
129
|
-
activity:
|
|
137
|
+
activity: activity_here(local),
|
|
130
138
|
layout,
|
|
131
139
|
baseline_total: state.baseline_total,
|
|
132
140
|
plan_fetched_ms: state.fetched_ms,
|
|
@@ -135,6 +143,7 @@ function header_of(
|
|
|
135
143
|
machine: state.gauges,
|
|
136
144
|
usages: state.usages,
|
|
137
145
|
form: redraw.ports.form,
|
|
146
|
+
repo: state.plan?.context.repo,
|
|
138
147
|
spinner: redraw.ports.is_tty ? SPOT : undefined,
|
|
139
148
|
link: run_board_link.linker(state.plan?.context.repo, redraw.ports.link),
|
|
140
149
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { backlogrun_launch } from '#scripts/backlog/backlogrun-launch'
|
|
3
4
|
import { cost_cli } from '#scripts/cost-runtime/cost-cli'
|
|
4
5
|
import { cost_verdict } from '#scripts/cost-runtime/cost-verdict'
|
|
6
|
+
import { run_event_setup } from '#scripts/run/event/run-event-setup'
|
|
5
7
|
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
6
8
|
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
7
9
|
import { run_merge_collect } from '#scripts/run/merge/run-merge-collect'
|
|
@@ -39,7 +41,6 @@ const RESUMED_VERDICT = 'resumed'
|
|
|
39
41
|
const CARRIED_VERDICT = 'carried'
|
|
40
42
|
const COUNTED_VERDICT = 'counted'
|
|
41
43
|
const ENDED_VERDICT = 'ended'
|
|
42
|
-
const EXPIRED_VERDICT = 'expired'
|
|
43
44
|
const NONE_VERDICT = 'none'
|
|
44
45
|
const UNREADABLE_VERDICT = 'unreadable'
|
|
45
46
|
const MISMATCH_VERDICT = 'mismatch'
|
|
@@ -94,28 +95,14 @@ function report_unreadable(is_json: boolean): number {
|
|
|
94
95
|
return report(UNREADABLE_VERDICT, undefined, is_json, FAILURE_EXIT_CODE)
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
function report_expired(carry: RunCarry, is_json: boolean): number {
|
|
98
|
-
console.error(run_carry.expired_message(carry))
|
|
99
|
-
|
|
100
|
-
return report_carry(EXPIRED_VERDICT, carry, is_json)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
98
|
function report_read(read: CarryRead, is_json: boolean): number {
|
|
104
99
|
if (read.kind === 'none') return report(NONE_VERDICT, undefined, is_json, SUCCESS_EXIT_CODE)
|
|
105
100
|
|
|
106
101
|
if (read.kind === 'unreadable') return report_unreadable(is_json)
|
|
107
102
|
|
|
108
|
-
if (read.kind === 'expired') return report_expired(read.carry, is_json)
|
|
109
|
-
|
|
110
103
|
return report_carry(CARRIED_VERDICT, read.carry, is_json)
|
|
111
104
|
}
|
|
112
105
|
|
|
113
|
-
function report_busy(carry: RunCarry, is_json: boolean): number {
|
|
114
|
-
console.error(run_carry.busy_message(carry))
|
|
115
|
-
|
|
116
|
-
return report(BUSY_VERDICT, carry, is_json, FAILURE_EXIT_CODE)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
106
|
function refusal_message(claim: CarryClaim, carry: RunCarry, invocation: string): string {
|
|
120
107
|
if (claim === 'busy') return run_carry.busy_message(carry)
|
|
121
108
|
|
|
@@ -149,7 +136,7 @@ function report_adoption(
|
|
|
149
136
|
|
|
150
137
|
// **Whose record it is, is the classifier's answer and not this command's guess.** `--end` is only
|
|
151
138
|
// reached on the clean-finish path, so a run that crashed or stopped on a guard leaves its record
|
|
152
|
-
// standing
|
|
139
|
+
// standing until a person carries or discards it; `resumed` is reached only where the run itself declared the
|
|
153
140
|
// cut, or where `--resume` says the reader has decided to carry it.
|
|
154
141
|
function report_claim(
|
|
155
142
|
target: string,
|
|
@@ -166,59 +153,47 @@ function report_claim(
|
|
|
166
153
|
return report(CLAIM_VERDICTS[claim], carry, is_json, FAILURE_EXIT_CODE)
|
|
167
154
|
}
|
|
168
155
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
is_json: boolean,
|
|
174
|
-
): number {
|
|
175
|
-
const carry = is_replacement
|
|
176
|
-
? run_carry.replace_carry(target, request.invocation, request.owner)
|
|
177
|
-
: run_carry.begin_carry(target, request.invocation, request.owner)
|
|
156
|
+
// When the run began: the moment `josh backlogrun` launched this owner, so the clock `run:board` was
|
|
157
|
+
// already counting from does not restart at the claim; `undefined` — now — for every other run.
|
|
158
|
+
async function launch_start(request: CarryClaimRequest): Promise<Date | undefined> {
|
|
159
|
+
const directory = await run_carry.repository_directory()
|
|
178
160
|
|
|
179
|
-
if (
|
|
161
|
+
if (directory === undefined) return undefined
|
|
180
162
|
|
|
181
|
-
return
|
|
163
|
+
return backlogrun_launch.started_for(directory, request.owner.pid)
|
|
182
164
|
}
|
|
183
165
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
// record. Only that run, or a person who knows it is over, ends it.
|
|
187
|
-
function begin_over_expired(
|
|
166
|
+
// A run that began is planning, which is what its board says until the supervisor takes it over.
|
|
167
|
+
async function start_fresh(
|
|
188
168
|
target: string,
|
|
189
|
-
carry: RunCarry,
|
|
190
169
|
request: CarryClaimRequest,
|
|
191
170
|
is_json: boolean,
|
|
192
|
-
): number {
|
|
193
|
-
|
|
171
|
+
): Promise<number> {
|
|
172
|
+
const started = await launch_start(request)
|
|
173
|
+
const carry = run_carry.begin_carry(target, request.invocation, request.owner, started)
|
|
194
174
|
|
|
195
|
-
|
|
196
|
-
// Replaced here, the record would come back with `started_at` set to now — and `backlog:budget
|
|
197
|
-
// --started` reads that field, so the 8-hour bound would restart at every cut and never end the
|
|
198
|
-
// run. The answer is `expired`, and a person who really is starting a new run clears the record
|
|
199
|
-
// with `--end`, which is what the expiry message says.
|
|
200
|
-
if (carry.is_handed_off === true) return report_expired(carry, is_json)
|
|
175
|
+
if (carry === undefined) return report_lost(is_json)
|
|
201
176
|
|
|
202
|
-
|
|
177
|
+
await run_event_setup.emit(run_event_setup.STAGE.PLANNING)
|
|
203
178
|
|
|
204
|
-
return
|
|
179
|
+
return report_carry(BEGAN_VERDICT, carry, is_json)
|
|
205
180
|
}
|
|
206
181
|
|
|
207
|
-
// **A
|
|
208
|
-
// restart the budget the cut exists to carry, and resuming into one would put two parents on a
|
|
209
|
-
// budget.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
182
|
+
// **A standing record is never replaced, and never resumed into by something else.** Replacing one
|
|
183
|
+
// would restart the budget the cut exists to carry, and resuming into one would put two parents on a
|
|
184
|
+
// single budget. Its age changes none of that: a record is replaced by nothing but a person's `--end`.
|
|
185
|
+
async function begin(
|
|
186
|
+
target: string,
|
|
187
|
+
request: CarryClaimRequest,
|
|
188
|
+
is_json: boolean,
|
|
189
|
+
): Promise<number> {
|
|
213
190
|
const read = run_carry.read_carry(target)
|
|
214
191
|
|
|
215
192
|
if (read.kind === 'unreadable') return report_unreadable(is_json)
|
|
216
193
|
|
|
217
194
|
if (read.kind === 'carried') return report_claim(target, read.carry, request, is_json)
|
|
218
195
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return start_fresh(target, request, false, is_json)
|
|
196
|
+
return await start_fresh(target, request, is_json)
|
|
222
197
|
}
|
|
223
198
|
|
|
224
199
|
function report_nothing_to_resume(is_json: boolean): number {
|
|
@@ -237,8 +212,6 @@ function adopt(target: string, request: CarryClaimRequest, is_json: boolean): nu
|
|
|
237
212
|
|
|
238
213
|
if (read.kind === 'unreadable') return report_unreadable(is_json)
|
|
239
214
|
|
|
240
|
-
if (read.kind === 'expired') return report_expired(read.carry, is_json)
|
|
241
|
-
|
|
242
215
|
return report_claim(target, read.carry, request, is_json)
|
|
243
216
|
}
|
|
244
217
|
|
|
@@ -254,10 +227,16 @@ function report_over(is_json: boolean): number {
|
|
|
254
227
|
return report(OVER_VERDICT, undefined, is_json, FAILURE_EXIT_CODE)
|
|
255
228
|
}
|
|
256
229
|
|
|
257
|
-
function claim_record(
|
|
230
|
+
async function claim_record(
|
|
231
|
+
target: string,
|
|
232
|
+
request: CarryClaimRequest,
|
|
233
|
+
is_json: boolean,
|
|
234
|
+
): Promise<number> {
|
|
258
235
|
if (cost_cli.session_verdict() === cost_verdict.OVER_VERDICT) return report_over(is_json)
|
|
259
236
|
|
|
260
|
-
|
|
237
|
+
if (request.is_adoption) return adopt(target, request, is_json)
|
|
238
|
+
|
|
239
|
+
return await begin(target, request, is_json)
|
|
261
240
|
}
|
|
262
241
|
|
|
263
242
|
// The record is no longer this session's to advance — handed off at a cut, or taken over by a live
|
|
@@ -334,9 +313,7 @@ async function count(
|
|
|
334
313
|
|
|
335
314
|
await record_retrospective(request)
|
|
336
315
|
|
|
337
|
-
return
|
|
338
|
-
? report_expired(carry, is_json)
|
|
339
|
-
: report_carry(COUNTED_VERDICT, carry, is_json)
|
|
316
|
+
return report_carry(COUNTED_VERDICT, carry, is_json)
|
|
340
317
|
}
|
|
341
318
|
|
|
342
319
|
// **A stop over a record that is still there pushes one ⏸️ confirmation before the record goes**.
|
|
@@ -400,7 +377,7 @@ async function act(
|
|
|
400
377
|
request: Exclude<Request, { kind: 'end' }>,
|
|
401
378
|
is_json: boolean,
|
|
402
379
|
): Promise<number> {
|
|
403
|
-
if (request.kind === 'claim') return claim_record(target, request.claim, is_json)
|
|
380
|
+
if (request.kind === 'claim') return await claim_record(target, request.claim, is_json)
|
|
404
381
|
|
|
405
382
|
const read = run_carry.read_carry(target)
|
|
406
383
|
|
|
@@ -463,7 +440,6 @@ const run_carry_cli = {
|
|
|
463
440
|
CARRIED_VERDICT,
|
|
464
441
|
COUNTED_VERDICT,
|
|
465
442
|
ENDED_VERDICT,
|
|
466
|
-
EXPIRED_VERDICT,
|
|
467
443
|
MISMATCH_VERDICT,
|
|
468
444
|
NONE_VERDICT,
|
|
469
445
|
OVER_VERDICT,
|
|
@@ -55,7 +55,7 @@ function record_ended(
|
|
|
55
55
|
now: Date = new Date(),
|
|
56
56
|
stopped?: string,
|
|
57
57
|
): void {
|
|
58
|
-
if (read.kind !== 'carried'
|
|
58
|
+
if (read.kind !== 'carried') return
|
|
59
59
|
|
|
60
60
|
const { invocation, started_at, owner_transcript } = read.carry
|
|
61
61
|
const ended_at = now.toISOString()
|
|
@@ -79,7 +79,15 @@ function read_ended(target: string): EndedRun | undefined {
|
|
|
79
79
|
return raw === undefined ? undefined : parse_ended(raw)
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
// Whether the run that began at `started_at` is the one `--end` last ended — what tells a record that is
|
|
83
|
+
// gone because its run ended from one that only failed to read.
|
|
84
|
+
function has_ended(target: string, started_at: string | undefined): boolean {
|
|
85
|
+
const ended = read_ended(target)
|
|
86
|
+
|
|
87
|
+
return ended !== undefined && ended.started_at === started_at
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const run_carry_ended = { ended_path, has_ended, read_ended, record_ended }
|
|
83
91
|
|
|
84
92
|
export { run_carry_ended }
|
|
85
93
|
export type { EndedRun }
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { backlog_budget } from '#scripts/backlog/backlog-budget'
|
|
2
1
|
import { git_command } from '#scripts/git/git-command'
|
|
3
2
|
import { process_identity } from '#scripts/josh/process-identity'
|
|
4
3
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
5
|
-
import { stamp_record, type StampRecordSpec } from '#scripts/josh/stamp-record'
|
|
6
4
|
import { json_value } from '#scripts/lib/json-value'
|
|
7
5
|
import { run_invocation } from '#scripts/run/run-invocation'
|
|
8
6
|
import { z } from 'zod'
|
|
@@ -10,7 +8,7 @@ import { run_carry_added, type CarryAddition } from './run-carry-added'
|
|
|
10
8
|
import { run_carry_change } from './run-carry-change'
|
|
11
9
|
import { run_carry_conversation } from './run-carry-conversation'
|
|
12
10
|
|
|
13
|
-
// A `backlogrun` declares a budget — `--max
|
|
11
|
+
// A `backlogrun` declares a budget — `--max` and `--idle` — and the session
|
|
14
12
|
// cut ends the session, so the next one would start from nothing. **The cut is an execution detail of
|
|
15
13
|
// the same authorization**, so the budget has to survive it: this record carries it across sessions,
|
|
16
14
|
// where there is no epic to read progress back off when the run began from the backlog.
|
|
@@ -20,9 +18,10 @@ import { run_carry_conversation } from './run-carry-conversation'
|
|
|
20
18
|
// one *invocation's* budget, and that invocation opens lanes — each a work tree of its own — so the
|
|
21
19
|
// key is the common git directory, which every lane of one repository shares.
|
|
22
20
|
//
|
|
23
|
-
// **The
|
|
24
|
-
//
|
|
25
|
-
//
|
|
21
|
+
// **The record has no expiry of its own.** A run is bounded by its idle wait, which `backlog:budget`
|
|
22
|
+
// counts from when the run last had work; an age counted from `started_at` would end a run that is
|
|
23
|
+
// still working or still inside that wait. So a record a run never ended — a crash — stays `standing`
|
|
24
|
+
// until a person carries it with `--resume` or discards it with `--end`.
|
|
26
25
|
//
|
|
27
26
|
// **Whose record it is, is decided by liveness and by a declared hand-off — never by the invocation
|
|
28
27
|
// text**. Comparing the `invocation` string alone walks straight through the case that happens most: a
|
|
@@ -38,21 +37,14 @@ import { run_carry_conversation } from './run-carry-conversation'
|
|
|
38
37
|
|
|
39
38
|
const CARRY_PREFIX = 'josh-run-carry-'
|
|
40
39
|
const REPOSITORY_DIRECTORY_INDEX = 1
|
|
41
|
-
// **The whole-run bound is `backlog-budget.ts`'s, imported rather than restated.** `backlogrun.md` →
|
|
42
|
-
// "Waiting, and never waiting forever" states the figure and that module applies it; a second copy
|
|
43
|
-
// here would drift, and the drift is silent in exactly the direction that matters — raised there and
|
|
44
|
-
// still 8 here, this record answers `expired`, `--begin` replaces it, and the budget restarts at
|
|
45
|
-
// zero — the loss this record exists to prevent.
|
|
46
|
-
const CARRY_MAX_AGE_HOURS = backlog_budget.WHOLE_RUN_BUDGET_HOURS
|
|
47
|
-
const CARRY_MAX_AGE_MS = backlog_budget.WHOLE_RUN_BUDGET_MS
|
|
48
40
|
const NO_INCREMENT = 0
|
|
49
41
|
|
|
50
42
|
// The most session cuts one invocation may take. This bounds `cuts`, which
|
|
51
43
|
// only `run:carry --cut` increments — a `backlogrun`'s session hand-off at a child's merge (~one per
|
|
52
|
-
// 50 minutes
|
|
44
|
+
// 50 minutes of work), not the `run:cut` phase cuts a lane child takes. Each
|
|
53
45
|
// cut drops the context it accumulated but pays a cold preamble to resume, so past a point the
|
|
54
|
-
// re-establishment costs more than the accumulation it sheds; six sits well under the ~10
|
|
55
|
-
// could otherwise take and refuses a run that has begun to churn. `is_at_cut_cap` reads it, and the
|
|
46
|
+
// re-establishment costs more than the accumulation it sheds; six sits well under the ~10 a night of
|
|
47
|
+
// work could otherwise take and refuses a run that has begun to churn. `is_at_cut_cap` reads it, and the
|
|
56
48
|
// `--cut` count is refused at it rather than incrementing past it — except a headless judgment
|
|
57
49
|
// session's hand-back to the driver, which `run_headless.is_cut_capped` exempts.
|
|
58
50
|
const MAX_CUTS = 6
|
|
@@ -185,11 +177,7 @@ interface CarryChange {
|
|
|
185
177
|
retrospective?: boolean
|
|
186
178
|
}
|
|
187
179
|
|
|
188
|
-
type CarryRead =
|
|
189
|
-
| { kind: 'none' }
|
|
190
|
-
| { kind: 'carried'; carry: RunCarry }
|
|
191
|
-
| { kind: 'expired'; carry: RunCarry }
|
|
192
|
-
| { kind: 'unreadable' }
|
|
180
|
+
type CarryRead = { kind: 'none' } | { kind: 'carried'; carry: RunCarry } | { kind: 'unreadable' }
|
|
193
181
|
|
|
194
182
|
const NO_OWNER: CarryOwner = {}
|
|
195
183
|
|
|
@@ -234,32 +222,23 @@ function parse_carry(raw: string): RunCarry | undefined {
|
|
|
234
222
|
return json_value.parse_with(raw, run_carry_schema)
|
|
235
223
|
}
|
|
236
224
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// `run-hold.ts`; what a `started_at` that is not a date means is decided there.
|
|
243
|
-
const CARRY_SPEC: StampRecordSpec<RunCarry> = {
|
|
244
|
-
parse: parse_carry,
|
|
245
|
-
timestamp_of: started_at_of,
|
|
246
|
-
max_age_ms: CARRY_MAX_AGE_MS,
|
|
247
|
-
}
|
|
225
|
+
// **A record that cannot be read is `unreadable`, never `none`** — absent is none; present and
|
|
226
|
+
// unparseable is the state a guard must not fall open on. A `started_at` that is not a date is the
|
|
227
|
+
// same state: it passes the schema as a string, and every reader of the record measures from it.
|
|
228
|
+
function classify(raw: string | undefined): CarryRead {
|
|
229
|
+
if (raw === undefined) return { kind: 'none' }
|
|
248
230
|
|
|
249
|
-
|
|
250
|
-
return stamp_record.is_older_than(started_at_of(carry), CARRY_MAX_AGE_MS, now)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function classify(raw: string | undefined, now: Date): CarryRead {
|
|
254
|
-
const read = stamp_record.classify(raw, CARRY_SPEC, now)
|
|
231
|
+
const carry = parse_carry(raw)
|
|
255
232
|
|
|
256
|
-
if (
|
|
233
|
+
if (carry === undefined || Number.isNaN(Date.parse(carry.started_at))) {
|
|
234
|
+
return { kind: 'unreadable' }
|
|
235
|
+
}
|
|
257
236
|
|
|
258
|
-
return { kind:
|
|
237
|
+
return { kind: 'carried', carry }
|
|
259
238
|
}
|
|
260
239
|
|
|
261
|
-
function read_carry(target: string
|
|
262
|
-
return classify(stamp_file.read_stamp_text(target)
|
|
240
|
+
function read_carry(target: string): CarryRead {
|
|
241
|
+
return classify(stamp_file.read_stamp_text(target))
|
|
263
242
|
}
|
|
264
243
|
|
|
265
244
|
// The identity to record for a pid the caller declared. `read_start` is what makes the pair an
|
|
@@ -304,20 +283,6 @@ function begin_carry(
|
|
|
304
283
|
return stamp_file.create_stamp(target, carry) ? carry : undefined
|
|
305
284
|
}
|
|
306
285
|
|
|
307
|
-
// Replacing a record whose bound is spent. It removes and then creates exclusively rather than
|
|
308
|
-
// writing over the old bytes, for the reason `run-hold.ts` replaces a stale hold the same way: a
|
|
309
|
-
// plain write would put the race back one branch over.
|
|
310
|
-
function replace_carry(
|
|
311
|
-
target: string,
|
|
312
|
-
invocation: string,
|
|
313
|
-
owner: CarryOwner = NO_OWNER,
|
|
314
|
-
now: Date = new Date(),
|
|
315
|
-
): RunCarry | undefined {
|
|
316
|
-
stamp_file.remove_stamp(target)
|
|
317
|
-
|
|
318
|
-
return begin_carry(target, invocation, owner, now)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
286
|
// Taking over a standing record: the counters and `started_at` are the run's and stay, the owner
|
|
322
287
|
// becomes this session, and the hand-off is spent. **`cuts` is not incremented here** — `--cut`
|
|
323
288
|
// counts the cut, and a record adopted after a crash crossed no cut at all.
|
|
@@ -371,6 +336,21 @@ function is_foreign_live_owner(carry: RunCarry, owner: CarryOwner): boolean {
|
|
|
371
336
|
return is_owner_live(carry) && !is_owned_by(carry, owner)
|
|
372
337
|
}
|
|
373
338
|
|
|
339
|
+
// Whether a process other than the caller still holds the record. **By process alone, never by
|
|
340
|
+
// conversation**: the wake supervisor inherits the environment of the session that started it, so a
|
|
341
|
+
// conversation match would read that live session's record as the supervisor's own. The driver adopts
|
|
342
|
+
// the record for the supervisor's process (`run-wake-driver.ts` → `claim_record`), so without this a
|
|
343
|
+
// supervisor reads itself as a session at work.
|
|
344
|
+
function is_held_by_another(
|
|
345
|
+
carry: RunCarry,
|
|
346
|
+
is_live: (carry: RunCarry) => boolean = is_owner_live,
|
|
347
|
+
): boolean {
|
|
348
|
+
const caller = owner_of(process.pid)
|
|
349
|
+
const is_own = carry.owner_pid === caller.pid && carry.owner_start === caller.start
|
|
350
|
+
|
|
351
|
+
return !is_own && is_live(carry)
|
|
352
|
+
}
|
|
353
|
+
|
|
374
354
|
function is_handed_off_to(carry: RunCarry, invocation: string): boolean {
|
|
375
355
|
return carry.is_handed_off === true && carry.invocation === invocation
|
|
376
356
|
}
|
|
@@ -424,9 +404,7 @@ function end_carry(target: string): void {
|
|
|
424
404
|
// no record — `none` or `unreadable` — is not a run whose retrospective has run, so it answers `false`;
|
|
425
405
|
// `run:step` reads it to decide whether the stop position still owes a retrospective.
|
|
426
406
|
function retrospective_done_of(read: CarryRead): boolean {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
return read.carry.retrospective === true
|
|
407
|
+
return read.kind === 'carried' && read.carry.retrospective === true
|
|
430
408
|
}
|
|
431
409
|
|
|
432
410
|
// **What the resumed session has to be told, computed rather than stored.** A stored remainder would
|
|
@@ -458,10 +436,6 @@ function describe_carry(carry: RunCarry): string {
|
|
|
458
436
|
return `${carry.invocation} started ${carry.started_at}; ${String(carry.merged)} merged, ${String(carry.filed)} filed, ${String(carry.failures)} failed in a row, ${String(carry.outages)} outage(s) in a row, ${String(carry.cuts)} cut(s) crossed${done_note(carry)}`
|
|
459
437
|
}
|
|
460
438
|
|
|
461
|
-
function expired_message(carry: RunCarry): string {
|
|
462
|
-
return `The ${String(CARRY_MAX_AGE_HOURS)}-hour whole-run bound is spent: ${describe_carry(carry)}. Clear it with \`${END_COMMAND}\`.`
|
|
463
|
-
}
|
|
464
|
-
|
|
465
439
|
// A record left standing by a run that never reached `--end`. Naming both invocations is the point:
|
|
466
440
|
// the reader has to see that the budget in the way belongs to something else before deciding whether
|
|
467
441
|
// that run is over.
|
|
@@ -525,8 +499,6 @@ function unknown_message(): string {
|
|
|
525
499
|
}
|
|
526
500
|
|
|
527
501
|
const run_carry = {
|
|
528
|
-
CARRY_MAX_AGE_HOURS,
|
|
529
|
-
CARRY_MAX_AGE_MS,
|
|
530
502
|
END_COMMAND,
|
|
531
503
|
MAX_CUTS,
|
|
532
504
|
NO_OWNER,
|
|
@@ -543,12 +515,11 @@ const run_carry = {
|
|
|
543
515
|
is_at_cut_cap,
|
|
544
516
|
describe_carry,
|
|
545
517
|
end_carry,
|
|
546
|
-
expired_message,
|
|
547
518
|
mismatch_message,
|
|
548
519
|
fresh_carry,
|
|
549
520
|
is_count_refused,
|
|
550
|
-
is_expired,
|
|
551
521
|
is_foreign_live_owner,
|
|
522
|
+
is_held_by_another,
|
|
552
523
|
is_owner_live,
|
|
553
524
|
lost_message,
|
|
554
525
|
nothing_to_resume_message,
|
|
@@ -556,7 +527,6 @@ const run_carry = {
|
|
|
556
527
|
parse_carry,
|
|
557
528
|
read_carry,
|
|
558
529
|
remaining_of,
|
|
559
|
-
replace_carry,
|
|
560
530
|
repository_directory,
|
|
561
531
|
retrospective_done_of,
|
|
562
532
|
standing_message,
|