@agent-delivery-harness/cli 0.1.0 → 0.5.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.
Files changed (40) hide show
  1. package/LICENSE +104 -201
  2. package/NOTICE +1 -1
  3. package/package.json +10 -5
  4. package/src/boundary.ts +302 -9
  5. package/src/command-activity.ts +75 -0
  6. package/src/commands/admit.ts +106 -0
  7. package/src/commands/check.ts +13 -2
  8. package/src/commands/emit-review-evidence.ts +38 -0
  9. package/src/commands/emit.ts +419 -0
  10. package/src/commands/gate.ts +127 -18
  11. package/src/commands/maintain.ts +203 -0
  12. package/src/commands/managed.ts +478 -0
  13. package/src/commands/prepare.ts +123 -15
  14. package/src/commands/record.ts +100 -8
  15. package/src/commands/resume.ts +53 -0
  16. package/src/commands/review-context.ts +19 -0
  17. package/src/commands/runs.ts +640 -0
  18. package/src/commands/save-context.ts +51 -0
  19. package/src/commands/submit-evidence.ts +46 -7
  20. package/src/commands/verify.ts +173 -5
  21. package/src/declared-checks.ts +50 -0
  22. package/src/index.ts +40 -4
  23. package/src/main.ts +38 -42
  24. package/src/ordinary-context.ts +99 -0
  25. package/src/provider-rails.ts +24 -0
  26. package/src/record-retention.ts +325 -0
  27. package/src/review-evidence.ts +284 -0
  28. package/src/run-archive-commands.ts +164 -0
  29. package/src/run-archive.ts +127 -0
  30. package/src/run-artifact-commands.ts +163 -0
  31. package/src/run-attachments.ts +112 -0
  32. package/src/run-export.ts +164 -0
  33. package/src/run-live.ts +102 -0
  34. package/src/run-projection.ts +396 -0
  35. package/src/run-server.ts +905 -0
  36. package/src/run-surface.ts +334 -0
  37. package/src/run-view-command.ts +77 -0
  38. package/src/run-view-html.ts +138 -0
  39. package/src/run-view-record.ts +119 -0
  40. package/src/run-view.ts +343 -0
