@joshuafolkken/kit 1.1373.0 → 1.1383.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/dependency-update/SKILL.md +6 -3
- 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-UTHMB4O2.js → chunk-2X2ART5G.js} +4 -33
- package/dist/hooks/{chunk-NZN2UFI6.js → chunk-4MGHBTOM.js} +162 -233
- package/dist/hooks/{chunk-YLBBSSTF.js → chunk-6AC4C2IO.js} +91 -67
- package/dist/hooks/{chunk-AKGCP4LX.js → chunk-6MAW7BCM.js} +12 -12
- package/dist/hooks/{chunk-JCRDBQXB.js → chunk-7VSZ2N4B.js} +3 -3
- package/dist/hooks/{chunk-GG72ANKI.js → chunk-KC6LUZZ2.js} +1 -1
- package/dist/hooks/{chunk-BZMPVFDY.js → chunk-KCF2YE7O.js} +5 -5
- package/dist/hooks/{chunk-7WKYHEGU.js → chunk-LC56R53A.js} +3 -3
- package/dist/hooks/{chunk-V4TKZHER.js → chunk-LOJTBU5V.js} +3 -3
- package/dist/hooks/{chunk-JGW24KKM.js → chunk-RYP3TSUJ.js} +3 -3
- package/dist/hooks/{chunk-SLSFIWTX.js → chunk-TT742SIO.js} +8 -5
- package/dist/hooks/{chunk-Z5TMJIPW.js → chunk-Y5R47QHU.js} +1 -1
- package/dist/hooks/{chunk-7UBFU3CL.js → chunk-YKUITFT7.js} +2 -2
- 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 +190 -191
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +3 -3
- package/dist/hooks/stop-guard.js +19 -15
- package/dist/josh.js +40 -5
- package/package.json +4 -4
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/scripts/adopt/adopt-logic.ts +4 -1
- package/scripts/adopt/adopt.ts +7 -4
- package/scripts/backlog/backlog-budget-cli.ts +2 -20
- package/scripts/backlog/backlog-budget.ts +72 -109
- package/scripts/backlog/backlog-drive-cli.ts +10 -3
- package/scripts/backlog/backlog-drive.ts +5 -2
- package/scripts/backlog/backlog-offer-cli.ts +1 -15
- package/scripts/cost-runtime/context-cut-payback.ts +9 -4
- package/scripts/cost-runtime/cost-pricing.ts +94 -33
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/document/read-set-pricing.ts +7 -5
- 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/git/git-spawn-sync.ts +9 -1
- package/scripts/hooks/stop-guard.ts +20 -9
- package/scripts/init/init-ai-copy.ts +10 -3
- package/scripts/init/init-copy-content.ts +8 -1
- package/scripts/init/security-policy.ts +79 -0
- package/scripts/josh/josh-commands-ai.ts +1 -1
- package/scripts/josh/josh-commands-maintenance.ts +3 -0
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/josh/josh-commands-versioning.ts +2 -1
- package/scripts/lane/lane-cli.ts +1 -3
- package/scripts/propagate/propagate-run.ts +17 -1
- package/scripts/propagate/propagate-steps.ts +9 -1
- package/scripts/run/board/run-board-header.ts +107 -84
- 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-render.ts +1 -5
- package/scripts/run/board/run-board-span.ts +0 -1
- package/scripts/run/board/run-board-status.ts +0 -17
- package/scripts/run/board/run-board-tick.ts +1 -0
- package/scripts/run/carry/run-carry-cli.ts +8 -62
- package/scripts/run/carry/run-carry-ended.ts +1 -1
- package/scripts/run/carry/run-carry.ts +22 -68
- package/scripts/run/cut/run-cut.ts +4 -4
- package/scripts/run/event/run-event-render.ts +0 -1
- package/scripts/run/event/run-event-scope.ts +4 -8
- package/scripts/run/event/run-event-stream-emit.ts +0 -25
- package/scripts/run/event/run-event-stream.ts +23 -31
- 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 +3 -3
- package/scripts/run/run-stranded.ts +25 -17
- package/scripts/run/wake/run-wake-cli.ts +7 -9
- package/scripts/run/wake/run-wake-describe.ts +3 -5
- package/scripts/run/wake/run-wake-driver.ts +3 -3
- 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/sync/sync-ai-files.ts +2 -0
- package/scripts/version/latest-update.ts +20 -6
- package/scripts/version/peer-floor.ts +99 -0
- package/scripts/version/playwright-browsers.ts +109 -0
- package/scripts/version/publishable-range-check.ts +41 -4
- package/templates/SECURITY.md +23 -0
- package/scripts/backlog/backlog-idle.ts +0 -88
|
@@ -157,28 +157,40 @@ function alert_of(spec: GaugeSpec, value: number, pressure: number | undefined):
|
|
|
157
157
|
return verdict ?? threshold_alert(value, spec)
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// One gauge's reading: its value, the width its figure is right-aligned to where that is wider than
|
|
161
|
+
// the figure's own, and the kernel's pressure verdict where the gauge takes one.
|
|
162
|
+
interface Reading {
|
|
163
|
+
value: number | undefined
|
|
164
|
+
width: number
|
|
165
|
+
pressure?: number | undefined
|
|
166
|
+
}
|
|
167
|
+
|
|
160
168
|
// Icon, figure, then bar: the figure is what a person reads, and its fixed
|
|
161
169
|
// width keeps every bar starting in one column. The bar stops full past `spec.full`; the figure still
|
|
162
170
|
// says how far past, and takes the warning colors only, so a quiet machine's figures stay the terminal's
|
|
163
171
|
// own.
|
|
164
|
-
function gauge(spec: GaugeSpec,
|
|
172
|
+
function gauge(spec: GaugeSpec, reading: Reading): string | undefined {
|
|
173
|
+
const { value, width } = reading
|
|
174
|
+
|
|
165
175
|
if (value === undefined) return undefined
|
|
166
176
|
|
|
167
|
-
const alert = alert_of(spec, value, pressure)
|
|
177
|
+
const alert = alert_of(spec, value, reading.pressure)
|
|
168
178
|
const warning = alert === 'normal' ? undefined : GAUGE_SHADES[alert]
|
|
169
179
|
const bar = bar_of(value, spec.full, undefined, warning ?? spec.normal)
|
|
170
180
|
|
|
171
|
-
return `${spec.icon} ${painted(warning, spec.format(value))} ${bar}`
|
|
181
|
+
return `${spec.icon} ${painted(warning, spec.format(value).padStart(width))} ${bar}`
|
|
172
182
|
}
|
|
173
183
|
|
|
174
|
-
// `undefined` when not one gauge could be read, so the header draws no line at all.
|
|
175
|
-
|
|
184
|
+
// `undefined` when not one gauge could be read, so the header draws no line at all. `width` is the
|
|
185
|
+
// header's figure column: a figure narrower than it is right-aligned to it, so the first gauge's bar
|
|
186
|
+
// starts under the progress bar whichever gauge that is.
|
|
187
|
+
function line_of(gauges: MachineGauges | undefined, width = 0): string | undefined {
|
|
176
188
|
if (gauges === undefined) return undefined
|
|
177
189
|
|
|
178
190
|
const parts = [
|
|
179
|
-
gauge(SPECS.cpu, gauges.cpu_percent),
|
|
180
|
-
gauge(SPECS.memory, gauges.memory_percent, gauges.memory_pressure),
|
|
181
|
-
gauge(SPECS.swap, gauges.swap_mb_per_s),
|
|
191
|
+
gauge(SPECS.cpu, { value: gauges.cpu_percent, width }),
|
|
192
|
+
gauge(SPECS.memory, { value: gauges.memory_percent, width, pressure: gauges.memory_pressure }),
|
|
193
|
+
gauge(SPECS.swap, { value: gauges.swap_mb_per_s, width }),
|
|
182
194
|
].filter((part) => part !== undefined)
|
|
183
195
|
|
|
184
196
|
return parts.length === 0 ? undefined : parts.join(GAP)
|
|
@@ -188,6 +200,7 @@ const run_board_machine = {
|
|
|
188
200
|
CPU_YELLOW,
|
|
189
201
|
GAP,
|
|
190
202
|
MEMORY_YELLOW,
|
|
203
|
+
PERCENT_WIDTH,
|
|
191
204
|
gauges_of,
|
|
192
205
|
line_of,
|
|
193
206
|
percent_text,
|
|
@@ -373,11 +373,7 @@ function bounded(parts: FrameParts, bounds: FrameBounds): Array<string> {
|
|
|
373
373
|
|
|
374
374
|
function render(view: BoardView): Array<string> {
|
|
375
375
|
const { header, notes } = view
|
|
376
|
-
const above = [
|
|
377
|
-
...run_board_header.header_lines(header),
|
|
378
|
-
...resume_lines(view.resume),
|
|
379
|
-
...run_board_header.idle_lines(header),
|
|
380
|
-
]
|
|
376
|
+
const above = [...run_board_header.header_lines(header), ...resume_lines(view.resume)]
|
|
381
377
|
const parts = {
|
|
382
378
|
above,
|
|
383
379
|
plan: plan_sections(header, view.size),
|
|
@@ -28,7 +28,6 @@ function ended_span(ended: EndedRun | undefined): BoardSpan | undefined {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// The running run wins over the ended one, so a run that begins replaces the finished board at once.
|
|
31
|
-
// An expired carry is still the running run's budget spent, not a run that ended, so it draws nothing.
|
|
32
31
|
function span_of(read: CarryRead, ended: EndedRun | undefined): BoardSpan | undefined {
|
|
33
32
|
if (read.kind !== 'carried') return read.kind === 'none' ? ended_span(ended) : undefined
|
|
34
33
|
|
|
@@ -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,
|
|
@@ -30,8 +29,6 @@ interface ItemStatus {
|
|
|
30
29
|
// The run as a whole, read off the same stream.
|
|
31
30
|
interface RunActivity {
|
|
32
31
|
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
32
|
// The run's last position is a `stop` that names no child — the run itself ended.
|
|
36
33
|
is_stopped: boolean
|
|
37
34
|
}
|
|
@@ -243,19 +240,6 @@ function settle_closed(
|
|
|
243
240
|
return new Map([...statuses].map(([issue, status]) => [issue, settled(issue, status, read)]))
|
|
244
241
|
}
|
|
245
242
|
|
|
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
243
|
function is_run_stop(event: RunEvent | undefined): boolean {
|
|
260
244
|
return event?.kind === KIND.STOP && run_event_scope.issue_named(event) === undefined
|
|
261
245
|
}
|
|
@@ -268,7 +252,6 @@ function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActi
|
|
|
268
252
|
|
|
269
253
|
return {
|
|
270
254
|
last_event_ms: last === undefined ? undefined : moment(last),
|
|
271
|
-
idle: idle_of(events),
|
|
272
255
|
is_stopped: is_ended || is_run_stop(position),
|
|
273
256
|
}
|
|
274
257
|
}
|
|
@@ -135,6 +135,7 @@ function header_of(
|
|
|
135
135
|
machine: state.gauges,
|
|
136
136
|
usages: state.usages,
|
|
137
137
|
form: redraw.ports.form,
|
|
138
|
+
repo: state.plan?.context.repo,
|
|
138
139
|
spinner: redraw.ports.is_tty ? SPOT : undefined,
|
|
139
140
|
link: run_board_link.linker(state.plan?.context.repo, redraw.ports.link),
|
|
140
141
|
}
|
|
@@ -39,7 +39,6 @@ const RESUMED_VERDICT = 'resumed'
|
|
|
39
39
|
const CARRIED_VERDICT = 'carried'
|
|
40
40
|
const COUNTED_VERDICT = 'counted'
|
|
41
41
|
const ENDED_VERDICT = 'ended'
|
|
42
|
-
const EXPIRED_VERDICT = 'expired'
|
|
43
42
|
const NONE_VERDICT = 'none'
|
|
44
43
|
const UNREADABLE_VERDICT = 'unreadable'
|
|
45
44
|
const MISMATCH_VERDICT = 'mismatch'
|
|
@@ -94,28 +93,14 @@ function report_unreadable(is_json: boolean): number {
|
|
|
94
93
|
return report(UNREADABLE_VERDICT, undefined, is_json, FAILURE_EXIT_CODE)
|
|
95
94
|
}
|
|
96
95
|
|
|
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
96
|
function report_read(read: CarryRead, is_json: boolean): number {
|
|
104
97
|
if (read.kind === 'none') return report(NONE_VERDICT, undefined, is_json, SUCCESS_EXIT_CODE)
|
|
105
98
|
|
|
106
99
|
if (read.kind === 'unreadable') return report_unreadable(is_json)
|
|
107
100
|
|
|
108
|
-
if (read.kind === 'expired') return report_expired(read.carry, is_json)
|
|
109
|
-
|
|
110
101
|
return report_carry(CARRIED_VERDICT, read.carry, is_json)
|
|
111
102
|
}
|
|
112
103
|
|
|
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
104
|
function refusal_message(claim: CarryClaim, carry: RunCarry, invocation: string): string {
|
|
120
105
|
if (claim === 'busy') return run_carry.busy_message(carry)
|
|
121
106
|
|
|
@@ -149,7 +134,7 @@ function report_adoption(
|
|
|
149
134
|
|
|
150
135
|
// **Whose record it is, is the classifier's answer and not this command's guess.** `--end` is only
|
|
151
136
|
// reached on the clean-finish path, so a run that crashed or stopped on a guard leaves its record
|
|
152
|
-
// standing
|
|
137
|
+
// standing until a person carries or discards it; `resumed` is reached only where the run itself declared the
|
|
153
138
|
// cut, or where `--resume` says the reader has decided to carry it.
|
|
154
139
|
function report_claim(
|
|
155
140
|
target: string,
|
|
@@ -166,49 +151,17 @@ function report_claim(
|
|
|
166
151
|
return report(CLAIM_VERDICTS[claim], carry, is_json, FAILURE_EXIT_CODE)
|
|
167
152
|
}
|
|
168
153
|
|
|
169
|
-
function start_fresh(
|
|
170
|
-
target
|
|
171
|
-
request: CarryClaimRequest,
|
|
172
|
-
is_replacement: boolean,
|
|
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)
|
|
154
|
+
function start_fresh(target: string, request: CarryClaimRequest, is_json: boolean): number {
|
|
155
|
+
const carry = run_carry.begin_carry(target, request.invocation, request.owner)
|
|
178
156
|
|
|
179
157
|
if (carry === undefined) return report_lost(is_json)
|
|
180
158
|
|
|
181
159
|
return report_carry(BEGAN_VERDICT, carry, is_json)
|
|
182
160
|
}
|
|
183
161
|
|
|
184
|
-
// **
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
function begin_over_expired(
|
|
188
|
-
target: string,
|
|
189
|
-
carry: RunCarry,
|
|
190
|
-
request: CarryClaimRequest,
|
|
191
|
-
is_json: boolean,
|
|
192
|
-
): number {
|
|
193
|
-
if (run_carry.is_foreign_live_owner(carry, request.owner)) return report_busy(carry, is_json)
|
|
194
|
-
|
|
195
|
-
// **A hand-off says the resumption *is* that run, so the spent bound is this session's too.**
|
|
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)
|
|
201
|
-
|
|
202
|
-
console.error(run_carry.expired_message(carry))
|
|
203
|
-
|
|
204
|
-
return start_fresh(target, request, true, is_json)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// **A live record is never replaced, and never resumed into by something else.** Replacing one would
|
|
208
|
-
// restart the budget the cut exists to carry, and resuming into one would put two parents on a single
|
|
209
|
-
// budget. An expired one is replaced,
|
|
210
|
-
// because its run has spent the whole-run bound and a person typing the keyword again is starting a
|
|
211
|
-
// new run — unless its owner is still running, which is the case above.
|
|
162
|
+
// **A standing record is never replaced, and never resumed into by something else.** Replacing one
|
|
163
|
+
// would restart the budget the cut exists to carry, and resuming into one would put two parents on a
|
|
164
|
+
// single budget. Its age changes none of that: a record is replaced by nothing but a person's `--end`.
|
|
212
165
|
function begin(target: string, request: CarryClaimRequest, is_json: boolean): number {
|
|
213
166
|
const read = run_carry.read_carry(target)
|
|
214
167
|
|
|
@@ -216,9 +169,7 @@ function begin(target: string, request: CarryClaimRequest, is_json: boolean): nu
|
|
|
216
169
|
|
|
217
170
|
if (read.kind === 'carried') return report_claim(target, read.carry, request, is_json)
|
|
218
171
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return start_fresh(target, request, false, is_json)
|
|
172
|
+
return start_fresh(target, request, is_json)
|
|
222
173
|
}
|
|
223
174
|
|
|
224
175
|
function report_nothing_to_resume(is_json: boolean): number {
|
|
@@ -237,8 +188,6 @@ function adopt(target: string, request: CarryClaimRequest, is_json: boolean): nu
|
|
|
237
188
|
|
|
238
189
|
if (read.kind === 'unreadable') return report_unreadable(is_json)
|
|
239
190
|
|
|
240
|
-
if (read.kind === 'expired') return report_expired(read.carry, is_json)
|
|
241
|
-
|
|
242
191
|
return report_claim(target, read.carry, request, is_json)
|
|
243
192
|
}
|
|
244
193
|
|
|
@@ -334,9 +283,7 @@ async function count(
|
|
|
334
283
|
|
|
335
284
|
await record_retrospective(request)
|
|
336
285
|
|
|
337
|
-
return
|
|
338
|
-
? report_expired(carry, is_json)
|
|
339
|
-
: report_carry(COUNTED_VERDICT, carry, is_json)
|
|
286
|
+
return report_carry(COUNTED_VERDICT, carry, is_json)
|
|
340
287
|
}
|
|
341
288
|
|
|
342
289
|
// **A stop over a record that is still there pushes one ⏸️ confirmation before the record goes**.
|
|
@@ -463,7 +410,6 @@ const run_carry_cli = {
|
|
|
463
410
|
CARRIED_VERDICT,
|
|
464
411
|
COUNTED_VERDICT,
|
|
465
412
|
ENDED_VERDICT,
|
|
466
|
-
EXPIRED_VERDICT,
|
|
467
413
|
MISMATCH_VERDICT,
|
|
468
414
|
NONE_VERDICT,
|
|
469
415
|
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()
|
|
@@ -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
|
-
}
|
|
248
|
-
|
|
249
|
-
function is_expired(carry: RunCarry, now: Date): boolean {
|
|
250
|
-
return stamp_record.is_older_than(started_at_of(carry), CARRY_MAX_AGE_MS, now)
|
|
251
|
-
}
|
|
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' }
|
|
252
230
|
|
|
253
|
-
|
|
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.
|
|
@@ -424,9 +389,7 @@ function end_carry(target: string): void {
|
|
|
424
389
|
// no record — `none` or `unreadable` — is not a run whose retrospective has run, so it answers `false`;
|
|
425
390
|
// `run:step` reads it to decide whether the stop position still owes a retrospective.
|
|
426
391
|
function retrospective_done_of(read: CarryRead): boolean {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
return read.carry.retrospective === true
|
|
392
|
+
return read.kind === 'carried' && read.carry.retrospective === true
|
|
430
393
|
}
|
|
431
394
|
|
|
432
395
|
// **What the resumed session has to be told, computed rather than stored.** A stored remainder would
|
|
@@ -458,10 +421,6 @@ function describe_carry(carry: RunCarry): string {
|
|
|
458
421
|
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
422
|
}
|
|
460
423
|
|
|
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
424
|
// A record left standing by a run that never reached `--end`. Naming both invocations is the point:
|
|
466
425
|
// the reader has to see that the budget in the way belongs to something else before deciding whether
|
|
467
426
|
// that run is over.
|
|
@@ -525,8 +484,6 @@ function unknown_message(): string {
|
|
|
525
484
|
}
|
|
526
485
|
|
|
527
486
|
const run_carry = {
|
|
528
|
-
CARRY_MAX_AGE_HOURS,
|
|
529
|
-
CARRY_MAX_AGE_MS,
|
|
530
487
|
END_COMMAND,
|
|
531
488
|
MAX_CUTS,
|
|
532
489
|
NO_OWNER,
|
|
@@ -543,11 +500,9 @@ const run_carry = {
|
|
|
543
500
|
is_at_cut_cap,
|
|
544
501
|
describe_carry,
|
|
545
502
|
end_carry,
|
|
546
|
-
expired_message,
|
|
547
503
|
mismatch_message,
|
|
548
504
|
fresh_carry,
|
|
549
505
|
is_count_refused,
|
|
550
|
-
is_expired,
|
|
551
506
|
is_foreign_live_owner,
|
|
552
507
|
is_owner_live,
|
|
553
508
|
lost_message,
|
|
@@ -556,7 +511,6 @@ const run_carry = {
|
|
|
556
511
|
parse_carry,
|
|
557
512
|
read_carry,
|
|
558
513
|
remaining_of,
|
|
559
|
-
replace_carry,
|
|
560
514
|
repository_directory,
|
|
561
515
|
retrospective_done_of,
|
|
562
516
|
standing_message,
|
|
@@ -50,12 +50,12 @@ const { PRE_GATE_PHASE, IMPLEMENTATION_PHASE } = agent_role_profile
|
|
|
50
50
|
// `pnpm josh cost --cut` hand-off uses (`backlogrun-progress.md` → "The hand-off"). The shared
|
|
51
51
|
// threshold prevents the scheduler and worker boundaries from drifting apart.
|
|
52
52
|
const IMPLEMENTATION_CONTEXT_THRESHOLD = CONTEXT_CUT_THRESHOLD
|
|
53
|
-
// **The
|
|
54
|
-
//
|
|
53
|
+
// **The bound is `backlog-budget.ts`'s idle wait, imported rather than restated**, so the eight hours
|
|
54
|
+
// stay written once. A resume is expected within seconds of the cut, so this age is only a backstop
|
|
55
55
|
// against a fresh process that never started; an expired record is cleared and replaced by the next
|
|
56
56
|
// cut rather than blocking it.
|
|
57
|
-
const CUT_MAX_AGE_MS = backlog_budget.
|
|
58
|
-
const CUT_MAX_AGE_HOURS = backlog_budget.
|
|
57
|
+
const CUT_MAX_AGE_MS = backlog_budget.DEFAULT_IDLE_MS
|
|
58
|
+
const CUT_MAX_AGE_HOURS = backlog_budget.IDLE_WAIT_HOURS
|
|
59
59
|
|
|
60
60
|
const END_COMMAND = 'pnpm josh run:cut --end'
|
|
61
61
|
const READ_COMMAND = 'pnpm josh run:cut --json'
|
|
@@ -39,7 +39,6 @@ const KIND_LABELS: Readonly<Record<EventKind, KindLabel>> = {
|
|
|
39
39
|
heartbeat: { ja: '生存確認', en: 'heartbeat' },
|
|
40
40
|
'ship-launch': { ja: '出荷を開始', en: 'ship launched' },
|
|
41
41
|
'ship-stop': { ja: '出荷が停止', en: 'ship stopped' },
|
|
42
|
-
idle: { ja: '新着待ち', en: 'idle watch' },
|
|
43
42
|
filed: { ja: '起票', en: 'filed' },
|
|
44
43
|
note: { ja: '意見', en: 'note' },
|
|
45
44
|
'lane-phase': { ja: 'レーンの段階', en: 'lane phase' },
|
|
@@ -55,16 +55,12 @@ function scoped_events(
|
|
|
55
55
|
return events.filter((event) => is_within(event, floor))
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// The scope the run's own carry record implies. A record that reads at all names the invocation's start,
|
|
59
|
-
//
|
|
60
|
-
// so rounding it to unknown would withhold the scope from exactly the long runs that most need one. Only an
|
|
61
|
-
// absent or unreadable record leaves the scope undetermined.
|
|
58
|
+
// The scope the run's own carry record implies. A record that reads at all names the invocation's start,
|
|
59
|
+
// however long ago that was; only an absent or unreadable record leaves the scope undetermined.
|
|
62
60
|
function scope_of(read: CarryRead): EventScope {
|
|
63
|
-
if (read.kind
|
|
64
|
-
return { kind: SINCE_SCOPE, started_at: read.carry.started_at }
|
|
65
|
-
}
|
|
61
|
+
if (read.kind !== 'carried') return UNKNOWN_EVENT_SCOPE
|
|
66
62
|
|
|
67
|
-
return
|
|
63
|
+
return { kind: SINCE_SCOPE, started_at: read.carry.started_at }
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
// The newest event within scope, or `undefined` when the scope could not be determined or nothing falls
|
|
@@ -70,30 +70,6 @@ async function emit_once(kind: string, text: string): Promise<boolean> {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/**
|
|
74
|
-
* Append one event unless the newest event of this kind already carries this text — for a marker whose
|
|
75
|
-
* value, not its presence, is the news. The idle window is re-derived on every
|
|
76
|
-
* offer of a watching loop; only a window that moved is worth a line. Returns whether it appended.
|
|
77
|
-
*/
|
|
78
|
-
async function emit_changed(kind: string, text: string): Promise<boolean> {
|
|
79
|
-
try {
|
|
80
|
-
const target = await stream_target()
|
|
81
|
-
|
|
82
|
-
if (target === undefined) return false
|
|
83
|
-
|
|
84
|
-
const newest = run_event_stream.read_events(target).findLast((event) => event.kind === kind)
|
|
85
|
-
|
|
86
|
-
if (newest?.text === text) return false
|
|
87
|
-
|
|
88
|
-
return run_event_stream.append(target, kind, text, now_iso()).appended
|
|
89
|
-
} catch (error) {
|
|
90
|
-
// Best-effort: a failed append is dropped rather than raised into the loop's work.
|
|
91
|
-
error_text.trace_swallowed('run_event_stream_emit.emit_changed', error)
|
|
92
|
-
|
|
93
|
-
return false
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
73
|
// The events of the invocation now running: the stream outlives every invocation, so a marker a previous
|
|
98
74
|
// run left — a stall that ended with no dispatch after it — must not hold back this run's first one. An
|
|
99
75
|
// undetermined scope (no carry record) keeps the whole stream, the side that under-notifies rather than
|
|
@@ -187,7 +163,6 @@ const run_event_stream_emit = {
|
|
|
187
163
|
all_events,
|
|
188
164
|
current_events,
|
|
189
165
|
emit,
|
|
190
|
-
emit_changed,
|
|
191
166
|
emit_heartbeat,
|
|
192
167
|
emit_once,
|
|
193
168
|
emit_once_since,
|