@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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { parse } from 'csv-parse/sync'
|
|
2
|
+
import { run_board_render_title } from './run-board-render-title'
|
|
3
|
+
|
|
4
|
+
// The reviewers who helped kit, read off the published roster's CSV. The roster is a spreadsheet anyone
|
|
5
|
+
// with edit access changes, so nothing in it is trusted as drawn: a sheet whose header is not `name,url`
|
|
6
|
+
// is not the roster, a row without a name or with a link that is not `https://` is dropped, and a name
|
|
7
|
+
// loses every control character — an escape in a cell would otherwise be written to the terminal as one.
|
|
8
|
+
|
|
9
|
+
const HEADER = ['name', 'url'] as const
|
|
10
|
+
// Enough for any person's or project's name, short enough that one cell cannot fill the line; a
|
|
11
|
+
// longer one is cut to an ellipsis, as a row's title is.
|
|
12
|
+
const NAME_LIMIT = 40
|
|
13
|
+
const CONTROL = /\p{Cc}/gu
|
|
14
|
+
const SAFE_URL = /^https:\/\/[^\s\p{Cc}]+$/u
|
|
15
|
+
|
|
16
|
+
interface Thanker {
|
|
17
|
+
name: string
|
|
18
|
+
// The page the name opens; `undefined` draws the name unlinked.
|
|
19
|
+
url: string | undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Thanks = ReadonlyArray<Thanker>
|
|
23
|
+
|
|
24
|
+
function name_of(cell: string): string {
|
|
25
|
+
return run_board_render_title.title_of(cell.replaceAll(CONTROL, '').trim(), NAME_LIMIT)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// An empty cell is a name without a link; any other value has to be a whole `https://` URL.
|
|
29
|
+
function url_of(cell: string): string | undefined | false {
|
|
30
|
+
const url = cell.trim()
|
|
31
|
+
|
|
32
|
+
if (url === '') return undefined
|
|
33
|
+
|
|
34
|
+
return SAFE_URL.test(url) && URL.canParse(url) ? url : false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function thanker_of(row: ReadonlyArray<string>): Array<Thanker> {
|
|
38
|
+
const [name_cell = '', url_cell = ''] = row
|
|
39
|
+
const name = name_of(name_cell)
|
|
40
|
+
const url = url_of(url_cell)
|
|
41
|
+
|
|
42
|
+
if (name === '' || url === false) return []
|
|
43
|
+
|
|
44
|
+
return [{ name, url }]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function is_roster(header: ReadonlyArray<string> | undefined): boolean {
|
|
48
|
+
return header?.length === HEADER.length && HEADER.every((cell, index) => header[index] === cell)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function rows_of(csv: string): Array<Array<string>> | undefined {
|
|
52
|
+
try {
|
|
53
|
+
return parse(csv, { relax_column_count: true, skip_empty_lines: true, bom: true })
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// `undefined` for a text that is not the roster, so a failed read is told apart from an empty roster.
|
|
60
|
+
function thanks_of(csv: string): Thanks | undefined {
|
|
61
|
+
const [header, ...rows] = rows_of(csv) ?? []
|
|
62
|
+
|
|
63
|
+
if (!is_roster(header)) return undefined
|
|
64
|
+
|
|
65
|
+
return rows.flatMap((row) => thanker_of(row))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const run_board_thanks_csv = { NAME_LIMIT, thanks_of }
|
|
69
|
+
|
|
70
|
+
export { run_board_thanks_csv }
|
|
71
|
+
export type { Thanker, Thanks }
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
2
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
3
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
4
|
+
import { timed_fetch } from '#scripts/lib/timed-fetch'
|
|
5
|
+
import { run_progress } from '#scripts/run/progress/run-progress'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import { run_board_thanks_csv, type Thanks } from './run-board-thanks-csv'
|
|
8
|
+
|
|
9
|
+
// The roster `run:board` thanks its reviewers from: a Google spreadsheet published as CSV, changed
|
|
10
|
+
// apart from any build or release, so it is read at run time. Its answer is kept in the project's
|
|
11
|
+
// record beside the board's other state and reused for five minutes — Google's own cache holds it as
|
|
12
|
+
// long — so neither a redraw nor a fresh `--once` asks again inside that window. A read that fails
|
|
13
|
+
// keeps the last answer, and with none draws no line; nothing about the failure reaches the screen.
|
|
14
|
+
|
|
15
|
+
const ROSTER_URL =
|
|
16
|
+
'https://docs.google.com/spreadsheets/d/e/2PACX-1vR8Ia38Wiw6Kfyk9Or594J8ZRS8okynV30CpOtq5B6_GyJG7A1-t8Au64jTYvmzgIOlbafcKKgrNFVJ/pub?gid=0&single=true&output=csv'
|
|
17
|
+
const RECORD_PREFIX = 'josh-run-board-thanks-'
|
|
18
|
+
const TTL_MINUTES = 5
|
|
19
|
+
const TTL_MS = TTL_MINUTES * run_progress.MS_PER_MINUTE
|
|
20
|
+
|
|
21
|
+
const thanker_schema = z.object({ name: z.string(), url: z.string().optional() })
|
|
22
|
+
const record_schema = z.object({ checked_ms: z.number(), thanks: z.array(thanker_schema) })
|
|
23
|
+
|
|
24
|
+
interface ThanksRecord {
|
|
25
|
+
checked_ms: number
|
|
26
|
+
thanks: Thanks
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// What the read works through, injected in tests.
|
|
30
|
+
interface ThanksDependencies {
|
|
31
|
+
fetch_text: () => Promise<string>
|
|
32
|
+
record_path: string
|
|
33
|
+
now: () => number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function read_record(record_path: string): ThanksRecord | undefined {
|
|
37
|
+
const raw = stamp_file.read_stamp_text(record_path)
|
|
38
|
+
const parsed = raw === undefined ? undefined : json_value.parse_with(raw, record_schema)
|
|
39
|
+
|
|
40
|
+
if (parsed === undefined) return undefined
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
checked_ms: parsed.checked_ms,
|
|
44
|
+
thanks: parsed.thanks.map(({ name, url }) => ({ name, url })),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A record that cannot be written leaves the next read to ask again — never a reason to fail the board.
|
|
49
|
+
function write_record(record_path: string, record: ThanksRecord): void {
|
|
50
|
+
try {
|
|
51
|
+
stamp_file.replace_stamp(record_path, record)
|
|
52
|
+
} catch {
|
|
53
|
+
// The board draws what it read either way.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fetch_roster(read: () => Promise<string>): Promise<Thanks | undefined> {
|
|
58
|
+
try {
|
|
59
|
+
return run_board_thanks_csv.thanks_of(await read())
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The recorded roster while it is younger than the record's life; `undefined` once it is stale. A
|
|
66
|
+
// record dated after now — the clock stepped back — is stale too, or it would hold until the clock
|
|
67
|
+
// caught up with it.
|
|
68
|
+
function fresh_thanks(record: ThanksRecord | undefined, now_ms: number): Thanks | undefined {
|
|
69
|
+
if (record === undefined) return undefined
|
|
70
|
+
|
|
71
|
+
const age_ms = now_ms - record.checked_ms
|
|
72
|
+
|
|
73
|
+
return age_ms >= 0 && age_ms < TTL_MS ? record.thanks : undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A response that is not a success is a failed read, so its body is never taken for the roster.
|
|
77
|
+
async function fetch_text(): Promise<string> {
|
|
78
|
+
const response = await timed_fetch(ROSTER_URL)
|
|
79
|
+
|
|
80
|
+
if (!response.ok) throw new Error(`roster read answered ${String(response.status)}`)
|
|
81
|
+
|
|
82
|
+
return await response.text()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const LIVE_DEPS: ThanksDependencies = {
|
|
86
|
+
fetch_text,
|
|
87
|
+
record_path: stamp_file.stamp_path(RECORD_PREFIX, PROJECT_ROOT),
|
|
88
|
+
now: () => Date.now(),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function read_with(deps: ThanksDependencies): Promise<Thanks> {
|
|
92
|
+
const now_ms = deps.now()
|
|
93
|
+
const record = read_record(deps.record_path)
|
|
94
|
+
const fresh = fresh_thanks(record, now_ms)
|
|
95
|
+
|
|
96
|
+
if (fresh !== undefined) return fresh
|
|
97
|
+
|
|
98
|
+
const thanks = (await fetch_roster(deps.fetch_text)) ?? record?.thanks ?? []
|
|
99
|
+
|
|
100
|
+
write_record(deps.record_path, { checked_ms: now_ms, thanks })
|
|
101
|
+
|
|
102
|
+
return thanks
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function read_thanks(deps: ThanksDependencies = LIVE_DEPS): Promise<Thanks> {
|
|
106
|
+
return await read_with(deps)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const run_board_thanks_read = { TTL_MS, read_thanks }
|
|
110
|
+
|
|
111
|
+
export { run_board_thanks_read }
|
|
112
|
+
export type { ThanksDependencies }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { run_board_fetch, type Fetch } from './run-board-fetch'
|
|
2
|
+
import { run_board_state, type BoardPorts, type BoardState, type Pace } from './run-board-state'
|
|
3
|
+
import type { Thanks } from './run-board-thanks-csv'
|
|
4
|
+
import { run_board_thanks_read } from './run-board-thanks-read'
|
|
5
|
+
|
|
6
|
+
// The reviewers' roster a `run:board` redraw draws above its legend. Like the GitHub reads
|
|
7
|
+
// (`run-board-github.ts`), **a live board never waits on it**: the read is launched and folded in on
|
|
8
|
+
// the redraw after it lands; one plain frame waits for it. It is launched again no sooner than the
|
|
9
|
+
// roster's own record goes stale. A board with no `read_thanks` port — the chat frame — reads none and
|
|
10
|
+
// draws none.
|
|
11
|
+
|
|
12
|
+
const { TTL_MS } = run_board_thanks_read
|
|
13
|
+
const { is_due } = run_board_state
|
|
14
|
+
|
|
15
|
+
type ThanksState = Pick<BoardState, 'thanks' | 'thanks_fetch' | 'thanks_attempted_ms'>
|
|
16
|
+
|
|
17
|
+
function launch_thanks(state: BoardState, ports: BoardPorts, now_ms: number): BoardState {
|
|
18
|
+
const { read_thanks } = ports
|
|
19
|
+
|
|
20
|
+
if (read_thanks === undefined || state.thanks_fetch !== undefined) return state
|
|
21
|
+
if (!is_due(state.thanks_attempted_ms, TTL_MS, now_ms)) return state
|
|
22
|
+
|
|
23
|
+
const thanks_fetch: Fetch<Thanks> = run_board_fetch.launch(read_thanks, state.thanks)
|
|
24
|
+
|
|
25
|
+
return { ...state, thanks_fetch, thanks_attempted_ms: now_ms }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fold_thanks(state: BoardState): BoardState {
|
|
29
|
+
const answer = state.thanks_fetch?.answer()
|
|
30
|
+
|
|
31
|
+
if (answer === undefined) return state
|
|
32
|
+
|
|
33
|
+
return { ...state, thanks: answer.value, thanks_fetch: undefined }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Folds what landed and launches what is due; at the `settled` pace, waits for it to land.
|
|
37
|
+
async function gather_thanks(
|
|
38
|
+
state: BoardState,
|
|
39
|
+
ports: BoardPorts,
|
|
40
|
+
now_ms: number,
|
|
41
|
+
pace: Pace,
|
|
42
|
+
): Promise<BoardState> {
|
|
43
|
+
const launched = launch_thanks(fold_thanks(state), ports, now_ms)
|
|
44
|
+
|
|
45
|
+
if (pace === 'settled' && launched.thanks_fetch !== undefined) await launched.thanks_fetch.landed
|
|
46
|
+
|
|
47
|
+
return fold_thanks(launched)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const run_board_thanks = { gather_thanks }
|
|
51
|
+
|
|
52
|
+
export { run_board_thanks }
|
|
53
|
+
export type { ThanksState }
|
|
@@ -8,10 +8,12 @@ import { run_board_link } from './run-board-link'
|
|
|
8
8
|
import { run_board_machine } from './run-board-machine'
|
|
9
9
|
import { run_board_notes, type Titles } from './run-board-notes'
|
|
10
10
|
import type { LocalRead } from './run-board-read'
|
|
11
|
+
import { run_board_reason } from './run-board-reason'
|
|
11
12
|
import { run_board_render, type FrameBounds } from './run-board-render'
|
|
12
13
|
import { run_board_spin, type Spun } from './run-board-spin'
|
|
13
14
|
import { run_board_state, type BoardPorts, type BoardState, type Pace } from './run-board-state'
|
|
14
15
|
import { run_board_status, type RunActivity } from './run-board-status'
|
|
16
|
+
import { run_board_thanks } from './run-board-thanks'
|
|
15
17
|
import { run_board_usage } from './run-board-usage'
|
|
16
18
|
|
|
17
19
|
// One `run:board` redraw. **Three speeds**: the frame is redrawn once a second, on the second, so
|
|
@@ -30,6 +32,7 @@ const LOCAL_READ_SECONDS = 5
|
|
|
30
32
|
const LOCAL_READ_MS = LOCAL_READ_SECONDS * run_progress.MS_PER_SECOND
|
|
31
33
|
const { FRESH_STATE, is_due } = run_board_state
|
|
32
34
|
const { PLAN_RETRY_MS, gather_github, statuses_for } = run_board_github
|
|
35
|
+
const { gather_thanks } = run_board_thanks
|
|
33
36
|
|
|
34
37
|
async function reread(state: BoardState, ports: BoardPorts, now_ms: number): Promise<BoardState> {
|
|
35
38
|
if (!is_due(state.read_ms, LOCAL_READ_MS, now_ms)) return state
|
|
@@ -88,17 +91,17 @@ function titles_of(state: BoardState): Titles {
|
|
|
88
91
|
return with_closed(state.plan, state.closed).context.titles
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
run_board_status.statuses_of(local.events, local.lanes, new Set()),
|
|
99
|
-
)
|
|
100
|
-
}
|
|
94
|
+
// A run `run:carry --end` closed and one nobody drives any more are both drawn as stopped.
|
|
95
|
+
function activity_here(local: LocalRead): RunActivity {
|
|
96
|
+
return run_board_status.activity_of(
|
|
97
|
+
local.events,
|
|
98
|
+
local.ended_ms !== undefined || local.is_stranded,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
101
|
|
|
102
|
+
// The plan laid under its statuses, each waiting row saying why it waits.
|
|
103
|
+
function planned_layout(state: BoardState, plan: BoardPlan, local: LocalRead): BoardLayout {
|
|
104
|
+
const { closed } = state
|
|
102
105
|
const read = {
|
|
103
106
|
open_numbers: plan.context.open_numbers,
|
|
104
107
|
read_ms: state.fetched_ms,
|
|
@@ -106,8 +109,25 @@ function layout_for(state: BoardState, local: LocalRead): BoardLayout {
|
|
|
106
109
|
labels: plan.labels,
|
|
107
110
|
}
|
|
108
111
|
const statuses = run_board_status.settle_closed(statuses_for(plan, local), read)
|
|
112
|
+
const reason_of = run_board_reason.reader({
|
|
113
|
+
plan,
|
|
114
|
+
statuses,
|
|
115
|
+
events: local.events,
|
|
116
|
+
free_lanes: local.free_lanes,
|
|
117
|
+
is_stopped: activity_here(local).is_stopped,
|
|
118
|
+
})
|
|
109
119
|
|
|
110
|
-
return run_board_layout.layout_of(with_closed(plan, closed), statuses)
|
|
120
|
+
return run_board_layout.layout_of(with_closed(plan, closed), statuses, reason_of)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The statuses scoped to the run's plan, a running child the plan's listing no longer holds settled.
|
|
124
|
+
// Before the plan is read, the run's own children alone.
|
|
125
|
+
function layout_for(state: BoardState, local: LocalRead): BoardLayout {
|
|
126
|
+
if (state.plan !== undefined) return planned_layout(state, state.plan, local)
|
|
127
|
+
|
|
128
|
+
return run_board_layout.local_layout_of(
|
|
129
|
+
run_board_status.statuses_of(local.events, local.lanes, new Set()),
|
|
130
|
+
)
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
// What one redraw draws with.
|
|
@@ -116,12 +136,20 @@ interface Redraw {
|
|
|
116
136
|
now_ms: number
|
|
117
137
|
}
|
|
118
138
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
// What the header reads off the local read.
|
|
140
|
+
type LocalHeader = Pick<
|
|
141
|
+
BoardHeader,
|
|
142
|
+
'started_ms' | 'ended_ms' | 'activity' | 'operations' | 'usual'
|
|
143
|
+
>
|
|
144
|
+
|
|
145
|
+
function local_header(local: LocalRead): LocalHeader {
|
|
146
|
+
return {
|
|
147
|
+
started_ms: local.started_ms,
|
|
148
|
+
ended_ms: local.ended_ms,
|
|
149
|
+
activity: activity_here(local),
|
|
150
|
+
operations: local.operations,
|
|
151
|
+
usual: local.usual,
|
|
152
|
+
}
|
|
125
153
|
}
|
|
126
154
|
|
|
127
155
|
function header_of(
|
|
@@ -132,9 +160,7 @@ function header_of(
|
|
|
132
160
|
): BoardHeader {
|
|
133
161
|
return {
|
|
134
162
|
now_ms: redraw.now_ms,
|
|
135
|
-
|
|
136
|
-
ended_ms: local.ended_ms,
|
|
137
|
-
activity: activity_here(local),
|
|
163
|
+
...local_header(local),
|
|
138
164
|
layout,
|
|
139
165
|
baseline_total: state.baseline_total,
|
|
140
166
|
plan_fetched_ms: state.fetched_ms,
|
|
@@ -161,6 +187,8 @@ function frame_of(state: BoardState, local: LocalRead, layout: BoardLayout, redr
|
|
|
161
187
|
header: header_of(state, local, layout, redraw),
|
|
162
188
|
notes: run_board_notes.notes_of(local.events, titles_of(state)),
|
|
163
189
|
resume: local.resume,
|
|
190
|
+
thanks: state.thanks,
|
|
191
|
+
link: redraw.ports.link,
|
|
164
192
|
...bounds,
|
|
165
193
|
})
|
|
166
194
|
|
|
@@ -203,8 +231,9 @@ async function tick(
|
|
|
203
231
|
}
|
|
204
232
|
|
|
205
233
|
const gathered = await gather_github(state_for(read, local), { ports, local, now_ms, pace })
|
|
234
|
+
const thanked = await gather_thanks(gathered, ports, now_ms, pace)
|
|
206
235
|
|
|
207
|
-
return draw_run(await resample(
|
|
236
|
+
return draw_run(await resample(thanked, ports, now_ms), local, { ports, now_ms })
|
|
208
237
|
}
|
|
209
238
|
|
|
210
239
|
const run_board_tick = {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { lane_ledger, type LedgerEntry } from '#scripts/lane/lane-ledger'
|
|
2
|
+
import { lane_stage_stats } from '#scripts/lane/lane-stage-stats'
|
|
3
|
+
import { run_board_phase, type Phase } from './run-board-phase'
|
|
4
|
+
|
|
5
|
+
// How long each phase usually takes: the lane ledger's median of the stage the phase is
|
|
6
|
+
// (`lane-stage-stats.ts`), so a running child that has stood at its phase far longer stands out
|
|
7
|
+
// (joshuafolkken/kit#3769). A phase is a `josh ship` stage under the names `run-board-phase.ts` gives
|
|
8
|
+
// them, and `implement` is the ledger's derived one. A phase no stage records — `investigate`, `plan`,
|
|
9
|
+
// `failed` — has no usual time, and is never drawn as long.
|
|
10
|
+
|
|
11
|
+
type UsualTimes = ReadonlyMap<Phase, number>
|
|
12
|
+
|
|
13
|
+
const IMPLEMENT_PHASE: Phase = 'implement'
|
|
14
|
+
|
|
15
|
+
// Each phase's ledger stage, from the stage-to-phase names a row's phase is drawn with. The ledger's
|
|
16
|
+
// `implement` runs from the dispatch, so it also holds the investigation and the plan the row draws
|
|
17
|
+
// before its `implement` phase: the usual time is the longer, which can only draw a phase as long
|
|
18
|
+
// later, never sooner.
|
|
19
|
+
const PHASE_STAGES: ReadonlyArray<readonly [string, Phase | undefined]> = [
|
|
20
|
+
[lane_stage_stats.IMPLEMENT, IMPLEMENT_PHASE],
|
|
21
|
+
...Object.entries(run_board_phase.SHIP_PHASES),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
function usual_of(entries: ReadonlyArray<LedgerEntry>): UsualTimes {
|
|
25
|
+
const medians = lane_stage_stats.medians(entries)
|
|
26
|
+
|
|
27
|
+
return new Map(
|
|
28
|
+
PHASE_STAGES.flatMap(([stage, phase]) => {
|
|
29
|
+
const median = medians.get(stage)
|
|
30
|
+
|
|
31
|
+
return phase === undefined || median === undefined ? [] : [[phase, median] as const]
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The repository's whole ledger, read where the board reads its stream; an absent one has no usual time.
|
|
37
|
+
function read(repository: string): UsualTimes {
|
|
38
|
+
return usual_of(lane_ledger.read_entries(lane_ledger.target_of(repository)))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const run_board_usual = { read, usual_of }
|
|
42
|
+
|
|
43
|
+
export { run_board_usual }
|
|
44
|
+
export type { UsualTimes }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
2
2
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
3
|
+
import { run_headless } from '#scripts/run/run-headless'
|
|
3
4
|
import { run_invocation } from '#scripts/run/run-invocation'
|
|
4
5
|
import { run_carry, type CarryChange, type CarryClaimRequest, type CarryOwner } from './run-carry'
|
|
5
6
|
|
|
@@ -178,6 +179,13 @@ function group_count(values: ParsedValues): number {
|
|
|
178
179
|
return groups.filter(Boolean).length
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
// The mark rides the environment the run launched its session with; the record keeps it beside the
|
|
183
|
+
// owner, so `josh backlogrun --stop` knows that owner for the run's own. An attached owner is left as
|
|
184
|
+
// it is, so its record is the one it always was.
|
|
185
|
+
function marked(owner: CarryOwner): CarryOwner {
|
|
186
|
+
return run_headless.is_headless_parent(process.env) ? { ...owner, is_headless: true } : owner
|
|
187
|
+
}
|
|
188
|
+
|
|
181
189
|
// **`--owner` is what makes liveness answerable at all.** The process that runs this command is a
|
|
182
190
|
// short-lived `tsx`, so its own pid says nothing about whether the run is still going; the caller
|
|
183
191
|
// names the process that is — its own session — and a `backlogrun` passes `--owner "$PPID"`. Absent,
|
|
@@ -192,7 +200,7 @@ function to_owner(value: OptionValue): CarryOwner | undefined {
|
|
|
192
200
|
|
|
193
201
|
const pid = Number(text)
|
|
194
202
|
|
|
195
|
-
return pid < MIN_PID ? undefined : run_carry.owner_of(pid)
|
|
203
|
+
return pid < MIN_PID ? undefined : marked(run_carry.owner_of(pid))
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
// A non-empty `--summary`, or nothing. An empty string is nothing here, so it reads as absent and the
|
|
@@ -6,10 +6,8 @@ import { cost_verdict } from '#scripts/cost-runtime/cost-verdict'
|
|
|
6
6
|
import { run_event_setup } from '#scripts/run/event/run-event-setup'
|
|
7
7
|
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
8
8
|
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
9
|
-
import { run_merge_collect } from '#scripts/run/merge/run-merge-collect'
|
|
10
9
|
import { run_cli_fault } from '#scripts/run/run-cli-fault'
|
|
11
10
|
import { run_headless } from '#scripts/run/run-headless'
|
|
12
|
-
import { run_stop_notify } from '#scripts/run/run-stop-notify'
|
|
13
11
|
import {
|
|
14
12
|
run_carry,
|
|
15
13
|
type CarryClaim,
|
|
@@ -19,8 +17,7 @@ import {
|
|
|
19
17
|
} from './run-carry'
|
|
20
18
|
import { run_carry_args, type CountRequest, type Request } from './run-carry-args'
|
|
21
19
|
import { run_carry_conversation } from './run-carry-conversation'
|
|
22
|
-
import {
|
|
23
|
-
import { run_carry_stash } from './run-carry-stash'
|
|
20
|
+
import { run_carry_end } from './run-carry-end'
|
|
24
21
|
|
|
25
22
|
// `josh run:carry` — the record that carries one invocation's budget across its own session cuts.
|
|
26
23
|
// A `backlogrun` begins it, counts a merge and a filing into it, marks each
|
|
@@ -316,52 +313,12 @@ async function count(
|
|
|
316
313
|
return report_carry(COUNTED_VERDICT, carry, is_json)
|
|
317
314
|
}
|
|
318
315
|
|
|
319
|
-
// **A stop over a record that is still there pushes one ⏸️ confirmation before the record goes**.
|
|
320
|
-
// `plan` is read from the record `--end` is about to remove, so the second `--end` reads `none` and
|
|
321
|
-
// plans nothing — one stop never notifies twice. A clean `--end` passes no reason and stays silent,
|
|
322
|
-
// because a completed run has its own notification. The closed-issue stash report
|
|
323
|
-
// (`run-carry-stash.ts`) is once per invocation on the same read.
|
|
324
|
-
async function report_end(read: CarryRead): Promise<void> {
|
|
325
|
-
if (read.kind === 'none') return
|
|
326
|
-
|
|
327
|
-
await run_carry_stash.report_orphans()
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Best-effort: the board's record of the ended run never stands between `--end` and the carry record it
|
|
331
|
-
// clears, so a failed write goes to stderr rather than leaving the run carried.
|
|
332
|
-
function keep_ended(directory: string, read: CarryRead, stopped: string | undefined): void {
|
|
333
|
-
try {
|
|
334
|
-
run_carry_ended.record_ended(run_carry_ended.ended_path(directory), read, new Date(), stopped)
|
|
335
|
-
} catch (error) {
|
|
336
|
-
console.error(`run:carry: the ended run was not recorded for run:board: ${String(error)}`)
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// The record goes, and the run it held stays readable as the last ended run, so `run:board` keeps the
|
|
341
|
-
// finished run on screen until the next one begins — a stopped one with the session to resume it from.
|
|
342
|
-
function close_record(directory: string, stopped: string | undefined): CarryRead {
|
|
343
|
-
const target = run_carry.carry_path(directory)
|
|
344
|
-
const read = run_carry.read_carry(target)
|
|
345
|
-
|
|
346
|
-
keep_ended(directory, read, stopped)
|
|
347
|
-
run_carry.end_carry(target)
|
|
348
|
-
|
|
349
|
-
return read
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// The merges the run ended with are recorded while its record can still count them.
|
|
353
316
|
async function finish(
|
|
354
317
|
directory: string,
|
|
355
318
|
stopped: string | undefined,
|
|
356
319
|
is_json: boolean,
|
|
357
320
|
): Promise<number> {
|
|
358
|
-
await
|
|
359
|
-
const read = close_record(directory, stopped)
|
|
360
|
-
const notice = run_stop_notify.plan(read, stopped)
|
|
361
|
-
|
|
362
|
-
if (notice !== undefined) await run_stop_notify.announce(notice)
|
|
363
|
-
|
|
364
|
-
await report_end(read)
|
|
321
|
+
const read = await run_carry_end.end_run(directory, { stopped, is_announced: true })
|
|
365
322
|
|
|
366
323
|
if (read.kind === 'none') return report(NONE_VERDICT, undefined, is_json, SUCCESS_EXIT_CODE)
|
|
367
324
|
|
|
@@ -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 }
|
|
@@ -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
|
|
|
@@ -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
|
|