@joshuafolkken/kit 1.1383.0 → 1.1435.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/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-LOJTBU5V.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-4MGHBTOM.js → chunk-3LE6VLSR.js} +25 -8
- package/dist/hooks/{chunk-Y5R47QHU.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-YKUITFT7.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-6MAW7BCM.js → chunk-JXQBK323.js} +2677 -190
- package/dist/hooks/{chunk-KCF2YE7O.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-LC56R53A.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-TT742SIO.js → chunk-XER6SSIB.js} +92 -19
- package/dist/hooks/{chunk-2X2ART5G.js → chunk-YW452ZAX.js} +500 -29
- package/dist/hooks/{chunk-6AC4C2IO.js → chunk-YZXYOAZ3.js} +187 -443
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +262 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +124 -15
- package/dist/hooks/stop-guard.js +98 -55
- package/dist/josh.js +63 -10
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/backlog/backlog-drive-cli.ts +2 -4
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +5 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-owner.ts +72 -5
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +80 -5
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +13 -2
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +30 -4
- package/scripts/init/start-steps.ts +77 -3
- package/scripts/init/start.ts +21 -13
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +25 -1
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +139 -37
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +58 -5
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +121 -0
- package/scripts/run/board/run-board-span.ts +49 -7
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +76 -13
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +53 -16
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-added.ts +1 -1
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-change.ts +1 -1
- package/scripts/run/carry/run-carry-cli.ts +39 -52
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry-ended.ts +9 -1
- package/scripts/run/carry/run-carry.ts +24 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +3 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +91 -0
- package/scripts/run/event/run-event-stream.ts +46 -9
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/run-stranded-detect.ts +58 -12
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +108 -74
- package/scripts/run/wake/run-wake-describe.ts +36 -2
- package/scripts/run/wake/run-wake-driver.ts +21 -5
- package/scripts/run/wake/run-wake-handoff.ts +1 -1
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -1,13 +1,13 @@
|
|
|
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
|
-
import { run_merge_collect } from '#scripts/run/merge/run-merge-collect'
|
|
8
9
|
import { run_cli_fault } from '#scripts/run/run-cli-fault'
|
|
9
10
|
import { run_headless } from '#scripts/run/run-headless'
|
|
10
|
-
import { run_stop_notify } from '#scripts/run/run-stop-notify'
|
|
11
11
|
import {
|
|
12
12
|
run_carry,
|
|
13
13
|
type CarryClaim,
|
|
@@ -17,8 +17,7 @@ import {
|
|
|
17
17
|
} from './run-carry'
|
|
18
18
|
import { run_carry_args, type CountRequest, type Request } from './run-carry-args'
|
|
19
19
|
import { run_carry_conversation } from './run-carry-conversation'
|
|
20
|
-
import {
|
|
21
|
-
import { run_carry_stash } from './run-carry-stash'
|
|
20
|
+
import { run_carry_end } from './run-carry-end'
|
|
22
21
|
|
|
23
22
|
// `josh run:carry` — the record that carries one invocation's budget across its own session cuts.
|
|
24
23
|
// A `backlogrun` begins it, counts a merge and a filing into it, marks each
|
|
@@ -151,25 +150,47 @@ function report_claim(
|
|
|
151
150
|
return report(CLAIM_VERDICTS[claim], carry, is_json, FAILURE_EXIT_CODE)
|
|
152
151
|
}
|
|
153
152
|
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
// When the run began: the moment `josh backlogrun` launched this owner, so the clock `run:board` was
|
|
154
|
+
// already counting from does not restart at the claim; `undefined` — now — for every other run.
|
|
155
|
+
async function launch_start(request: CarryClaimRequest): Promise<Date | undefined> {
|
|
156
|
+
const directory = await run_carry.repository_directory()
|
|
157
|
+
|
|
158
|
+
if (directory === undefined) return undefined
|
|
159
|
+
|
|
160
|
+
return backlogrun_launch.started_for(directory, request.owner.pid)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// A run that began is planning, which is what its board says until the supervisor takes it over.
|
|
164
|
+
async function start_fresh(
|
|
165
|
+
target: string,
|
|
166
|
+
request: CarryClaimRequest,
|
|
167
|
+
is_json: boolean,
|
|
168
|
+
): Promise<number> {
|
|
169
|
+
const started = await launch_start(request)
|
|
170
|
+
const carry = run_carry.begin_carry(target, request.invocation, request.owner, started)
|
|
156
171
|
|
|
157
172
|
if (carry === undefined) return report_lost(is_json)
|
|
158
173
|
|
|
174
|
+
await run_event_setup.emit(run_event_setup.STAGE.PLANNING)
|
|
175
|
+
|
|
159
176
|
return report_carry(BEGAN_VERDICT, carry, is_json)
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
// **A standing record is never replaced, and never resumed into by something else.** Replacing one
|
|
163
180
|
// would restart the budget the cut exists to carry, and resuming into one would put two parents on a
|
|
164
181
|
// single budget. Its age changes none of that: a record is replaced by nothing but a person's `--end`.
|
|
165
|
-
function begin(
|
|
182
|
+
async function begin(
|
|
183
|
+
target: string,
|
|
184
|
+
request: CarryClaimRequest,
|
|
185
|
+
is_json: boolean,
|
|
186
|
+
): Promise<number> {
|
|
166
187
|
const read = run_carry.read_carry(target)
|
|
167
188
|
|
|
168
189
|
if (read.kind === 'unreadable') return report_unreadable(is_json)
|
|
169
190
|
|
|
170
191
|
if (read.kind === 'carried') return report_claim(target, read.carry, request, is_json)
|
|
171
192
|
|
|
172
|
-
return start_fresh(target, request, is_json)
|
|
193
|
+
return await start_fresh(target, request, is_json)
|
|
173
194
|
}
|
|
174
195
|
|
|
175
196
|
function report_nothing_to_resume(is_json: boolean): number {
|
|
@@ -203,10 +224,16 @@ function report_over(is_json: boolean): number {
|
|
|
203
224
|
return report(OVER_VERDICT, undefined, is_json, FAILURE_EXIT_CODE)
|
|
204
225
|
}
|
|
205
226
|
|
|
206
|
-
function claim_record(
|
|
227
|
+
async function claim_record(
|
|
228
|
+
target: string,
|
|
229
|
+
request: CarryClaimRequest,
|
|
230
|
+
is_json: boolean,
|
|
231
|
+
): Promise<number> {
|
|
207
232
|
if (cost_cli.session_verdict() === cost_verdict.OVER_VERDICT) return report_over(is_json)
|
|
208
233
|
|
|
209
|
-
|
|
234
|
+
if (request.is_adoption) return adopt(target, request, is_json)
|
|
235
|
+
|
|
236
|
+
return await begin(target, request, is_json)
|
|
210
237
|
}
|
|
211
238
|
|
|
212
239
|
// The record is no longer this session's to advance — handed off at a cut, or taken over by a live
|
|
@@ -286,52 +313,12 @@ async function count(
|
|
|
286
313
|
return report_carry(COUNTED_VERDICT, carry, is_json)
|
|
287
314
|
}
|
|
288
315
|
|
|
289
|
-
// **A stop over a record that is still there pushes one ⏸️ confirmation before the record goes**.
|
|
290
|
-
// `plan` is read from the record `--end` is about to remove, so the second `--end` reads `none` and
|
|
291
|
-
// plans nothing — one stop never notifies twice. A clean `--end` passes no reason and stays silent,
|
|
292
|
-
// because a completed run has its own notification. The closed-issue stash report
|
|
293
|
-
// (`run-carry-stash.ts`) is once per invocation on the same read.
|
|
294
|
-
async function report_end(read: CarryRead): Promise<void> {
|
|
295
|
-
if (read.kind === 'none') return
|
|
296
|
-
|
|
297
|
-
await run_carry_stash.report_orphans()
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Best-effort: the board's record of the ended run never stands between `--end` and the carry record it
|
|
301
|
-
// clears, so a failed write goes to stderr rather than leaving the run carried.
|
|
302
|
-
function keep_ended(directory: string, read: CarryRead, stopped: string | undefined): void {
|
|
303
|
-
try {
|
|
304
|
-
run_carry_ended.record_ended(run_carry_ended.ended_path(directory), read, new Date(), stopped)
|
|
305
|
-
} catch (error) {
|
|
306
|
-
console.error(`run:carry: the ended run was not recorded for run:board: ${String(error)}`)
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// The record goes, and the run it held stays readable as the last ended run, so `run:board` keeps the
|
|
311
|
-
// finished run on screen until the next one begins — a stopped one with the session to resume it from.
|
|
312
|
-
function close_record(directory: string, stopped: string | undefined): CarryRead {
|
|
313
|
-
const target = run_carry.carry_path(directory)
|
|
314
|
-
const read = run_carry.read_carry(target)
|
|
315
|
-
|
|
316
|
-
keep_ended(directory, read, stopped)
|
|
317
|
-
run_carry.end_carry(target)
|
|
318
|
-
|
|
319
|
-
return read
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// The merges the run ended with are recorded while its record can still count them.
|
|
323
316
|
async function finish(
|
|
324
317
|
directory: string,
|
|
325
318
|
stopped: string | undefined,
|
|
326
319
|
is_json: boolean,
|
|
327
320
|
): Promise<number> {
|
|
328
|
-
await
|
|
329
|
-
const read = close_record(directory, stopped)
|
|
330
|
-
const notice = run_stop_notify.plan(read, stopped)
|
|
331
|
-
|
|
332
|
-
if (notice !== undefined) await run_stop_notify.announce(notice)
|
|
333
|
-
|
|
334
|
-
await report_end(read)
|
|
321
|
+
const read = await run_carry_end.end_run(directory, { stopped, is_announced: true })
|
|
335
322
|
|
|
336
323
|
if (read.kind === 'none') return report(NONE_VERDICT, undefined, is_json, SUCCESS_EXIT_CODE)
|
|
337
324
|
|
|
@@ -347,7 +334,7 @@ async function act(
|
|
|
347
334
|
request: Exclude<Request, { kind: 'end' }>,
|
|
348
335
|
is_json: boolean,
|
|
349
336
|
): Promise<number> {
|
|
350
|
-
if (request.kind === 'claim') return claim_record(target, request.claim, is_json)
|
|
337
|
+
if (request.kind === 'claim') return await claim_record(target, request.claim, is_json)
|
|
351
338
|
|
|
352
339
|
const read = run_carry.read_carry(target)
|
|
353
340
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { run_merge_collect } from '#scripts/run/merge/run-merge-collect'
|
|
2
|
+
import { run_stop_notify } from '#scripts/run/run-stop-notify'
|
|
3
|
+
import { run_carry, type CarryRead } from './run-carry'
|
|
4
|
+
import { run_carry_ended } from './run-carry-ended'
|
|
5
|
+
import { run_carry_stash } from './run-carry-stash'
|
|
6
|
+
|
|
7
|
+
// Ending a run's record, apart from the command that prints the verdict: `run:carry --end` and
|
|
8
|
+
// `josh backlogrun --stop` both end it here, so the one end is never written twice.
|
|
9
|
+
|
|
10
|
+
interface EndRequest {
|
|
11
|
+
// Why the run stopped; `undefined` is a clean finish.
|
|
12
|
+
stopped: string | undefined
|
|
13
|
+
// Whether a stop pushes its ⏸️ confirmation. A person who typed the stop is already at the terminal.
|
|
14
|
+
is_announced: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// **A stop over a record that is still there pushes one ⏸️ confirmation before the record goes**.
|
|
18
|
+
// `plan` is read from the record the end is about to remove, so the second end reads `none` and
|
|
19
|
+
// plans nothing — one stop never notifies twice. A clean end passes no reason and stays silent,
|
|
20
|
+
// because a completed run has its own notification.
|
|
21
|
+
async function announce_stop(read: CarryRead, request: EndRequest): Promise<void> {
|
|
22
|
+
if (!request.is_announced) return
|
|
23
|
+
|
|
24
|
+
const notice = run_stop_notify.plan(read, request.stopped)
|
|
25
|
+
|
|
26
|
+
if (notice !== undefined) await run_stop_notify.announce(notice)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The closed-issue stash report (`run-carry-stash.ts`) is once per invocation on the same read.
|
|
30
|
+
async function report_end(read: CarryRead): Promise<void> {
|
|
31
|
+
if (read.kind === 'none') return
|
|
32
|
+
|
|
33
|
+
await run_carry_stash.report_orphans()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Best-effort: the board's record of the ended run never stands between the end and the carry record it
|
|
37
|
+
// clears, so a failed write goes to stderr rather than leaving the run carried.
|
|
38
|
+
function keep_ended(directory: string, read: CarryRead, stopped: string | undefined): void {
|
|
39
|
+
try {
|
|
40
|
+
run_carry_ended.record_ended(run_carry_ended.ended_path(directory), read, new Date(), stopped)
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`run:carry: the ended run was not recorded for run:board: ${String(error)}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The record goes, and the run it held stays readable as the last ended run, so `run:board` keeps the
|
|
47
|
+
// finished run on screen until the next one begins — a stopped one with the session to resume it from.
|
|
48
|
+
function close_record(directory: string, stopped: string | undefined): CarryRead {
|
|
49
|
+
const target = run_carry.carry_path(directory)
|
|
50
|
+
const read = run_carry.read_carry(target)
|
|
51
|
+
|
|
52
|
+
keep_ended(directory, read, stopped)
|
|
53
|
+
run_carry.end_carry(target)
|
|
54
|
+
|
|
55
|
+
return read
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The merges the run ended with are recorded while its record can still count them. The read handed
|
|
59
|
+
// back is the record as it stood before it was removed.
|
|
60
|
+
async function end_run(directory: string, request: EndRequest): Promise<CarryRead> {
|
|
61
|
+
await run_merge_collect.collect_merged(directory)
|
|
62
|
+
const read = close_record(directory, request.stopped)
|
|
63
|
+
|
|
64
|
+
await announce_stop(read, request)
|
|
65
|
+
await report_end(read)
|
|
66
|
+
|
|
67
|
+
return read
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const run_carry_end = { end_run }
|
|
71
|
+
|
|
72
|
+
export type { EndRequest }
|
|
73
|
+
export { run_carry_end }
|
|
@@ -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 }
|
|
@@ -102,6 +102,9 @@ interface RunCarry {
|
|
|
102
102
|
// The owner conversation's transcript, which outlives a restart of its process
|
|
103
103
|
// (`run-carry-conversation.ts`). Optional for the same reason as the pid.
|
|
104
104
|
owner_transcript?: string | undefined
|
|
105
|
+
// The owner is a session the run launched headless (`run-headless.ts`), so nobody can end the run
|
|
106
|
+
// from it. Kept here, beside the owner it describes, so it outlives the supervisor's own record.
|
|
107
|
+
is_owner_headless?: boolean | undefined
|
|
105
108
|
// Set by `--cut`, and by nothing else. A crash never reaches `--cut`, which is what makes a
|
|
106
109
|
// declared cut the only standing record carried without a person deciding.
|
|
107
110
|
is_handed_off?: boolean | undefined
|
|
@@ -138,6 +141,7 @@ interface CarryOwner {
|
|
|
138
141
|
pid?: number | undefined
|
|
139
142
|
start?: string | undefined
|
|
140
143
|
transcript?: string | undefined
|
|
144
|
+
is_headless?: boolean | undefined
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
// **`resume` is the only one of the four that establishes anything.** The other three are refusals,
|
|
@@ -199,6 +203,7 @@ const run_carry_schema = z.object({
|
|
|
199
203
|
owner_pid: z.number().optional(),
|
|
200
204
|
owner_start: z.string().optional(),
|
|
201
205
|
owner_transcript: z.string().optional(),
|
|
206
|
+
is_owner_headless: z.boolean().optional(),
|
|
202
207
|
is_handed_off: z.boolean().optional(),
|
|
203
208
|
done: z.array(z.number()).optional(),
|
|
204
209
|
retrospective: z.boolean().optional(),
|
|
@@ -265,6 +270,7 @@ function fresh_carry(invocation: string, owner: CarryOwner, now: Date): RunCarry
|
|
|
265
270
|
owner_pid: owner.pid,
|
|
266
271
|
owner_start: owner.start,
|
|
267
272
|
owner_transcript: owner.transcript,
|
|
273
|
+
is_owner_headless: owner.is_headless,
|
|
268
274
|
}
|
|
269
275
|
}
|
|
270
276
|
|
|
@@ -297,7 +303,7 @@ function adopt_carry(
|
|
|
297
303
|
owner: CarryOwner = NO_OWNER,
|
|
298
304
|
): RunCarry | undefined {
|
|
299
305
|
// The run's own fields carry across untouched — the counters, the outage fold timestamp, `done` so the successor does not re-run what the cut session finished,
|
|
300
|
-
// and `retrospective` so a run resumed after a cut does not repeat its retrospective. Only the
|
|
306
|
+
// and `retrospective` so a run resumed after a cut does not repeat its retrospective. Only the
|
|
301
307
|
// ownership fields are overridden: the new owner replaces the old — a caller that declared none
|
|
302
308
|
// writes `undefined`, so the previous owner cannot survive — and the declared hand-off is spent.
|
|
303
309
|
const next: RunCarry = {
|
|
@@ -305,6 +311,7 @@ function adopt_carry(
|
|
|
305
311
|
owner_pid: owner.pid,
|
|
306
312
|
owner_start: owner.start,
|
|
307
313
|
owner_transcript: owner.transcript,
|
|
314
|
+
is_owner_headless: owner.is_headless,
|
|
308
315
|
is_handed_off: false,
|
|
309
316
|
}
|
|
310
317
|
|
|
@@ -336,6 +343,21 @@ function is_foreign_live_owner(carry: RunCarry, owner: CarryOwner): boolean {
|
|
|
336
343
|
return is_owner_live(carry) && !is_owned_by(carry, owner)
|
|
337
344
|
}
|
|
338
345
|
|
|
346
|
+
// Whether a process other than the caller still holds the record. **By process alone, never by
|
|
347
|
+
// conversation**: the wake supervisor inherits the environment of the session that started it, so a
|
|
348
|
+
// conversation match would read that live session's record as the supervisor's own. The driver adopts
|
|
349
|
+
// the record for the supervisor's process (`run-wake-driver.ts` → `claim_record`), so without this a
|
|
350
|
+
// supervisor reads itself as a session at work.
|
|
351
|
+
function is_held_by_another(
|
|
352
|
+
carry: RunCarry,
|
|
353
|
+
is_live: (carry: RunCarry) => boolean = is_owner_live,
|
|
354
|
+
): boolean {
|
|
355
|
+
const caller = owner_of(process.pid)
|
|
356
|
+
const is_own = carry.owner_pid === caller.pid && carry.owner_start === caller.start
|
|
357
|
+
|
|
358
|
+
return !is_own && is_live(carry)
|
|
359
|
+
}
|
|
360
|
+
|
|
339
361
|
function is_handed_off_to(carry: RunCarry, invocation: string): boolean {
|
|
340
362
|
return carry.is_handed_off === true && carry.invocation === invocation
|
|
341
363
|
}
|
|
@@ -504,6 +526,7 @@ const run_carry = {
|
|
|
504
526
|
fresh_carry,
|
|
505
527
|
is_count_refused,
|
|
506
528
|
is_foreign_live_owner,
|
|
529
|
+
is_held_by_another,
|
|
507
530
|
is_owner_live,
|
|
508
531
|
lost_message,
|
|
509
532
|
nothing_to_resume_message,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
4
|
+
import { issue_watch } from '#scripts/issue/issue-watch'
|
|
4
5
|
import { session_cite } from '#scripts/issue/session-cite'
|
|
5
6
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
6
7
|
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
@@ -254,6 +255,10 @@ async function proceed(issue_number: string, hold: string, cost: string): Promis
|
|
|
254
255
|
await run_label.mark(issue_number)
|
|
255
256
|
const reads = await gather_reads(issue_number)
|
|
256
257
|
|
|
258
|
+
// The issue the report prints is the one the run has read, so it is the baseline a person's later
|
|
259
|
+
// update is measured against (`issue-watch.ts`).
|
|
260
|
+
await issue_watch.record_baseline_here(issue_number)
|
|
261
|
+
|
|
257
262
|
return emit({ issue_number, hold, cost, verdict: reads.verdict, report: reads.report })
|
|
258
263
|
}
|
|
259
264
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
2
|
+
|
|
3
|
+
// The words of a `pr-checks` event (joshuafolkken/kit#3770): `josh followup` writes them and `run:board`
|
|
4
|
+
// reads them back, so the one spelling lives here. A line reads
|
|
5
|
+
// `#3754 PR 3765 pass 6 pending 2 fail 0 coderabbit running` — the issue first, as every event names
|
|
6
|
+
// it, so the stream's readers key it to the child as they key any other.
|
|
7
|
+
|
|
8
|
+
const CODERABBIT_STATES = ['running', 'done', 'rate-limited', 'failed', 'none'] as const
|
|
9
|
+
|
|
10
|
+
type CodeRabbitState = (typeof CODERABBIT_STATES)[number]
|
|
11
|
+
|
|
12
|
+
interface PrChecks {
|
|
13
|
+
pr: number
|
|
14
|
+
pass: number
|
|
15
|
+
pending: number
|
|
16
|
+
fail: number
|
|
17
|
+
coderabbit: CodeRabbitState
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CHECKS_PATTERN =
|
|
21
|
+
/^#\d+ PR (?<pr>\d+) pass (?<pass>\d+) pending (?<pending>\d+) fail (?<fail>\d+) coderabbit (?<coderabbit>\S+)$/u
|
|
22
|
+
|
|
23
|
+
function text_of(issue: string, checks: PrChecks): string {
|
|
24
|
+
const { pr, pass, pending, fail, coderabbit } = checks
|
|
25
|
+
|
|
26
|
+
return `${issue_cite.plain(issue)} PR ${String(pr)} pass ${String(pass)} pending ${String(pending)} fail ${String(fail)} coderabbit ${coderabbit}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const STATE_NAMES: ReadonlySet<string> = new Set(CODERABBIT_STATES)
|
|
30
|
+
|
|
31
|
+
function is_coderabbit_state(value = ''): value is CodeRabbitState {
|
|
32
|
+
return STATE_NAMES.has(value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The checks a line carries, `undefined` for a line written in some other shape.
|
|
36
|
+
function checks_of(text: string): PrChecks | undefined {
|
|
37
|
+
const groups = CHECKS_PATTERN.exec(text)?.groups
|
|
38
|
+
const coderabbit = groups?.['coderabbit']
|
|
39
|
+
|
|
40
|
+
if (groups === undefined || !is_coderabbit_state(coderabbit)) return undefined
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
pr: Number(groups['pr']),
|
|
44
|
+
pass: Number(groups['pass']),
|
|
45
|
+
pending: Number(groups['pending']),
|
|
46
|
+
fail: Number(groups['fail']),
|
|
47
|
+
coderabbit,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const run_event_checks = { checks_of, text_of }
|
|
52
|
+
|
|
53
|
+
export { run_event_checks }
|
|
54
|
+
export type { CodeRabbitState, PrChecks }
|
|
@@ -28,6 +28,7 @@ const KIND_LABELS: Readonly<Record<EventKind, KindLabel>> = {
|
|
|
28
28
|
outage: { ja: '障害', en: 'outage' },
|
|
29
29
|
cut: { ja: 'セッション交代', en: 'session cut' },
|
|
30
30
|
resume: { ja: '実装を再開', en: 'implementation resumed' },
|
|
31
|
+
restart: { ja: '止めて出し直し', en: 'restarted' },
|
|
31
32
|
drain: { ja: 'バックログが空', en: 'backlog drained' },
|
|
32
33
|
stop: { ja: '停止', en: 'stopped' },
|
|
33
34
|
'pr-opened': { ja: 'PR 作成', en: 'PR opened' },
|
|
@@ -44,6 +45,8 @@ const KIND_LABELS: Readonly<Record<EventKind, KindLabel>> = {
|
|
|
44
45
|
'lane-phase': { ja: 'レーンの段階', en: 'lane phase' },
|
|
45
46
|
add: { ja: '割り込み', en: 'added' },
|
|
46
47
|
'lane-limit': { ja: 'レーン上限', en: 'lane limit' },
|
|
48
|
+
setup: { ja: '準備', en: 'setup' },
|
|
49
|
+
'pr-checks': { ja: 'PR のチェック', en: 'PR checks' },
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
const LABELS_BY_KIND: ReadonlyMap<string, KindLabel> = new Map(Object.entries(KIND_LABELS))
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
2
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
3
|
+
import { run_event_stream } from './run-event-stream'
|
|
4
|
+
import { run_event_stream_emit } from './run-event-stream-emit'
|
|
5
|
+
|
|
6
|
+
// The stages a run passes between `josh backlogrun` and its first child, and those of a later hand-back
|
|
7
|
+
// to a session, each written to the stream as one `setup` event by the step that enters it. **The text
|
|
8
|
+
// is the stage's own words**: `run:board` draws it as written, so the message a person reads and the
|
|
9
|
+
// `💓` beside it come from one event and cannot disagree. Only the stages of a lane's launch — opening
|
|
10
|
+
// it, installing into it, dispatching its child — name an issue, which is how a reader tells them from
|
|
11
|
+
// the stages before them.
|
|
12
|
+
|
|
13
|
+
const STAGE = {
|
|
14
|
+
// `josh backlogrun` started the parent session, which is reading its procedure.
|
|
15
|
+
STARTING: 'starting session',
|
|
16
|
+
// `run:carry --begin` claimed the run, and the parent is planning it.
|
|
17
|
+
PLANNING: 'planning',
|
|
18
|
+
// The supervisor took the run over from the parent and is asking for the first offer.
|
|
19
|
+
HANDING_OFF: 'handing off',
|
|
20
|
+
// The driver answered something only a session can act on — an untriaged child, an epic left to a
|
|
21
|
+
// person — and the supervisor is waking one. The next hand-off replaces it.
|
|
22
|
+
JUDGING: 'waiting on judgment',
|
|
23
|
+
// The supervisor started that session, which is now acting on the driver's answer. It is detached, so
|
|
24
|
+
// its end is the supervisor's next hand-off, which replaces this.
|
|
25
|
+
SESSION: 'judgment session running',
|
|
26
|
+
// The driver's launch of a lane returned without a child: no lane was free to open, or a step after
|
|
27
|
+
// the open failed. It names no issue, so the lane's own stage ends here whatever the driver does next.
|
|
28
|
+
NO_CHILD: 'no child launched',
|
|
29
|
+
} as const
|
|
30
|
+
|
|
31
|
+
const OPENING = 'opening lane'
|
|
32
|
+
const INSTALLING = 'installing dependencies'
|
|
33
|
+
const DISPATCHING = 'dispatching'
|
|
34
|
+
// The stages of one lane's launch, in the order a lane passes them.
|
|
35
|
+
const LANE_STAGE = { OPENING, INSTALLING, DISPATCHING } as const
|
|
36
|
+
|
|
37
|
+
// The driver is about to open a lane for `issue` — for every child, not only the first.
|
|
38
|
+
function opening_lane(issue: string): string {
|
|
39
|
+
return `${OPENING} ${issue_cite.plain(issue)}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The lane's work tree exists and its dependencies are about to be installed into it.
|
|
43
|
+
function installing(issue: string): string {
|
|
44
|
+
return `${INSTALLING} ${issue_cite.plain(issue)}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The lane is ready and its child is about to be dispatched; the child's own `child-launch` follows.
|
|
48
|
+
function dispatching(issue: string): string {
|
|
49
|
+
return `${DISPATCHING} ${issue_cite.plain(issue)}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Best-effort, as every append to this stream is: a stage that could not be written is a board that
|
|
53
|
+
// says less, never a run that failed.
|
|
54
|
+
async function emit(text: string): Promise<void> {
|
|
55
|
+
await run_event_stream_emit.emit(run_event_stream.EVENT_KIND.SETUP, text)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The same append to a stream the caller resolves, dated the moment it hands over.
|
|
59
|
+
function append_stage(resolve_target: () => string, text: string, at: Date): void {
|
|
60
|
+
try {
|
|
61
|
+
const { SETUP } = run_event_stream.EVENT_KIND
|
|
62
|
+
|
|
63
|
+
run_event_stream.append(resolve_target(), SETUP, text, at.toISOString())
|
|
64
|
+
} catch (error) {
|
|
65
|
+
error_text.trace_swallowed('run_event_setup.append_stage', error)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// For a caller that already holds the repository's common git directory and the moment it wants the
|
|
70
|
+
// stage dated — the launcher, whose record and first stage carry one time.
|
|
71
|
+
function emit_in(repository: string, text: string, at: Date): void {
|
|
72
|
+
append_stage(() => run_event_stream.target_of(repository), text, at)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// For a caller that already holds the stream's own path — the supervisor, which resolved it once.
|
|
76
|
+
function emit_to(target: string, text: string, at: Date): void {
|
|
77
|
+
append_stage(() => target, text, at)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const run_event_setup = {
|
|
81
|
+
LANE_STAGE,
|
|
82
|
+
STAGE,
|
|
83
|
+
dispatching,
|
|
84
|
+
emit,
|
|
85
|
+
emit_in,
|
|
86
|
+
emit_to,
|
|
87
|
+
installing,
|
|
88
|
+
opening_lane,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { run_event_setup }
|
|
@@ -60,6 +60,10 @@ const EVENT_KIND = {
|
|
|
60
60
|
// resume clears the cut record, so without this the `cut` would stay the newest position and
|
|
61
61
|
// `run:step` would keep answering the resume command it had just run.
|
|
62
62
|
RESUME: 'resume',
|
|
63
|
+
// A person stopped a running lane child and re-dispatched its issue in a fresh session
|
|
64
|
+
// (`lane:restart`). It follows the fresh dispatch's `CHILD_LAUNCH`, so it is a launch to the parent and
|
|
65
|
+
// the board, and — like that launch — a batch record a lane child never reads as its own position.
|
|
66
|
+
RESTART: 'restart',
|
|
63
67
|
// The backlog emptied while nothing of the run's own was in flight — the drain. It marks the position
|
|
64
68
|
// at which the end-of-run retrospective is owed, *before* the idle watch opens, so `run:step` fires
|
|
65
69
|
// the retrospective at the drain rather than after the watch runs its course. It is not itself a
|
|
@@ -83,7 +87,8 @@ const EVENT_KIND = {
|
|
|
83
87
|
// stalled is pointed straight at dispatching the work rather than left waiting.
|
|
84
88
|
STALL: 'stall',
|
|
85
89
|
// The run's own driver is gone: the cut handed the budget off, the owner has died, and no supervisor
|
|
86
|
-
// is watching. Emitted once per strand episode by the stop-time detector
|
|
90
|
+
// is watching. Emitted once per strand episode by the stop-time detector — or by the `run:wake`
|
|
91
|
+
// supervisor as it fails, leaves that state behind and has warned of it —
|
|
87
92
|
// exactly as `STALL` is — the newest-event dedup keeps a run polled every stop from notifying more
|
|
88
93
|
// than once, and a terminal reader following the stream sees the line. The strand is the step before
|
|
89
94
|
// the stall: a stalled run still has a driver that could dispatch, a stranded one has none.
|
|
@@ -124,6 +129,16 @@ const EVENT_KIND = {
|
|
|
124
129
|
// arrival probe reads it as a wake: the lanes a raise frees are new room for a pool its baseline
|
|
125
130
|
// already holds. A trace: it says the run gained room, not where the run is.
|
|
126
131
|
LANE_LIMIT: 'lane-limit',
|
|
132
|
+
// One stage of the run's start — the session starting, the plan, the hand-off to the supervisor, a
|
|
133
|
+
// lane opening (`run-event-setup.ts`). `run:board` draws it while the run stands at it, so the
|
|
134
|
+
// minutes before one read as work rather than as a run that has stopped. A trace: it says the run is
|
|
135
|
+
// getting ready, not where the run is.
|
|
136
|
+
SETUP: 'setup',
|
|
137
|
+
// A child's pull request checks as `josh followup` last polled them — the CI counts and CodeRabbit's
|
|
138
|
+
// state (`run-event-checks.ts`), written only when they change, so `run:board` draws a child waiting
|
|
139
|
+
// in followup without asking GitHub (joshuafolkken/kit#3770). A trace: it says how the checks stand,
|
|
140
|
+
// not where the run is.
|
|
141
|
+
PR_CHECKS: 'pr-checks',
|
|
127
142
|
} as const
|
|
128
143
|
|
|
129
144
|
type EventKind = (typeof EVENT_KIND)[keyof typeof EVENT_KIND]
|
|
@@ -141,13 +156,21 @@ const TRACE_KINDS: ReadonlySet<string> = new Set([
|
|
|
141
156
|
EVENT_KIND.LANE_PHASE,
|
|
142
157
|
EVENT_KIND.ADD,
|
|
143
158
|
EVENT_KIND.LANE_LIMIT,
|
|
159
|
+
EVENT_KIND.SETUP,
|
|
160
|
+
EVENT_KIND.PR_CHECKS,
|
|
144
161
|
])
|
|
145
162
|
|
|
146
163
|
// The trace kinds that say only that the run was alive, so the bound rolls them off first. A `filed` or
|
|
147
164
|
// `note` line is a trace to the position readers but is the board's record of what the run found, so
|
|
148
165
|
// it rolls off only with the positions. A ship-stage `start` is disposable but goes last of them:
|
|
149
|
-
// `run:board` draws from that line.
|
|
150
|
-
|
|
166
|
+
// `run:board` draws from that line. Only the newest `setup` line is the stage the run stands at,
|
|
167
|
+
// so every one but the newest goes; likewise only each child's newest `pr-checks` is drawn.
|
|
168
|
+
const DISPOSABLE_KINDS: ReadonlySet<string> = new Set([
|
|
169
|
+
EVENT_KIND.SHIP_STAGE,
|
|
170
|
+
EVENT_KIND.HEARTBEAT,
|
|
171
|
+
EVENT_KIND.SETUP,
|
|
172
|
+
EVENT_KIND.PR_CHECKS,
|
|
173
|
+
])
|
|
151
174
|
|
|
152
175
|
const event_schema = z.object({
|
|
153
176
|
pos: z.number(),
|
|
@@ -217,17 +240,18 @@ function last_position(events: ReadonlyArray<RunEvent>): number {
|
|
|
217
240
|
return events[events.length - POSITION_INCREMENT]?.pos ?? FIRST_POSITION
|
|
218
241
|
}
|
|
219
242
|
|
|
220
|
-
// The issue a ship-stage names — `#<N>
|
|
243
|
+
// The issue a ship-stage or a `pr-checks` line names — `#<N> …` carries it first.
|
|
221
244
|
function issue_of(event: RunEvent): string {
|
|
222
245
|
return event.text.split(WORD_SEPARATOR)[FIRST_POSITION] ?? ''
|
|
223
246
|
}
|
|
224
247
|
|
|
225
|
-
// Each issue's newest ship-stage
|
|
226
|
-
|
|
248
|
+
// Each issue's newest line of one kind: the ship-stage the board reads its furthest phase from, and the
|
|
249
|
+
// `pr-checks` it draws a followup row's end from.
|
|
250
|
+
function newest_by_issue(events: ReadonlyArray<RunEvent>, kind: string): ReadonlySet<RunEvent> {
|
|
227
251
|
const newest = new Map<string, RunEvent>()
|
|
228
252
|
|
|
229
253
|
for (const event of events) {
|
|
230
|
-
if (event.kind ===
|
|
254
|
+
if (event.kind === kind) newest.set(issue_of(event), event)
|
|
231
255
|
}
|
|
232
256
|
|
|
233
257
|
return new Set(newest.values())
|
|
@@ -241,10 +265,22 @@ function is_stage_start(event: RunEvent): boolean {
|
|
|
241
265
|
return event.text.split(WORD_SEPARATOR)[STAGE_PHASE_INDEX] === run_ship_stage.PHASE.START
|
|
242
266
|
}
|
|
243
267
|
|
|
268
|
+
// The disposable lines the bound never drops: each issue's newest ship-stage and `pr-checks` and the
|
|
269
|
+
// run's newest `setup` stage, the ones `run:board` draws from now.
|
|
270
|
+
function drawn_lines(events: ReadonlyArray<RunEvent>): ReadonlySet<RunEvent> {
|
|
271
|
+
const setup = events.findLast((event) => event.kind === EVENT_KIND.SETUP)
|
|
272
|
+
|
|
273
|
+
return new Set([
|
|
274
|
+
...newest_by_issue(events, EVENT_KIND.SHIP_STAGE),
|
|
275
|
+
...newest_by_issue(events, EVENT_KIND.PR_CHECKS),
|
|
276
|
+
...(setup === undefined ? [] : [setup]),
|
|
277
|
+
])
|
|
278
|
+
}
|
|
279
|
+
|
|
244
280
|
// The lines the bound may drop, in the order it drops them: the ones that draw nothing first, then the
|
|
245
281
|
// ones `run:board` draws from, each group oldest first.
|
|
246
282
|
function droppable(events: ReadonlyArray<RunEvent>): ReadonlyArray<RunEvent> {
|
|
247
|
-
const kept =
|
|
283
|
+
const kept = drawn_lines(events)
|
|
248
284
|
const disposable = events.filter((event) => DISPOSABLE_KINDS.has(event.kind) && !kept.has(event))
|
|
249
285
|
|
|
250
286
|
return [
|
|
@@ -261,7 +297,8 @@ function droppable(events: ReadonlyArray<RunEvent>): ReadonlyArray<RunEvent> {
|
|
|
261
297
|
//
|
|
262
298
|
// **Each issue's newest ship-stage is not disposable**: it is the only record `run:board` draws a lane's
|
|
263
299
|
// review-and-later phase from, and on a stream full of positions it would otherwise roll off in its own
|
|
264
|
-
// append.
|
|
300
|
+
// append. **The newest `setup` stage is kept for the same reason**: it is the stage `run:board` draws
|
|
301
|
+
// while the run stands at it, and a run that starts on a full stream would otherwise show none.
|
|
265
302
|
//
|
|
266
303
|
// **A ship-stage `start` outlasts the other disposable lines**: each one is a phase on the lane's track,
|
|
267
304
|
// so it is kept past the `done` lines and the heartbeats.
|