@@ -0,0 +1,640 @@
1
+ import { runViewCommand } from "../run-view-command.ts";
2
+ import { readArchiveFile } from "../run-archive-commands.ts";
3
+ import { runArchiveCommand } from "../run-archive-commands.ts";
4
+ import { runArtifactCommand } from "../run-artifact-commands.ts";
5
+ /**
6
+ * `runs` — read the run store back: `runs list`, `runs show <id>`, and
7
+ * `runs serve`, the local page over the same files.
8
+ *
9
+ * THE VIEWER IS NOT A JUDGE. Everything rendered here is self-attested: an
10
+ * executor wrote most of it, and the executor could have written anything.
11
+ * Every readout says so in as many words, because the failure this surface
12
+ * invites is an operator reading `complete` as though the product had verified
13
+ * something. It has not. The completeness readout is observability, and the
14
+ * viewer supplies no record tree SHA at all, so every rule phrased over a
15
+ * record's candidate is evaluated over any paired round and labeled unbound.
16
+ *
17
+ * EVERY STRING HERE IS HOSTILE UNTIL RENDERED. Rationales, decisions, blocker
18
+ * summaries and gate labels are executor-written free text on their way to a
19
+ * terminal. `oneLine` neutralizes the escape sequences and collapses the
20
+ * whitespace, so a rationale carrying a newline and a plausible-looking
21
+ * completion row renders as one row's worth of text and forges nothing.
22
+ */
23
+ import { stat } from "node:fs/promises";
24
+ import path from "node:path";
25
+ import {
26
+ RUN_EVENT_KINDS,
27
+ RUN_EVENT_KINDS_V1,
28
+ RUN_EVENT_SPEC,
29
+ RUN_EVENT_SPEC_V2,
30
+ RUN_JOURNAL_STATUSES,
31
+ describeRunEventPayload,
32
+ evaluateRunJournal,
33
+ type RunEventPayloadGrammar,
34
+ type RunEventVersion,
35
+ } from "@agent-delivery-harness/kernel";
36
+ import {
37
+ READOUT_LABELS,
38
+ detailOf,
39
+ readoutRows,
40
+ roundRows,
41
+ } from "../run-projection.ts";
42
+ import { buildRunExport } from "../run-export.ts";
43
+ import { startRunServer, type RunServerHandle } from "../run-server.ts";
44
+ import {
45
+ oneLine,
46
+ oneLineOf,
47
+ resolveRunSurface,
48
+ resolveWorktreeRoot,
49
+ runSurfaceBlocker,
50
+ type RunSurface,
51
+ } from "../run-surface.ts";
52
+ import type { CommandResult, ConfigFreeCommandContext, ConfigFreeCommandDescriptor } from "../boundary.ts";
53
+
54
+ const USAGE = [
55
+ "Usage: delivery-harness runs capabilities --json",
56
+ " delivery-harness runs list [--json] [--limit <n>] [--status <status>] [--open|--ended]",
57
+ " delivery-harness runs show [<run-id>] [--json]",
58
+ " delivery-harness runs grammar <kind> [--version 1|2] [--json]",
59
+ " delivery-harness runs view <run-id> [--json] [--record <repository-relative-path>]",
60
+ " delivery-harness runs export <run-id> --output <file>",
61
+ " delivery-harness runs archive <file> [--artifact <id>]",
62
+ " delivery-harness runs capture <run-id> --json <request>",
63
+ " delivery-harness runs artifact <run-id> <artifact-id> [--json]",
64
+ " delivery-harness runs serve [--repo <path>]... [--archive <file>]... [--port <n>] [--freshness-seconds <n>] [--record <repository-relative-path>]",
65
+ ].join("\n");
66
+
67
+ const unresolvable = (reason: string): CommandResult => ({
68
+ kind: "blocked",
69
+ blockers: [
70
+ runSurfaceBlocker({
71
+ code: "run_store_unresolvable",
72
+ summary: "The run store could not be resolved.",
73
+ details: oneLine(reason, 200),
74
+ remediation: {
75
+ id: "run-inside-a-repository",
76
+ summary: "Run this command inside a git repository; the run store lives under its common directory.",
77
+ },
78
+ }),
79
+ ],
80
+ });
81
+
82
+ export const runsCommand: ConfigFreeCommandDescriptor = {
83
+ name: "runs",
84
+ sourceId: "delivery-harness.cli.runs",
85
+ summary: "List, show, and serve the delivery runs this repository has recorded.",
86
+ usage: USAGE,
87
+ configFree: true,
88
+ async run(context: ConfigFreeCommandContext): Promise<CommandResult> {
89
+ const [subcommand, ...rest] = context.args;
90
+ if (subcommand === "capabilities") {
91
+ if (rest.length !== 1 || rest[0] !== "--json") return { kind: "usage", message: "Usage: runs capabilities --json" };
92
+ context.write(`${JSON.stringify({ spec: "run-capabilities/1", writerVersions: ["run-event/1", "run-event/2"], artifactCapture: true })}\n`);
93
+ return { kind: "ok" };
94
+ }
95
+ if (subcommand === "grammar") return showGrammar(context, rest);
96
+ if (subcommand === "view") return runViewCommand(context, rest);
97
+ if (subcommand === "export" || subcommand === "archive") return runArchiveCommand(context, subcommand, rest);
98
+ if (subcommand === "capture" || subcommand === "artifact") return runArtifactCommand(context, subcommand, rest);
99
+ if (subcommand === undefined) return { kind: "usage", message: `runs needs a subcommand.\n${USAGE}` };
100
+ if (subcommand !== "list" && subcommand !== "show" && subcommand !== "serve") {
101
+ return { kind: "usage", message: `Unknown runs subcommand ${oneLine(subcommand, 64)}.\n${USAGE}` };
102
+ }
103
+ // `serve` resolves its OWN repositories — one per `--repo`, none of them
104
+ // necessarily the invoking worktree — so it never asks the invoking
105
+ // worktree's store to resolve first.
106
+ if (subcommand === "serve") return serveRuns(context, rest);
107
+
108
+ // `list`'s argument surface is decided BEFORE the store is resolved. A
109
+ // mistyped bound is a usage error wherever it was typed, and an operator
110
+ // standing outside a repository should not have to fix the repository to
111
+ // find out the flag was wrong.
112
+ const listArgs = subcommand === "list" ? parseListArgs(rest) : undefined;
113
+ if (listArgs !== undefined && !listArgs.ok) return { kind: "usage", message: listArgs.message };
114
+ const showArgs = subcommand === "show" ? parseShowArgs(rest) : undefined;
115
+ if (showArgs !== undefined && !showArgs.ok) return { kind: "usage", message: showArgs.message };
116
+
117
+ const resolved = await resolveRunSurface(context.rootDir);
118
+ if (!resolved.ok) return unresolvable(resolved.reason);
119
+
120
+ if (listArgs !== undefined && listArgs.ok) return listRuns(resolved.surface, context, listArgs.args);
121
+ if (showArgs === undefined || !showArgs.ok) throw new Error("runs show arguments were not parsed");
122
+ let runId = showArgs.args.runId;
123
+ if (runId === undefined) {
124
+ const current = await resolved.surface.store.current(resolved.surface.worktreeKey);
125
+ if (!current.ok || current.runId === undefined) {
126
+ return {
127
+ kind: "blocked",
128
+ blockers: [runSurfaceBlocker({
129
+ code: "run_unresolvable",
130
+ summary: "There is no current run that can be resolved in this worktree.",
131
+ details: current.ok
132
+ ? "no run id was supplied and the worktree has no readable current run"
133
+ : `no run id was supplied; current-run lookup was refused: ${oneLine(current.rejections[0]?.message ?? "unreadable", 200)}`,
134
+ remediation: {
135
+ id: "start-or-name-a-run",
136
+ summary: "Start a run with `delivery-harness emit run.started`, or pass a run id from `delivery-harness runs list`.",
137
+ },
138
+ })],
139
+ };
140
+ }
141
+ runId = current.runId;
142
+ }
143
+ return showRun(resolved.surface, context, runId, showArgs.args.json);
144
+ },
145
+ };
146
+
147
+ interface ShowArgs { readonly runId?: string; readonly json: boolean }
148
+ type ShowParse = { readonly ok: true; readonly args: ShowArgs } | { readonly ok: false; readonly message: string };
149
+
150
+ function parseShowArgs(args: readonly string[]): ShowParse {
151
+ let runId: string | undefined;
152
+ let json = false;
153
+ for (const token of args) {
154
+ if (token === "--json") {
155
+ if (json) return { ok: false, message: `runs show: --json was given twice.\n${USAGE}` };
156
+ json = true;
157
+ continue;
158
+ }
159
+ if (token.startsWith("-")) return { ok: false, message: `runs show: unknown flag ${oneLine(token, 64)}.\n${USAGE}` };
160
+ if (runId !== undefined) return { ok: false, message: `runs show accepts at most one run id.\n${USAGE}` };
161
+ runId = token;
162
+ }
163
+ return { ok: true, args: { json, ...(runId === undefined ? {} : { runId }) } };
164
+ }
165
+
166
+ interface GrammarArgs { readonly kind: string; readonly version: RunEventVersion; readonly json: boolean }
167
+ type GrammarParse = { readonly ok: true; readonly args: GrammarArgs } | { readonly ok: false; readonly message: string };
168
+
169
+ function parseGrammarArgs(args: readonly string[]): GrammarParse {
170
+ let kind: string | undefined;
171
+ let version: RunEventVersion = RUN_EVENT_SPEC_V2;
172
+ let versionSeen = false;
173
+ let json = false;
174
+ for (let index = 0; index < args.length; index += 1) {
175
+ const token = args[index]!;
176
+ if (token === "--json") {
177
+ if (json) return { ok: false, message: `runs grammar: --json was given twice.\n${USAGE}` };
178
+ json = true;
179
+ continue;
180
+ }
181
+ if (token === "--version") {
182
+ if (versionSeen) return { ok: false, message: `runs grammar: --version was given twice.\n${USAGE}` };
183
+ const value = args[index + 1];
184
+ if (value !== "1" && value !== "2") return { ok: false, message: `runs grammar: --version needs 1 or 2.\n${USAGE}` };
185
+ version = value === "1" ? RUN_EVENT_SPEC : RUN_EVENT_SPEC_V2;
186
+ versionSeen = true;
187
+ index += 1;
188
+ continue;
189
+ }
190
+ if (token.startsWith("-")) return { ok: false, message: `runs grammar: unknown flag ${oneLine(token, 64)}.\n${USAGE}` };
191
+ if (kind !== undefined) return { ok: false, message: `runs grammar accepts exactly one event kind.\n${USAGE}` };
192
+ kind = token;
193
+ }
194
+ if (kind === undefined) return { ok: false, message: `runs grammar needs an event kind.\n${USAGE}` };
195
+ return { ok: true, args: { kind, version, json } };
196
+ }
197
+
198
+ function renderGrammar(grammar: RunEventPayloadGrammar, context: ConfigFreeCommandContext): void {
199
+ context.write(`${grammar.kind} (${grammar.version})`);
200
+ for (const member of grammar.members) {
201
+ context.write(` ${member.name} ${member.required ? "required" : "optional"}${member.values === undefined ? "" : ` values: ${member.values.join(", ")}`}`);
202
+ }
203
+ }
204
+
205
+ function showGrammar(context: ConfigFreeCommandContext, args: readonly string[]): CommandResult {
206
+ const parsed = parseGrammarArgs(args);
207
+ if (!parsed.ok) return { kind: "usage", message: parsed.message };
208
+ const grammar = describeRunEventPayload(parsed.args.kind, parsed.args.version);
209
+ if (grammar === undefined) {
210
+ const kinds = parsed.args.version === RUN_EVENT_SPEC ? RUN_EVENT_KINDS_V1 : RUN_EVENT_KINDS;
211
+ return {
212
+ kind: "usage",
213
+ message: `runs grammar: ${oneLine(parsed.args.kind, 128)} is not a ${parsed.args.version} event kind; accepted kinds: ${kinds.join(", ")}.\n${USAGE}`,
214
+ };
215
+ }
216
+ if (parsed.args.json) context.write(JSON.stringify(grammar, null, 2));
217
+ else renderGrammar(grammar, context);
218
+ return { kind: "ok" };
219
+ }
220
+
221
+ // ── list ─────────────────────────────────────────────────────────────────────
222
+
223
+ /**
224
+ * The one label this listing prints for a journal it could not read at all.
225
+ * It is not a completeness verdict — nothing was evaluated — so it is not one
226
+ * of the kernel's statuses, and it is spelled once, here.
227
+ */
228
+ export const RUN_LIST_UNREADABLE = "unreadable";
229
+
230
+ /**
231
+ * `absent` is the completeness vocabulary's answer to "no journal bound this
232
+ * candidate", which an inventory of the journals that exist can never be. It
233
+ * is excluded rather than accepted-and-never-matched, because a selector that
234
+ * cannot select anything whatever the store holds is a false affordance: an
235
+ * agent filtering on it would read the empty result as "no such runs" rather
236
+ * than as "this question cannot be asked here".
237
+ */
238
+ const STATUS_NEVER_LISTED = "absent";
239
+
240
+ /**
241
+ * Exactly the statuses a row of this listing can carry, which is what
242
+ * `--status` accepts. Derived from the kernel's closed vocabulary so a status
243
+ * renamed there is a compile-and-test problem here rather than a filter that
244
+ * silently stops matching.
245
+ */
246
+ export const RUN_LIST_STATUSES: readonly string[] = Object.freeze([
247
+ ...RUN_JOURNAL_STATUSES.filter((status) => status !== STATUS_NEVER_LISTED),
248
+ RUN_LIST_UNREADABLE,
249
+ ]);
250
+
251
+ /** The spec every machine-readable inventory carries, beside `runs show --json`'s export spec. */
252
+ export const RUN_INVENTORY_SPEC = "run-inventory/1";
253
+
254
+ interface ListArgs {
255
+ readonly json: boolean;
256
+ readonly limit?: number;
257
+ readonly status?: string;
258
+ /** True selects the runs with no `run.ended`; false selects the ended ones. */
259
+ readonly open?: boolean;
260
+ }
261
+
262
+ type ListParse = { readonly ok: true; readonly args: ListArgs } | { readonly ok: false; readonly message: string };
263
+
264
+ /**
265
+ * The separate-argument form the rest of this command uses, and the same
266
+ * refusal discipline: an unknown flag, a repeated one, a positional, and a
267
+ * value that is not a bound are all usage errors rather than conveniences.
268
+ *
269
+ * A LIMIT IS A COUNT OF ROWS, so it is a positive integer and nothing else.
270
+ * `0` is refused rather than read as "no rows" or as "no bound": both readings
271
+ * are plausible, an operator cannot tell which one they got from the output,
272
+ * and neither is a thing anyone means to ask for.
273
+ */
274
+ function parseListArgs(args: readonly string[]): ListParse {
275
+ const refuse = (reason: string): ListParse => ({ ok: false, message: `runs list: ${reason}.\n${USAGE}` });
276
+ let json = false;
277
+ let limit: number | undefined;
278
+ let status: string | undefined;
279
+ let open: boolean | undefined;
280
+
281
+ for (let index = 0; index < args.length; index += 1) {
282
+ const token = args[index]!;
283
+ if (token === "--json") {
284
+ if (json) return refuse("--json was given twice");
285
+ json = true;
286
+ continue;
287
+ }
288
+ if (token === "--open" || token === "--ended") {
289
+ // Two names for one question, so asking it twice — however spelled —
290
+ // has no answer to give.
291
+ if (open !== undefined) return refuse("use at most one of --open and --ended");
292
+ open = token === "--open";
293
+ continue;
294
+ }
295
+ if (token === "--limit" || token === "--status") {
296
+ const value = args[index + 1];
297
+ if (value === undefined) return refuse(`${token} needs a value`);
298
+ index += 1;
299
+ if (token === "--limit") {
300
+ if (limit !== undefined) return refuse("--limit was given twice");
301
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1) {
302
+ return refuse(`--limit needs a positive whole number, not ${oneLine(value, 64)}`);
303
+ }
304
+ limit = Number(value);
305
+ continue;
306
+ }
307
+ if (status !== undefined) return refuse("--status was given twice");
308
+ if (!RUN_LIST_STATUSES.includes(value)) {
309
+ return refuse(`--status accepts ${RUN_LIST_STATUSES.join(", ")}, not ${oneLine(value, 64)}`);
310
+ }
311
+ status = value;
312
+ continue;
313
+ }
314
+ if (token.startsWith("--")) return refuse(`unknown flag ${oneLine(token, 64)}`);
315
+ return refuse("it takes no positional arguments");
316
+ }
317
+
318
+ return {
319
+ ok: true,
320
+ args: { json, ...(limit === undefined ? {} : { limit }), ...(status === undefined ? {} : { status }), ...(open === undefined ? {} : { open }) },
321
+ };
322
+ }
323
+
324
+ /** The journal's size on disk, or zero where it cannot be measured. */
325
+ async function sizeOf(runsDir: string, runId: string): Promise<number> {
326
+ try {
327
+ return (await stat(path.join(runsDir, `${runId}.jsonl`))).size;
328
+ } catch {
329
+ return 0;
330
+ }
331
+ }
332
+
333
+ /** One run as both surfaces see it: the same five facts, rendered or serialized. */
334
+ interface InventoryRow {
335
+ readonly runId: string;
336
+ readonly status: string;
337
+ readonly open: boolean;
338
+ readonly current: boolean;
339
+ readonly bytes: number;
340
+ }
341
+
342
+ /**
343
+ * Every run the store holds, in the store's own order.
344
+ *
345
+ * THE ORDER IS THE STORE'S, and the store sorts run ids ascending. That is
346
+ * what makes a bounded listing reproducible: the same store bounded the same
347
+ * way returns the same rows, and the row a `--limit 1` returns is the one an
348
+ * unbounded listing printed first. Nothing here re-orders it, so the order
349
+ * this surface documents is the order the store defines and not a second one
350
+ * that happens to agree today.
351
+ */
352
+ async function inventoryOf(
353
+ surface: RunSurface,
354
+ ): Promise<{ readonly rows: readonly InventoryRow[]; readonly currentRunId: string | undefined }> {
355
+ const runIds = await surface.store.list();
356
+ const current = await surface.store.current(surface.worktreeKey);
357
+ const currentRunId = current.ok ? current.runId : undefined;
358
+
359
+ const rows: InventoryRow[] = [];
360
+ for (const runId of runIds) {
361
+ const bytes = await sizeOf(surface.runsDir, runId);
362
+ const read = await surface.store.read(runId);
363
+ rows.push(
364
+ read.ok
365
+ ? {
366
+ runId,
367
+ status: evaluateRunJournal(read.events).status,
368
+ open: !read.events.some((event) => event.kind === "run.ended"),
369
+ current: runId === currentRunId,
370
+ bytes,
371
+ }
372
+ : // Nothing was read, so no lifecycle or completeness state is claimed.
373
+ // The worktree pointer and on-disk size are independent readable facts.
374
+ { runId, status: RUN_LIST_UNREADABLE, open: false, current: runId === currentRunId, bytes },
375
+ );
376
+ }
377
+ return { rows, currentRunId };
378
+ }
379
+
380
+ /**
381
+ * `runs list`, human and machine.
382
+ *
383
+ * THE FILTER RUNS BEFORE THE BOUND. A bound applied first would spend its rows
384
+ * on runs the filter then discards, so `--status x --limit 1` could answer
385
+ * "none" for a store that holds one — the bound is a bound on the ANSWER, not
386
+ * on how far the store was read.
387
+ *
388
+ * WHAT `total` COUNTS is the set the filters selected, before the bound. That
389
+ * is the number a reader needs to know it was bounded (`returned` below it
390
+ * means rows were cut), and with no filter it is the whole store — which is
391
+ * why the unfiltered human listing's total line is unchanged.
392
+ */
393
+ async function listRuns(surface: RunSurface, context: ConfigFreeCommandContext, args: ListArgs): Promise<CommandResult> {
394
+ const { rows, currentRunId } = await inventoryOf(surface);
395
+ const selected = rows.filter(
396
+ (row) =>
397
+ (args.status === undefined || row.status === args.status) &&
398
+ (args.open === undefined || (row.status !== RUN_LIST_UNREADABLE && row.open === args.open)),
399
+ );
400
+ const totalBytes = selected.reduce((sum, row) => sum + row.bytes, 0);
401
+ const shown = args.limit === undefined ? selected : selected.slice(0, args.limit);
402
+ const truncated = shown.length < selected.length;
403
+
404
+ if (args.json) {
405
+ context.write(
406
+ JSON.stringify(
407
+ {
408
+ spec: RUN_INVENTORY_SPEC,
409
+ labels: READOUT_LABELS,
410
+ runsDir: surface.runsDir,
411
+ current: currentRunId ?? null,
412
+ runs: shown,
413
+ total: { count: selected.length, bytes: totalBytes },
414
+ returned: shown.length,
415
+ truncated,
416
+ },
417
+ null,
418
+ 2,
419
+ ),
420
+ );
421
+ return { kind: "ok" };
422
+ }
423
+
424
+ // The status column carries a completeness verdict, so this readout is
425
+ // labeled exactly like `show`'s. `list` is the command an operator reaches
426
+ // for first, before it knows which id to show; an unlabeled `complete` here
427
+ // is the misreading the labels exist to prevent.
428
+ const lines: string[] = [`runs in ${oneLine(surface.runsDir, 400)}`, ` (${READOUT_LABELS})`];
429
+ for (const row of shown) {
430
+ lines.push(
431
+ row.status === RUN_LIST_UNREADABLE
432
+ ? ` ${row.runId} ${RUN_LIST_UNREADABLE} ${row.bytes} bytes`
433
+ : ` ${row.runId} ${row.status} ${row.open ? "open" : "ended"}${row.current ? " current" : ""} ${row.bytes} bytes`,
434
+ );
435
+ }
436
+ lines.push(`total ${totalBytes} bytes across ${selected.length} run(s)`);
437
+ // Said only when something was actually cut, so the unbounded listing keeps
438
+ // the bytes it always had.
439
+ if (truncated) lines.push(`showing ${shown.length} of ${selected.length} run(s) (--limit ${args.limit})`);
440
+ for (const line of lines) context.write(line);
441
+ return { kind: "ok" };
442
+ }
443
+
444
+ // ── show ─────────────────────────────────────────────────────────────────────
445
+
446
+ async function showRun(surface: RunSurface, context: ConfigFreeCommandContext, runId: string, json: boolean): Promise<CommandResult> {
447
+ const read = await surface.store.read(runId);
448
+ if (!read.ok) {
449
+ return {
450
+ kind: "blocked",
451
+ blockers: [
452
+ runSurfaceBlocker({
453
+ code: "run_unresolvable",
454
+ summary: "That run has no readable journal in this store.",
455
+ details: `run ${oneLine(runId, 128)}: ${oneLine(read.rejections[0]?.message ?? "unreadable", 200)}`,
456
+ remediation: { id: "list-the-runs", summary: "Run `delivery-harness runs list` to see the runs this repository holds." },
457
+ }),
458
+ ],
459
+ };
460
+ }
461
+
462
+ const events = read.events;
463
+ if (json) {
464
+ const worktreeRoot = await resolveWorktreeRoot(context.rootDir);
465
+ const rootDir = worktreeRoot.ok ? worktreeRoot.root : context.rootDir;
466
+ context.write(JSON.stringify(buildRunExport({
467
+ runId,
468
+ events,
469
+ rootDir,
470
+ refusedAppends: await surface.store.readNotes(runId),
471
+ }), null, 2));
472
+ return { kind: "ok" };
473
+ }
474
+ const open = !events.some((event) => event.kind === "run.ended");
475
+ const current = await surface.store.current(surface.worktreeKey);
476
+ const isCurrent = current.ok && current.runId === runId;
477
+
478
+ context.write(`run ${runId} ${open ? "open" : "ended"}${isCurrent ? " current in this worktree" : ""}`);
479
+ context.write(" events:");
480
+ for (const event of events) {
481
+ context.write(` ${event.seq} ${event.at} ${event.kind.padEnd(20)} ${event.actor.role.padEnd(8)} ${detailOf(event)}`);
482
+ }
483
+
484
+ const rounds = roundRows(events);
485
+ if (rounds.length > 0) {
486
+ context.write(" rounds:");
487
+ for (const row of rounds) context.write(` ${row}`);
488
+ }
489
+
490
+ const decisions = events.filter((event) => event.kind === "decision.recorded");
491
+ if (decisions.length > 0) {
492
+ context.write(" decisions:");
493
+ for (const decision of decisions) context.write(` ${detailOf(decision)}`);
494
+ }
495
+
496
+ const notes = await surface.store.readNotes(runId);
497
+ if (notes.length > 0) {
498
+ context.write(" refused appends:");
499
+ for (const entry of notes) {
500
+ const note = (typeof entry === "object" && entry !== null ? entry : {}) as Record<string, unknown>;
501
+ context.write(
502
+ ` ${oneLineOf(note["at"], 32)} ${oneLineOf(note["kind"], 128)} ${oneLineOf(note["code"], 64)}${note["pattern"] === undefined ? "" : ` ${oneLineOf(note["pattern"], 64)}`}`,
503
+ );
504
+ }
505
+ }
506
+
507
+ // No record tree sha and no mandated pair: the viewer has neither, and
508
+ // pretending otherwise would turn an observation into a claim.
509
+ //
510
+ // THE ROOT IS THE WORKTREE'S, NOT THE INVOCATION'S. The config-presence note
511
+ // is a question about the repository, and `runs serve` answers it at the
512
+ // toplevel of the path it was given. Answering it here at the cwd would tell
513
+ // an operator standing in `packages/cli` that this repository carries no
514
+ // config while the page, over the same journal, says it carries one — two
515
+ // answers to one question, which is the thing the shared projection exists
516
+ // to prevent. The cwd stands in only where git can name no root at all, and
517
+ // then it is the only root there is.
518
+ const worktreeRoot = await resolveWorktreeRoot(context.rootDir);
519
+ const rootDir = worktreeRoot.ok ? worktreeRoot.root : context.rootDir;
520
+ for (const row of readoutRows(events, evaluateRunJournal(events), rootDir)) context.write(row);
521
+ return { kind: "ok" };
522
+ }
523
+
524
+ // ── serve ────────────────────────────────────────────────────────────────────
525
+
526
+ interface ServeArgs {
527
+ readonly archives:readonly string[];
528
+ readonly recordPath?:string;
529
+ readonly freshnessWindowMs?:number;
530
+ readonly repos: readonly string[];
531
+ readonly port?: number;
532
+ }
533
+
534
+ type ServeParse = { readonly ok: true; readonly args: ServeArgs } | { readonly ok: false; readonly message: string };
535
+
536
+ /**
537
+ * The ports a browser leaves out of the `Host` header, being the defaults of
538
+ * the schemes it would dial them under.
539
+ *
540
+ * `run-server.ts` decides whether a request is addressed to the socket it
541
+ * arrived on by comparing `Host` against the bound `host:port` for EXACT
542
+ * equality — it parses nothing, normalizes nothing, and matches no list of
543
+ * names that "mean" loopback. That exactness is the property; it is what makes
544
+ * the page's answer to a DNS rebind checkable rather than a matter of parsing
545
+ * taste. A page served on one of these ports is one no browser can ever reach:
546
+ * the request arrives carrying `Host: 127.0.0.1`, with no port at all, and is
547
+ * answered 403 — the same 403 a rebind gets, with nothing to tell them apart.
548
+ *
549
+ * So the choice is between widening the check and refusing the port, and it is
550
+ * settled by what each costs. Widening costs the property. Refusing costs an
551
+ * operator nothing they wanted: a loopback viewer of a run store has no reason
552
+ * to sit on a privileged port, and the ephemeral default is one flag away.
553
+ * The refusal lives HERE, at the one surface an operator types a port at,
554
+ * rather than beside the check it protects — a second guard inside the server
555
+ * would make neither one provable.
556
+ */
557
+ const BROWSER_ELIDED_PORTS: readonly number[] = [80, 443];
558
+
559
+ /**
560
+ * The separate-argument form every other command uses. `--flag=value` is
561
+ * REFUSED rather than accepted as a convenience: one spelling means an
562
+ * operator who mistypes a path gets a usage error instead of a server quietly
563
+ * watching a repository named `--repo=/some/path`.
564
+ */
565
+ function parseServeArgs(args: readonly string[], rootDir: string): ServeParse {
566
+ const repos: string[] = [];
567
+ const archives:string[]=[];
568
+ let recordPath:string|undefined;
569
+ let freshnessWindowMs:number|undefined;
570
+ let port: number | undefined;
571
+
572
+ for (let index = 0; index < args.length; index += 1) {
573
+ const token = args[index]!;
574
+ if (token === "--repo" || token === "--port" || token === "--archive" || token === "--freshness-seconds" || token === "--record") {
575
+ const value = args[index + 1];
576
+ if (value === undefined) return { ok: false, message: `${token} needs a value.\n${USAGE}` };
577
+ index += 1;
578
+ if(token==="--record"){if(recordPath!==undefined)return {ok:false,message:"Use one explicit record path"};recordPath=value;continue;}
579
+ if(token==="--archive"){archives.push(path.resolve(rootDir,value));continue;}
580
+ if(token==="--freshness-seconds"){if(!/^\d+$/.test(value)||!Number.isSafeInteger(Number(value))||Number(value)>86400)return {ok:false,message:"freshness seconds must be between 0 and 86400"};freshnessWindowMs=Number(value)*1000;continue;}
581
+ if (token === "--repo") {
582
+ repos.push(path.resolve(rootDir, value));
583
+ continue;
584
+ }
585
+ if (!/^\d{1,5}$/.test(value)) return { ok: false, message: `--port needs a port number.\n${USAGE}` };
586
+ const parsed = Number(value);
587
+ if (parsed > 65535) return { ok: false, message: `--port needs a port number.\n${USAGE}` };
588
+ if (BROWSER_ELIDED_PORTS.includes(parsed)) {
589
+ return {
590
+ ok: false,
591
+ message:
592
+ `--port ${parsed} cannot be served: a browser omits a scheme's default port from the Host header, ` +
593
+ `and this page answers only to the exact host:port it bound.\n${USAGE}`,
594
+ };
595
+ }
596
+ port = parsed;
597
+ continue;
598
+ }
599
+ if (token.startsWith("--")) return { ok: false, message: `Unknown flag ${oneLine(token, 64)}.\n${USAGE}` };
600
+ return { ok: false, message: `runs serve takes no positional arguments.\n${USAGE}` };
601
+ }
602
+
603
+ // No `--repo` means the worktree the operator is standing in, which is the
604
+ // only repository they can have meant.
605
+ return { ok: true, args: { repos: repos.length === 0 && archives.length===0 ? [rootDir] : repos, archives,...(recordPath===undefined?{}:{recordPath}),...(freshnessWindowMs===undefined?{}:{freshnessWindowMs}), ...(port === undefined ? {} : { port }) } };
606
+ }
607
+
608
+ /**
609
+ * Serves until the invocation is signalled.
610
+ *
611
+ * There is no other exit. A viewer's job is to be there while the operator
612
+ * watches, and the operator ends it with the interrupt the boundary already
613
+ * maps; a run ending is not a reason to stop serving, because the next run
614
+ * starts in the same store.
615
+ */
616
+ async function serveRuns(context: ConfigFreeCommandContext, args: readonly string[]): Promise<CommandResult> {
617
+ const parsed = parseServeArgs(args, context.rootDir);
618
+ if (!parsed.ok) return { kind: "usage", message: parsed.message };
619
+
620
+ let archives:{label:string;text:string}[];try{archives=await Promise.all(parsed.args.archives.map(async file=>({label:path.basename(file),text:await readArchiveFile(file)})));}catch{return unresolvable("An explicitly selected archive could not be read within its size limit.");}
621
+ const started = await startRunServer({ repos: parsed.args.repos,archives,...(parsed.args.recordPath===undefined?{}:{recordPath:parsed.args.recordPath}),...(parsed.args.freshnessWindowMs===undefined?{}:{freshnessWindowMs:parsed.args.freshnessWindowMs}), ...(parsed.args.port === undefined ? {} : { port: parsed.args.port }) });
622
+ if (!started.ok) return unresolvable(started.reason);
623
+
624
+ const server: RunServerHandle = started.server;
625
+ context.write(`serving ${parsed.args.repos.length} repository path(s) at ${server.url}`);
626
+ context.write(` (${READOUT_LABELS})`);
627
+ try {
628
+ await untilSignalled(context.signal);
629
+ } finally {
630
+ await server.close();
631
+ }
632
+ return { kind: "ok" };
633
+ }
634
+
635
+ /** Resolves when the invocation's signal aborts; never, when it has none. */
636
+ function untilSignalled(signal: AbortSignal | undefined): Promise<void> {
637
+ if (signal === undefined) return new Promise<void>(() => {});
638
+ if (signal.aborted) return Promise.resolve();
639
+ return new Promise<void>((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
640
+ }