@joshuafolkken/kit 1.1387.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-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
- 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 +288 -229
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +62 -9
- 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-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -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-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 +24 -0
- 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 +15 -0
- package/scripts/init/start-steps.ts +46 -0
- package/scripts/init/start.ts +1 -0
- 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 +24 -0
- 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 +129 -53
- 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 +49 -3
- 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 +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- 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 +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -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 +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- 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/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 +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- 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,3 +1,4 @@
|
|
|
1
|
+
import type { Declared } from '#scripts/backlog/backlog-overlap'
|
|
1
2
|
import { backlog_plan, type PlanContext } from '#scripts/backlog/backlog-plan'
|
|
2
3
|
import type { WavePlan } from '#scripts/backlog/backlog-waves'
|
|
3
4
|
import type { EpicChild } from '#scripts/epic/epic-graph'
|
|
@@ -25,8 +26,22 @@ interface BoardPlan {
|
|
|
25
26
|
// Each issue's label names as the open listing read them, so a running row is drawn as its labels
|
|
26
27
|
// say it stopped.
|
|
27
28
|
labels: ReadonlyMap<number, ReadonlyArray<string>>
|
|
29
|
+
// The paths each issue's body declares (`Plan.scope.declared`), which a waiting row is checked
|
|
30
|
+
// against a running one by; a plan read without them checks none.
|
|
31
|
+
declared?: Declared | undefined
|
|
28
32
|
}
|
|
29
33
|
|
|
34
|
+
// Why a waiting row waits, or the stage its lane's launch is at, in the board's own words.
|
|
35
|
+
interface RowReason {
|
|
36
|
+
text: string
|
|
37
|
+
// When the row entered a stage, where the reason is one — how long it has stood there is counted
|
|
38
|
+
// from it.
|
|
39
|
+
since_ms?: number | undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// What a waiting child's row says after its blockers; `undefined` says nothing.
|
|
43
|
+
type ReasonOf = (child: EpicChild) => RowReason | undefined
|
|
44
|
+
|
|
30
45
|
interface BoardRow {
|
|
31
46
|
number: number
|
|
32
47
|
title: string | undefined
|
|
@@ -36,6 +51,8 @@ interface BoardRow {
|
|
|
36
51
|
status: ItemStatus | undefined
|
|
37
52
|
// The standing blockers this row still waits on, as the plan names them.
|
|
38
53
|
waits: ReadonlyArray<string>
|
|
54
|
+
// Why the row waits beyond its blockers (`run-board-reason.ts`); a row the run has touched has none.
|
|
55
|
+
reason?: RowReason | undefined
|
|
39
56
|
}
|
|
40
57
|
|
|
41
58
|
type WaveEntry =
|
|
@@ -60,6 +77,11 @@ interface Overlay {
|
|
|
60
77
|
statuses: ReadonlyMap<number, ItemStatus>
|
|
61
78
|
}
|
|
62
79
|
|
|
80
|
+
// The overlay a layout is drawn from: the plan under the statuses, and what each waiting row says.
|
|
81
|
+
interface Drawn extends Overlay {
|
|
82
|
+
reason_of: ReasonOf | undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
63
85
|
const SETTLED_STATES: ReadonlySet<ItemState> = new Set(['merged', 'parked', 'done'])
|
|
64
86
|
|
|
65
87
|
function is_settled(state: ItemState): boolean {
|
|
@@ -80,13 +102,20 @@ function reference_of(edge: IssueReference, repo: string): string {
|
|
|
80
102
|
return edge.repo === repo ? String(edge.number) : issue_cite.plain(edge.number, edge.repo)
|
|
81
103
|
}
|
|
82
104
|
|
|
83
|
-
|
|
105
|
+
// Whether a blocker the plan names still stands for `child` — the wait its row draws as 🔗.
|
|
106
|
+
function is_blocked(child: EpicChild, overlay: Overlay): boolean {
|
|
107
|
+
return child.blocked_by.some((edge) => is_waiting_on(edge, overlay))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function plan_row(child: EpicChild, state: ItemState, overlay: Drawn): BoardRow {
|
|
84
111
|
const { context } = overlay.plan
|
|
85
112
|
const waits = child.blocked_by
|
|
86
113
|
.filter((edge) => is_waiting_on(edge, overlay))
|
|
87
114
|
.map((edge) => reference_of(edge, context.repo))
|
|
115
|
+
const reason = state === 'waiting' ? overlay.reason_of?.(child) : undefined
|
|
88
116
|
|
|
89
117
|
return {
|
|
118
|
+
...(reason !== undefined && { reason }),
|
|
90
119
|
number: child.number,
|
|
91
120
|
title: context.titles.get(child.number),
|
|
92
121
|
kind: run_board_kind.kind_of(child.labels),
|
|
@@ -119,7 +148,7 @@ function active_rows(statuses: ReadonlyMap<number, ItemStatus>, read: RowRead):
|
|
|
119
148
|
function place_in_epic(
|
|
120
149
|
entries: Array<WaveEntry>,
|
|
121
150
|
epic: number,
|
|
122
|
-
overlay:
|
|
151
|
+
overlay: Drawn,
|
|
123
152
|
row: BoardRow,
|
|
124
153
|
): void {
|
|
125
154
|
const group = entries.find((entry) => entry.kind === 'epic' && entry.epic === epic)
|
|
@@ -144,7 +173,7 @@ function wave_state(child: EpicChild): ItemState {
|
|
|
144
173
|
|
|
145
174
|
// One wave, its rows in the plan's order, an epic's children gathered under the epic where the first
|
|
146
175
|
// of them falls.
|
|
147
|
-
function wave_entries(wave: ReadonlyArray<EpicChild>, overlay:
|
|
176
|
+
function wave_entries(wave: ReadonlyArray<EpicChild>, overlay: Drawn): Array<WaveEntry> {
|
|
148
177
|
const entries: Array<WaveEntry> = []
|
|
149
178
|
|
|
150
179
|
for (const child of wave) {
|
|
@@ -158,7 +187,7 @@ function wave_entries(wave: ReadonlyArray<EpicChild>, overlay: Overlay): Array<W
|
|
|
158
187
|
return entries
|
|
159
188
|
}
|
|
160
189
|
|
|
161
|
-
function unreached_rows(overlay:
|
|
190
|
+
function unreached_rows(overlay: Drawn, is_people: boolean): Array<BoardRow> {
|
|
162
191
|
const state: ItemState = is_people ? 'human' : 'waiting'
|
|
163
192
|
|
|
164
193
|
return overlay.plan.waves.unreached
|
|
@@ -166,8 +195,25 @@ function unreached_rows(overlay: Overlay, is_people: boolean): Array<BoardRow> {
|
|
|
166
195
|
.map((child) => plan_row(child, state, overlay))
|
|
167
196
|
}
|
|
168
197
|
|
|
169
|
-
|
|
170
|
-
|
|
198
|
+
// Every issue the plan holds, wave or unreached.
|
|
199
|
+
function children_of(plan: BoardPlan): Array<EpicChild> {
|
|
200
|
+
return [...plan.waves.waves.flat(), ...plan.waves.unreached]
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The children whose rows wait on the run rather than on a person: the run has not touched them, and
|
|
204
|
+
// no decision is asked of anyone.
|
|
205
|
+
function waiting_children(overlay: Overlay): Array<EpicChild> {
|
|
206
|
+
return children_of(overlay.plan).filter(
|
|
207
|
+
(child) => is_untouched(child, overlay) && !is_human(child),
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function layout_of(
|
|
212
|
+
plan: BoardPlan,
|
|
213
|
+
statuses: ReadonlyMap<number, ItemStatus>,
|
|
214
|
+
reason_of?: ReasonOf,
|
|
215
|
+
): BoardLayout {
|
|
216
|
+
const overlay = { plan, statuses, reason_of }
|
|
171
217
|
const waves = plan.waves.waves.map((wave) =>
|
|
172
218
|
wave_entries(
|
|
173
219
|
wave.filter((child) => is_untouched(child, overlay)),
|
|
@@ -195,9 +241,9 @@ function local_layout_of(statuses: ReadonlyMap<number, ItemStatus>): BoardLayout
|
|
|
195
241
|
}
|
|
196
242
|
}
|
|
197
243
|
|
|
198
|
-
//
|
|
244
|
+
// The run's scope a lane is checked against.
|
|
199
245
|
function numbers_of(plan: BoardPlan): ReadonlySet<number> {
|
|
200
|
-
return new Set(
|
|
246
|
+
return new Set(children_of(plan).map((child) => child.number))
|
|
201
247
|
}
|
|
202
248
|
|
|
203
249
|
// Every row the layout draws, an epic's children among them — what the header counts and the legend reads.
|
|
@@ -211,12 +257,14 @@ function rows_of(layout: BoardLayout): Array<BoardRow> {
|
|
|
211
257
|
|
|
212
258
|
const run_board_layout = {
|
|
213
259
|
EMPTY_LAYOUT,
|
|
260
|
+
is_blocked,
|
|
214
261
|
is_settled,
|
|
215
262
|
layout_of,
|
|
216
263
|
local_layout_of,
|
|
217
264
|
numbers_of,
|
|
218
265
|
rows_of,
|
|
266
|
+
waiting_children,
|
|
219
267
|
}
|
|
220
268
|
|
|
221
269
|
export { run_board_layout }
|
|
222
|
-
export type { BoardLayout, BoardPlan, BoardRow, WaveEntry }
|
|
270
|
+
export type { BoardLayout, BoardPlan, BoardRow, Overlay, ReasonOf, RowReason, WaveEntry }
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
2
|
+
import { run_event_setup } from '#scripts/run/event/run-event-setup'
|
|
3
|
+
import { run_board_labels } from './run-board-labels'
|
|
4
|
+
import type { LaneEvent } from './run-board-lane-event'
|
|
5
|
+
import { run_board_operation, type Operation } from './run-board-operation'
|
|
6
|
+
import type { SessionOperations } from './run-board-operation-read'
|
|
7
|
+
import type { RunActivity } from './run-board-status'
|
|
8
|
+
|
|
9
|
+
// The header's third line: what the run is doing now, as its kind, when it happened and its words.
|
|
10
|
+
// Always one line while the stream holds an event — the newer of a running child's newest operation and
|
|
11
|
+
// its newest event, or the newest operation of the session a stage is the work of, else the stage the
|
|
12
|
+
// run is at, else how many children run, else how long it has watched, each of the last three with why;
|
|
13
|
+
// an ended run keeps the newest thing it did. **An operation changes with every call a session makes**,
|
|
14
|
+
// where a stage or a count stands for minutes. Every time is an event's or a call's own, so the line
|
|
15
|
+
// never guesses one; the header ages it and draws it in its time column (joshuafolkken/kit#3767).
|
|
16
|
+
|
|
17
|
+
const { KIND_BLANK, NOW_ICONS, PHASE_ICONS, STAGE_ICONS, STATE_ICONS, WORDS } = run_board_labels
|
|
18
|
+
const { STAGE } = run_event_setup
|
|
19
|
+
const REASON_SEPARATOR = ' — '
|
|
20
|
+
const OPERATION_SEPARATOR = ' · '
|
|
21
|
+
const WORD_SEPARATOR = ' '
|
|
22
|
+
// The stages a session is at work on, and whose log says what it does. Every other stage is the
|
|
23
|
+
// driver's or the supervisor's own step, which writes no log of calls.
|
|
24
|
+
const STAGE_SESSIONS: Readonly<Partial<Record<string, 'owner' | 'judgment'>>> = {
|
|
25
|
+
[STAGE.PLANNING]: 'owner',
|
|
26
|
+
[STAGE.SESSION]: 'judgment',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface NowRead {
|
|
30
|
+
now_ms: number
|
|
31
|
+
// Set once the run has ended: the newest event's age stops there.
|
|
32
|
+
ended_ms: number | undefined
|
|
33
|
+
activity: RunActivity
|
|
34
|
+
// The newest operation of each session of the run, `undefined` where none was read.
|
|
35
|
+
operations?: SessionOperations | undefined
|
|
36
|
+
// The issues of the children running now: a lane left by an earlier run is not this run's work.
|
|
37
|
+
children?: ReadonlySet<number> | undefined
|
|
38
|
+
running: number
|
|
39
|
+
// What the plan has left for the run, `undefined` until the plan is read.
|
|
40
|
+
waiting: number | undefined
|
|
41
|
+
// How many of those wait on a person.
|
|
42
|
+
people: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface NowLine {
|
|
46
|
+
// The kind's icon, drawn under the `⚡`.
|
|
47
|
+
icon: string
|
|
48
|
+
// The icon's word in the legend, `undefined` where the line's own words or the phase legend name it.
|
|
49
|
+
legend?: string | undefined
|
|
50
|
+
// How long ago what the line says happened — up to an ended run's end — `undefined` where no event
|
|
51
|
+
// gave it a time.
|
|
52
|
+
age_ms: number | undefined
|
|
53
|
+
text: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A line before it is aged: when what it says happened.
|
|
57
|
+
type TimedLine = Omit<NowLine, 'age_ms'> & { since_ms: number | undefined }
|
|
58
|
+
|
|
59
|
+
// What stands behind the pool once nothing runs: nothing, only what a person has to decide, or issues
|
|
60
|
+
// the run cannot take yet.
|
|
61
|
+
function pool_reason(waiting: number, people: number): string {
|
|
62
|
+
if (waiting === 0) return WORDS.pool_empty
|
|
63
|
+
|
|
64
|
+
return waiting === people
|
|
65
|
+
? `${String(waiting)} ${WORDS.on_person}`
|
|
66
|
+
: `${String(waiting)} ${WORDS.waiting}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function running_of(running: number): string {
|
|
70
|
+
return `${String(running)} ${WORDS.in_progress}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Why the run is where it is: the children it runs, else what its plan has left. Nothing before the
|
|
74
|
+
// plan is read.
|
|
75
|
+
function reason_of(read: NowRead): string | undefined {
|
|
76
|
+
if (read.running > 0) return running_of(read.running)
|
|
77
|
+
|
|
78
|
+
return read.waiting === undefined ? undefined : pool_reason(read.waiting, read.people)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function with_reason(line: string, read: NowRead): string {
|
|
82
|
+
const reason = reason_of(read)
|
|
83
|
+
|
|
84
|
+
return reason === undefined ? line : `${line}${REASON_SEPARATOR}${reason}`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The newest thing an ended run did.
|
|
88
|
+
function latest_line(read: NowRead): TimedLine | undefined {
|
|
89
|
+
const { latest } = read.activity
|
|
90
|
+
|
|
91
|
+
if (latest === undefined) return undefined
|
|
92
|
+
|
|
93
|
+
return { icon: NOW_ICONS.ended, legend: WORDS.ended, since_ms: latest.at_ms, text: latest.text }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A run with nothing running and at no stage is watching its pool, since the newest thing it did — a
|
|
97
|
+
// stage it has left included, so a judgment session's minutes are not counted as watching.
|
|
98
|
+
function watch_line(read: NowRead): TimedLine | undefined {
|
|
99
|
+
const { idle_ms } = read.activity
|
|
100
|
+
|
|
101
|
+
if (idle_ms === undefined) return undefined
|
|
102
|
+
|
|
103
|
+
return { icon: NOW_ICONS.watching, since_ms: idle_ms, text: with_reason(WORDS.watching, read) }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// `words`, then what the session did — words that end on a command or a path.
|
|
107
|
+
function operated(words: string, operation: Operation): string {
|
|
108
|
+
return `${words}${OPERATION_SEPARATOR}${run_board_operation.words_of(operation)}`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function session_operation(read: NowRead, setup: string): Operation | undefined {
|
|
112
|
+
const session = STAGE_SESSIONS[setup]
|
|
113
|
+
|
|
114
|
+
return session === undefined ? undefined : read.operations?.[session]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The newest operation of the session a stage is the work of. One older than the stage is an earlier
|
|
118
|
+
// session's, or what the parent did before it — the watcher's log runs on across runs.
|
|
119
|
+
function stage_operation(read: NowRead, setup: string): Operation | undefined {
|
|
120
|
+
const operation = session_operation(read, setup)
|
|
121
|
+
|
|
122
|
+
if (operation === undefined) return undefined
|
|
123
|
+
|
|
124
|
+
return operation.at_ms < (read.activity.setup_ms ?? 0) ? undefined : operation
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A stage's icon, keyed by its words before the issue a lane's stage names; a stage the board does not
|
|
128
|
+
// know keeps the icon's columns blank, so its words start where every other line's do.
|
|
129
|
+
function stage_icon(setup: string): string {
|
|
130
|
+
const known = Object.keys(STAGE_ICONS).find(
|
|
131
|
+
(words) => setup === words || setup.startsWith(`${words}${WORD_SEPARATOR}`),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return (known === undefined ? undefined : STAGE_ICONS[known]) ?? KIND_BLANK
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The stage, then what its session last did and when; the stage, since when the run has stood there
|
|
138
|
+
// and why where no session is at work on it, or none could be read.
|
|
139
|
+
function stage_line(read: NowRead, setup: string): TimedLine {
|
|
140
|
+
const operation = stage_operation(read, setup)
|
|
141
|
+
const icon = stage_icon(setup)
|
|
142
|
+
|
|
143
|
+
return operation === undefined
|
|
144
|
+
? { icon, since_ms: read.activity.setup_ms, text: with_reason(setup, read) }
|
|
145
|
+
: { icon, since_ms: operation.at_ms, text: operated(setup, operation) }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// A running child's words, named by its issue.
|
|
149
|
+
function child_said(issue: number, words: string): string {
|
|
150
|
+
return `${issue_cite.plain(issue)}${OPERATION_SEPARATOR}${words}`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// What the running children's newest activity is read from: each session's newest operation and each
|
|
154
|
+
// child's newest event, kept to the children named.
|
|
155
|
+
interface ChildSources {
|
|
156
|
+
operations?: SessionOperations | undefined
|
|
157
|
+
lane_events?: ReadonlyMap<number, LaneEvent> | undefined
|
|
158
|
+
children?: ReadonlySet<number> | undefined
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The running children's newest operations, each named by its issue.
|
|
162
|
+
function operation_lines(read: ChildSources): Array<TimedLine> {
|
|
163
|
+
const lanes = read.operations?.lanes.filter((lane) => read.children?.has(lane.issue)) ?? []
|
|
164
|
+
|
|
165
|
+
return lanes.map(({ issue, operation }) => ({
|
|
166
|
+
icon: NOW_ICONS.operation,
|
|
167
|
+
legend: WORDS.operation,
|
|
168
|
+
since_ms: operation.at_ms,
|
|
169
|
+
text: child_said(issue, run_board_operation.words_of(operation)),
|
|
170
|
+
}))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The running children's newest events, each in its row's phase icon and its own words.
|
|
174
|
+
function event_lines(read: ChildSources): Array<TimedLine> {
|
|
175
|
+
const events = [...(read.lane_events ?? [])]
|
|
176
|
+
|
|
177
|
+
return events
|
|
178
|
+
.filter(([issue]) => read.children?.has(issue))
|
|
179
|
+
.map(([issue, event]) => ({
|
|
180
|
+
icon: PHASE_ICONS[event.phase],
|
|
181
|
+
since_ms: event.at_ms,
|
|
182
|
+
text: child_said(issue, event.words),
|
|
183
|
+
}))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function newest_first(left: TimedLine, right: TimedLine): number {
|
|
187
|
+
return (right.since_ms ?? 0) - (left.since_ms ?? 0)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The newest of the children's operations and events — an operation where the two are as new.
|
|
191
|
+
// `undefined` where neither could be read. A row reads its own child's the same way
|
|
192
|
+
// (joshuafolkken/kit#3769), so the header and the row never disagree on what is newest.
|
|
193
|
+
function newest_of(sources: ChildSources): TimedLine | undefined {
|
|
194
|
+
return [...operation_lines(sources), ...event_lines(sources)].toSorted(newest_first).at(0)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The running children's newest, or `undefined`, and the line counts the children instead.
|
|
198
|
+
function child_line(read: NowRead): TimedLine | undefined {
|
|
199
|
+
const { operations, children, activity } = read
|
|
200
|
+
|
|
201
|
+
return newest_of({ operations, children, lane_events: activity.lane_events })
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function quiet_line(read: NowRead): TimedLine | undefined {
|
|
205
|
+
if (read.running === 0) return watch_line(read)
|
|
206
|
+
|
|
207
|
+
return { icon: STATE_ICONS.running, since_ms: undefined, text: running_of(read.running) }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function timed_line(read: NowRead): TimedLine | undefined {
|
|
211
|
+
const { is_stopped, setup } = read.activity
|
|
212
|
+
|
|
213
|
+
if (is_stopped) return latest_line(read)
|
|
214
|
+
if (setup !== undefined) return stage_line(read, setup)
|
|
215
|
+
|
|
216
|
+
return child_line(read) ?? quiet_line(read)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// The line aged to now, or to an ended run's end: an age counting on after it would read as a hang.
|
|
220
|
+
function line_of(read: NowRead): NowLine | undefined {
|
|
221
|
+
const line = timed_line(read)
|
|
222
|
+
|
|
223
|
+
if (line === undefined) return undefined
|
|
224
|
+
|
|
225
|
+
const { since_ms, ...rest } = line
|
|
226
|
+
const until_ms = read.ended_ms ?? read.now_ms
|
|
227
|
+
|
|
228
|
+
return { ...rest, age_ms: since_ms === undefined ? undefined : until_ms - since_ms }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const run_board_now = { line_of, newest_of }
|
|
232
|
+
|
|
233
|
+
export { run_board_now }
|
|
234
|
+
export type { ChildSources, NowLine, NowRead, TimedLine }
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { time_density_hook } from '#scripts/time-runtime/time-density-hook'
|
|
2
|
+
import { run_board_operation, type Operation } from './run-board-operation'
|
|
3
|
+
|
|
4
|
+
// The disk half of the third line's operation: the end of each session's log, read every time the
|
|
5
|
+
// board re-reads its run. **A fixed, small read**: a dispatch log grows to tens of megabytes and the
|
|
6
|
+
// watcher's log across runs, while the board re-reads every five seconds. The newest call is nearly
|
|
7
|
+
// always inside it; where one line is longer than the read, there is no operation and the line says
|
|
8
|
+
// what it said before. Nothing here may fail — a log that is missing or unreadable is no operation.
|
|
9
|
+
|
|
10
|
+
const TAIL_BYTES = 65_536
|
|
11
|
+
|
|
12
|
+
// A session's log and the checkout its paths are drawn from.
|
|
13
|
+
interface OperationSource {
|
|
14
|
+
log: string | undefined
|
|
15
|
+
root: string | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface LaneSource extends OperationSource {
|
|
19
|
+
issue: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface OperationSources {
|
|
23
|
+
lanes: ReadonlyArray<LaneSource>
|
|
24
|
+
// The parent session's transcript.
|
|
25
|
+
owner: OperationSource
|
|
26
|
+
// The watcher's log, which a judgment session appends to.
|
|
27
|
+
judgment: OperationSource
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface LaneOperation {
|
|
31
|
+
issue: number
|
|
32
|
+
operation: Operation
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The newest operation of every session the run may have at work; which of them is at work now is the
|
|
36
|
+
// reader's to say.
|
|
37
|
+
interface SessionOperations {
|
|
38
|
+
lanes: ReadonlyArray<LaneOperation>
|
|
39
|
+
owner: Operation | undefined
|
|
40
|
+
judgment: Operation | undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type ReadTail = (file_path: string, max_bytes: number) => string
|
|
44
|
+
|
|
45
|
+
function operation_of(source: OperationSource, read_tail: ReadTail): Operation | undefined {
|
|
46
|
+
if (source.log === undefined) return undefined
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
return run_board_operation.latest_of(read_tail(source.log, TAIL_BYTES), source.root)
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function lane_operation(source: LaneSource, read_tail: ReadTail): Array<LaneOperation> {
|
|
56
|
+
const operation = operation_of(source, read_tail)
|
|
57
|
+
|
|
58
|
+
return operation === undefined ? [] : [{ issue: Number(source.issue), operation }]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function read(
|
|
62
|
+
sources: OperationSources,
|
|
63
|
+
read_tail: ReadTail = time_density_hook.read_tail,
|
|
64
|
+
): SessionOperations {
|
|
65
|
+
return {
|
|
66
|
+
lanes: sources.lanes.flatMap((lane) => lane_operation(lane, read_tail)),
|
|
67
|
+
owner: operation_of(sources.owner, read_tail),
|
|
68
|
+
judgment: operation_of(sources.judgment, read_tail),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const run_board_operation_read = { TAIL_BYTES, read }
|
|
73
|
+
|
|
74
|
+
export { run_board_operation_read }
|
|
75
|
+
export type { LaneOperation, OperationSources, SessionOperations }
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
// The newest thing a session did, read off the end of its log — a lane child's dispatch log, the
|
|
6
|
+
// parent's transcript or the watcher's log, all one JSON object a line with the tool calls of an
|
|
7
|
+
// `assistant` line under `message.content`. **Only what a machine decided is kept**: the tool's name
|
|
8
|
+
// and what it was pointed at. The call's `description` is the session's own prose, in the session's
|
|
9
|
+
// language, and the board is written in English.
|
|
10
|
+
|
|
11
|
+
interface Operation {
|
|
12
|
+
// The tool's name: `Bash`, `Edit`, `Read`, …
|
|
13
|
+
kind: string
|
|
14
|
+
// The head of the command, or the file's path; `undefined` for a tool pointed at neither.
|
|
15
|
+
target: string | undefined
|
|
16
|
+
at_ms: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A command is cut here, so a long argument — or a value passed in one — is not drawn.
|
|
20
|
+
const TARGET_LIMIT = 40
|
|
21
|
+
const CUT_MARK = '…'
|
|
22
|
+
const TOOL_USE = 'tool_use'
|
|
23
|
+
// A line that holds no call is never parsed: one can be tens of thousands of characters long.
|
|
24
|
+
const TOOL_USE_MARK = `"${TOOL_USE}"`
|
|
25
|
+
const NEWLINE = '\n'
|
|
26
|
+
// One variable set ahead of the command, with what parts it from the command: it carries a value,
|
|
27
|
+
// never what the command does. The value is read as the shell reads one word — a quoted stretch with
|
|
28
|
+
// its escapes and line breaks, a command run for its output, an escaped character, a plain one.
|
|
29
|
+
const QUOTED = /"(?:[^"\\]|\\.)*"|'[^']*'/su
|
|
30
|
+
const SUBSTITUTED = /\$\((?:[^()]|\([^()]*\))*\)|`[^`]*`/su
|
|
31
|
+
const PLAIN = /\\.|[^\s"'`;&|()\\]/su
|
|
32
|
+
const VALUE = `(?:${QUOTED.source}|${SUBSTITUTED.source}|${PLAIN.source})*`
|
|
33
|
+
const ASSIGNMENT = new RegExp(String.raw`^\w+=${VALUE}(?:\s*(?:&&|;)\s*|\s+|$)`, 'su')
|
|
34
|
+
// A variable still at the head once every readable one is gone: its value could not be read as one.
|
|
35
|
+
const UNREAD_ASSIGNMENT = /^\w+=/u
|
|
36
|
+
const SPACES = /\s+/gu
|
|
37
|
+
// The printable ASCII a target begins with: what follows may be the session's own language.
|
|
38
|
+
const ASCII_HEAD = /^[ -~]*/u
|
|
39
|
+
|
|
40
|
+
// A call names its tool; every other block of a line — its text, its thinking — is not one.
|
|
41
|
+
const CALL_SCHEMA = z.looseObject({
|
|
42
|
+
type: z.literal(TOOL_USE),
|
|
43
|
+
name: z.string(),
|
|
44
|
+
input: z.unknown(),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
function is_dated(text: string): boolean {
|
|
48
|
+
return !Number.isNaN(Date.parse(text))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const MESSAGE_SCHEMA = z.looseObject({ content: z.array(z.unknown()) })
|
|
52
|
+
const LINE_SCHEMA = z.looseObject({
|
|
53
|
+
timestamp: z.string().refine(is_dated),
|
|
54
|
+
message: MESSAGE_SCHEMA,
|
|
55
|
+
})
|
|
56
|
+
// An input that is no object reads as one pointed at nothing.
|
|
57
|
+
const INPUT_SCHEMA = z
|
|
58
|
+
.looseObject({ command: z.string().optional(), file_path: z.string().optional() })
|
|
59
|
+
.catch({})
|
|
60
|
+
|
|
61
|
+
type Call = z.infer<typeof CALL_SCHEMA>
|
|
62
|
+
type Input = z.infer<typeof INPUT_SCHEMA>
|
|
63
|
+
|
|
64
|
+
function ascii_head(text: string): string {
|
|
65
|
+
return (ASCII_HEAD.exec(text)?.[0] ?? '').trim()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cut(text: string): string {
|
|
69
|
+
return text.length > TARGET_LIMIT ? `${text.slice(0, TARGET_LIMIT)}${CUT_MARK}` : text
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The command without every variable set ahead of it, one at a time.
|
|
73
|
+
function without_assignments(command: string): string {
|
|
74
|
+
const rest = command.replace(ASSIGNMENT, '')
|
|
75
|
+
|
|
76
|
+
return rest === command ? command : without_assignments(rest)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The first line of what the command runs, cut to the limit. A command whose variable cannot be read
|
|
80
|
+
// to its end is drawn as nothing: a part of the value would stand where the command should.
|
|
81
|
+
function command_target(command: string): string {
|
|
82
|
+
const [first = ''] = without_assignments(command.trim()).split(NEWLINE)
|
|
83
|
+
if (UNREAD_ASSIGNMENT.test(first)) return ''
|
|
84
|
+
|
|
85
|
+
return cut(ascii_head(first.replaceAll(SPACES, ' ')))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The path from `root` down; a file outside it is drawn by its name alone, never by where it is.
|
|
89
|
+
function path_target(file_path: string, root: string | undefined): string {
|
|
90
|
+
const relative = root === undefined ? undefined : path.relative(root, file_path)
|
|
91
|
+
const is_inside = relative !== undefined && !relative.startsWith('..') && relative !== ''
|
|
92
|
+
|
|
93
|
+
return ascii_head(is_inside ? relative : path.basename(file_path))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function shown(target: string): string | undefined {
|
|
97
|
+
return target === '' ? undefined : target
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function target_of(input: Input, root: string | undefined): string | undefined {
|
|
101
|
+
if (input.command !== undefined) return shown(command_target(input.command))
|
|
102
|
+
|
|
103
|
+
return input.file_path === undefined ? undefined : shown(path_target(input.file_path, root))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function is_call(block: unknown): block is Call {
|
|
107
|
+
return CALL_SCHEMA.safeParse(block).success
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function last_call(content: ReadonlyArray<unknown>): Call | undefined {
|
|
111
|
+
return content.findLast((block) => is_call(block))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The last call a line holds, `undefined` for a line that is cut, is not JSON, or holds none.
|
|
115
|
+
function line_operation(line: string, root: string | undefined): Operation | undefined {
|
|
116
|
+
const parsed = json_value.parse_with(line, LINE_SCHEMA)
|
|
117
|
+
const call = last_call(parsed?.message.content ?? [])
|
|
118
|
+
|
|
119
|
+
if (parsed === undefined || call === undefined) return undefined
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
kind: call.name,
|
|
123
|
+
target: target_of(INPUT_SCHEMA.parse(call.input), root),
|
|
124
|
+
at_ms: Date.parse(parsed.timestamp),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The newest operation in `tail` — the end of a log, whose first line is usually cut mid-way and so
|
|
129
|
+
// reads as no call. `root` is the checkout the session works in, which its paths are drawn from.
|
|
130
|
+
function latest_of(tail: string, root?: string): Operation | undefined {
|
|
131
|
+
const lines = tail.split(NEWLINE).filter((line) => line.includes(TOOL_USE_MARK))
|
|
132
|
+
|
|
133
|
+
for (const line of lines.toReversed()) {
|
|
134
|
+
const operation = line_operation(line, root)
|
|
135
|
+
|
|
136
|
+
if (operation !== undefined) return operation
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The operation as the board writes it: `Bash pnpm josh test:related`, or the tool alone.
|
|
143
|
+
function words_of(operation: Operation): string {
|
|
144
|
+
return operation.target === undefined ? operation.kind : `${operation.kind} ${operation.target}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const run_board_operation = { TARGET_LIMIT, latest_of, words_of }
|
|
148
|
+
|
|
149
|
+
export { run_board_operation }
|
|
150
|
+
export type { Operation }
|
|
@@ -37,6 +37,7 @@ const { STAGE } = run_ship_stage
|
|
|
37
37
|
const LAUNCHED_PHASE: Phase = 'investigate'
|
|
38
38
|
const WORD_SEPARATOR = ' '
|
|
39
39
|
const STAGE_INDEX = 1
|
|
40
|
+
const PHASE_INDEX = 2
|
|
40
41
|
|
|
41
42
|
// `josh ship`'s stages, each its own phase; the preflight is the ship's own start.
|
|
42
43
|
const SHIP_PHASES: Readonly<Partial<Record<string, Phase>>> = {
|
|
@@ -64,6 +65,11 @@ function stage_word_of(event: RunEvent): string {
|
|
|
64
65
|
return event.text.split(WORD_SEPARATOR)[STAGE_INDEX] ?? ''
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// The word after the stage — whether it started, succeeded, failed or was passed over.
|
|
69
|
+
function phase_word_of(event: RunEvent): string {
|
|
70
|
+
return event.text.split(WORD_SEPARATOR)[PHASE_INDEX] ?? ''
|
|
71
|
+
}
|
|
72
|
+
|
|
67
73
|
// A stage marks its phase when it starts; its `done`, and one a resumed ship passed over, mark none.
|
|
68
74
|
function ship_phase_of(event: RunEvent): Phase | undefined {
|
|
69
75
|
if (!run_event_stream.is_stage_start(event)) return undefined
|
|
@@ -79,6 +85,16 @@ function phase_of(event: RunEvent): Phase | undefined {
|
|
|
79
85
|
return KIND_PHASES[event.kind]
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
// The phase a `lane-phase` or `ship-stage` event's stage belongs to, started or not, so a line written
|
|
89
|
+
// from it draws the icon its row's track does — a stage that failed is the ship's failure.
|
|
90
|
+
function stage_phase_of(event: RunEvent): Phase | undefined {
|
|
91
|
+
if (event.kind === KIND.LANE_PHASE) return LANE_PHASES[stage_word_of(event)]
|
|
92
|
+
|
|
93
|
+
if (phase_word_of(event) === run_ship_stage.PHASE.FAILED) return 'failed'
|
|
94
|
+
|
|
95
|
+
return SHIP_PHASES[stage_word_of(event)]
|
|
96
|
+
}
|
|
97
|
+
|
|
82
98
|
// A failed ship keeps only its own start: everything after the newest `ship` folds into `failed`.
|
|
83
99
|
function failed_after(track: ReadonlyArray<Phase>): Array<Phase> {
|
|
84
100
|
const ship = track.lastIndexOf('ship')
|
|
@@ -130,7 +146,16 @@ function replayed(track: ReadonlyArray<Phase>, attempt: ReadonlyArray<Phase>): A
|
|
|
130
146
|
return [...track.slice(0, start), ...(is_covered ? attempt : new Set([...current, ...attempt]))]
|
|
131
147
|
}
|
|
132
148
|
|
|
133
|
-
const run_board_phase = {
|
|
149
|
+
const run_board_phase = {
|
|
150
|
+
LAUNCHED_PHASE,
|
|
151
|
+
PHASES,
|
|
152
|
+
SHIP_PHASES,
|
|
153
|
+
attempt_of,
|
|
154
|
+
history_after,
|
|
155
|
+
phase_of,
|
|
156
|
+
replayed,
|
|
157
|
+
stage_phase_of,
|
|
158
|
+
}
|
|
134
159
|
|
|
135
160
|
export { run_board_phase }
|
|
136
161
|
export type { Phase }
|