@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,334 @@
1
+ /**
2
+ * What `emit`, `runs`, and the boundary wrap share: how the run store is
3
+ * found, how an event envelope is built, and how a store-derived string is
4
+ * made safe to print.
5
+ *
6
+ * THE ONE STORE OVERRIDE. `DELIVERY_HARNESS_RUN_STORE` names an absolute
7
+ * directory to resolve the store under instead of the repository's git common
8
+ * directory, read HERE — at resolution time, on every CLI path that reaches a
9
+ * run store — so no path can be honouring it while another is not. It exists
10
+ * because a process that is not a delivery can invoke this CLI from a worktree
11
+ * that has a delivery run current: the repository's own test suite does,
12
+ * hundreds of times per `npm run check`, and every wrapped invocation appended
13
+ * a `command.completed` to whatever run the executor had open there. Such a
14
+ * process pins the override at a directory of its own and is thereby unable to
15
+ * reach any live store. An invocation with the override unset writes where it
16
+ * always did; an invocation under test with it unset is a defect in the test,
17
+ * not a behaviour of the product, so nothing here tries to detect a test
18
+ * runner.
19
+ *
20
+ * WHAT THE OVERRIDE DOES NOT COLLAPSE. Each repository keeps its own store
21
+ * under the named directory, at a subdirectory digested from its common
22
+ * directory, so the two rules the surface documents survive being overridden:
23
+ * two paths in different repositories are two stores, and two worktrees of one
24
+ * repository are one store. `repo.commonDir` on every event still names the
25
+ * repository, never the override — the event says where the run happened, not
26
+ * where its journal was filed.
27
+ *
28
+ * WHY THE STORE IS RESOLVED HERE AND NOT WIRED. The record store is wired from
29
+ * the config, because a delivery record belongs to a configured gate. A run
30
+ * belongs to the *repository* — it outlives the worktree it ran in and it
31
+ * exists in repositories that have no harness config at all. So the run store
32
+ * is resolved from git and nothing else, through the kernel's namespace
33
+ * resolver, with a direct runner and the `GIT_` namespace cleared: an
34
+ * inherited `GIT_DIR` must never relocate the store into someone else's
35
+ * repository.
36
+ *
37
+ * WHY EVERY PRINTED STRING GOES THROUGH `oneLine`. A journal carries
38
+ * executor-written free text — rationales, decisions, blocker summaries. It is
39
+ * rendered to a terminal in a row-per-event table, so an escape sequence could
40
+ * repaint the screen and an embedded newline could forge a row that looks
41
+ * exactly like a CLI-written completion. Neutralizing strips the sequences;
42
+ * collapsing the whitespace is what removes the second attack, and it has to
43
+ * happen on every single-line field, not just the ones that look risky.
44
+ */
45
+ import { lstatSync } from "node:fs";
46
+ import path from "node:path";
47
+ import {
48
+ createBlocker,
49
+ createRunStore,
50
+ sha256Hex,
51
+ evaluateRunJournal,
52
+ gitNamespaceClearedEnvironment,
53
+ neutralizeForDisplay,
54
+ resolveRunStoreLocation,
55
+ runGitDirect,
56
+ type Blocker,
57
+ type RunEventInput,
58
+ type RunEventKind,
59
+ type RunJournalRow,
60
+ type RunStore,
61
+ } from "@agent-delivery-harness/kernel";
62
+
63
+ /** The config-free commands' own source id family. */
64
+ export const RUN_SURFACE_SOURCE = "delivery-harness.cli.run-surface";
65
+
66
+ export interface RunSurface {
67
+ readonly store: RunStore;
68
+ /** The repository's OWN git common directory, which is what an event names. */
69
+ readonly commonDir: string;
70
+ /** The store's `runs` directory, under the override when one is named. */
71
+ readonly runsDir: string;
72
+ /** The invoking worktree's pointer key — the identity of "the current run" here. */
73
+ readonly worktreeKey: string;
74
+ }
75
+
76
+ export type RunSurfaceResolution =
77
+ | { readonly ok: true; readonly surface: RunSurface }
78
+ | { readonly ok: false; readonly reason: string };
79
+
80
+ /** The environment variable that relocates the run store. See this file's header. */
81
+ export const RUN_STORE_OVERRIDE = "DELIVERY_HARNESS_RUN_STORE";
82
+
83
+ /**
84
+ * The directory the store is rooted at for a repository whose common directory
85
+ * is `commonDir`.
86
+ *
87
+ * Blank is unset, following this repository's other environment levers. A
88
+ * value that is set but not absolute FAILS CLOSED — the resolution is refused
89
+ * rather than falling back to the repository's own store, because a caller
90
+ * that mis-spells its override is exactly the caller that must not reach a
91
+ * live store by accident.
92
+ */
93
+ function storeRootFor(
94
+ commonDir: string,
95
+ override: string | undefined,
96
+ ): { readonly ok: true; readonly root: string } | { readonly ok: false; readonly reason: string } {
97
+ const named = override?.trim() ?? "";
98
+ if (named.length === 0) return { ok: true, root: commonDir };
99
+ if (!path.isAbsolute(named)) {
100
+ return { ok: false, reason: `${RUN_STORE_OVERRIDE} must name an absolute directory` };
101
+ }
102
+ return { ok: true, root: path.join(named, sha256Hex(commonDir)) };
103
+ }
104
+
105
+ /**
106
+ * Resolves the run store for the invoking worktree. Never throws: a config-free
107
+ * command turns a failure into a typed blocker, and the boundary wrap turns it
108
+ * into silence.
109
+ *
110
+ * The override is read here, on this one path, because this is the one path
111
+ * every CLI reach for a run store goes through: `emit`, `runs`, `runs serve`,
112
+ * the boundary wrap's automatic completion, and `verify`'s journal row.
113
+ */
114
+ export async function resolveRunSurface(cwd: string): Promise<RunSurfaceResolution> {
115
+ const location = await resolveRunStoreLocation({
116
+ cwd,
117
+ run: runGitDirect,
118
+ env: gitNamespaceClearedEnvironment(),
119
+ });
120
+ if (!location.ok) return { ok: false, reason: location.reason };
121
+ const rooted = storeRootFor(location.commonDir, process.env[RUN_STORE_OVERRIDE]);
122
+ if (!rooted.ok) return { ok: false, reason: rooted.reason };
123
+ const store = createRunStore(rooted.root);
124
+ return {
125
+ ok: true,
126
+ surface: {
127
+ store,
128
+ commonDir: location.commonDir,
129
+ runsDir: store.runsDir,
130
+ worktreeKey: location.worktreeKey,
131
+ },
132
+ };
133
+ }
134
+
135
+ /**
136
+ * The worktree ROOT of a path, for the one thing the run surface needs it for:
137
+ * the config-presence note names a root, and BOTH surfaces resolve it here.
138
+ * `runs serve` renders a root the operator named rather than the one it was
139
+ * invoked in; `runs show` renders the root of the worktree it was invoked in
140
+ * rather than the directory, which may be any depth below it. One question
141
+ * about one repository has to have one answer whichever surface asks it.
142
+ *
143
+ * `--show-toplevel` is a plumbing query that reads no index and runs no hook,
144
+ * alias, or pager, and it runs here with the `GIT_` namespace cleared for the
145
+ * same reason the store resolution does: a `GIT_DIR` inherited from the
146
+ * operator's shell must not decide which repository a `--repo` path names.
147
+ */
148
+ export async function resolveWorktreeRoot(
149
+ cwd: string,
150
+ ): Promise<{ readonly ok: true; readonly root: string } | { readonly ok: false; readonly reason: string }> {
151
+ const outcome = await runGitDirect({
152
+ cwd,
153
+ args: ["rev-parse", "--path-format=absolute", "--show-toplevel"],
154
+ env: gitNamespaceClearedEnvironment(),
155
+ });
156
+ const root = outcome.stdout.trim();
157
+ if (outcome.code !== 0 || root.length === 0) return { ok: false, reason: `not a git worktree: ${cwd}` };
158
+ return { ok: true, root };
159
+ }
160
+
161
+ /**
162
+ * The writing process's own instant, at the contract's second granularity.
163
+ * `at` is never settable through an argument surface: whoever writes the event
164
+ * is whoever is holding the clock.
165
+ */
166
+ export function runInstant(): string {
167
+ return `${new Date().toISOString().slice(0, 19)}Z`;
168
+ }
169
+
170
+ /** The two envelope members a payload may own; both are copied, never invented. */
171
+ const MIRRORED = ["ticket", "candidateTreeSha"] as const;
172
+
173
+ /**
174
+ * Builds the envelope around a payload. The mirrored members are copied
175
+ * verbatim — including a value the validator will reject — because the store
176
+ * requires the envelope and the payload to agree exactly, and a "helpful"
177
+ * coercion here would turn a malformed payload into a disagreement instead.
178
+ */
179
+ export function buildRunEvent(input: {
180
+ readonly runId: string;
181
+ readonly commonDir: string;
182
+ readonly kind: string;
183
+ readonly role: "cli" | "executor";
184
+ readonly payload: unknown;
185
+ readonly version?: "run-event/1" | "run-event/2";
186
+ readonly eventId?: string;
187
+ }): RunEventInput {
188
+ const payload = typeof input.payload === "object" && input.payload !== null ? (input.payload as Record<string, unknown>) : undefined;
189
+ const mirrored: Record<string, unknown> = {};
190
+ if (payload !== undefined) {
191
+ for (const member of MIRRORED) {
192
+ if (Object.prototype.hasOwnProperty.call(payload, member) && payload[member] !== undefined) {
193
+ mirrored[member] = payload[member];
194
+ }
195
+ }
196
+ }
197
+ return {
198
+ version: input.version ?? "run-event/1",
199
+ ...(input.eventId === undefined ? {} : { eventId: input.eventId }),
200
+ runId: input.runId,
201
+ at: runInstant(),
202
+ repo: { commonDir: input.commonDir },
203
+ kind: input.kind as RunEventKind,
204
+ actor: { role: input.role },
205
+ ...mirrored,
206
+ attestation: "self",
207
+ payload: (input.payload ?? null) as Readonly<Record<string, unknown>>,
208
+ } as RunEventInput;
209
+ }
210
+
211
+ /**
212
+ * One row's worth of a string: neutralized, then whitespace-collapsed so no
213
+ * free-text member can end a row and start one of its own, then bounded.
214
+ */
215
+ export function oneLine(value: string, maximum = 240): string {
216
+ const collapsed = neutralizeForDisplay(value).replace(/\s+/g, " ").trim();
217
+ return collapsed.length <= maximum ? collapsed : `${collapsed.slice(0, Math.max(maximum - 1, 0))}…`;
218
+ }
219
+
220
+ /** The same treatment for anything that is not already a string. */
221
+ export function oneLineOf(value: unknown, maximum = 240): string {
222
+ if (typeof value === "string") return oneLine(value, maximum);
223
+ if (value === undefined) return "";
224
+ return oneLine(JSON.stringify(value) ?? String(value), maximum);
225
+ }
226
+
227
+ /**
228
+ * The labels the row carries wherever it is printed. `bound to the record` is
229
+ * the one that differs from the viewer's: `verify` always has a record's tree
230
+ * sha, so its round constraints were judged against THIS candidate rather than
231
+ * against any paired round.
232
+ */
233
+ export const RUN_JOURNAL_ROW_LABELS = "self-attested; observability, not evidence; bound to the record";
234
+
235
+ /** Nothing was found, so nothing was evaluated. The one shape `absent` takes. */
236
+ const ABSENT: RunJournalRow = { status: "absent", missing: [], attestation: "self" };
237
+
238
+ /**
239
+ * The self-attested completeness row for the candidate a delivery record binds.
240
+ *
241
+ * FOUND BY THE RECORD'S TREE SHA, NOT BY THE POINTER. The run whose journal
242
+ * describes this candidate has usually ended by the time anyone verifies it,
243
+ * and `run.ended` clears the worktree pointer — so "the current run" is exactly
244
+ * the wrong question. The store scans instead, which is affordable because it
245
+ * is unpruned by design and small, and which is what lets a journal outlive the
246
+ * worktree its run happened in.
247
+ *
248
+ * EVERY FAILURE IS `absent`. No store, no match, a journal that refuses the
249
+ * read discipline, a journal that vanished between the scan and the read: the
250
+ * row says nothing was found rather than inventing a verdict. A reader that
251
+ * treats `absent` as a failure does so behind its own opt-in — this function
252
+ * never decides that.
253
+ */
254
+ export async function resolveRunJournalRow(input: {
255
+ readonly cwd: string;
256
+ readonly treeSha: string;
257
+ readonly reviewedCandidateTreeShas?: readonly string[];
258
+ readonly mandatedLensIds?: readonly string[];
259
+ }): Promise<RunJournalRow> {
260
+ const resolved = await resolveRunSurface(input.cwd);
261
+ if (!resolved.ok) return ABSENT;
262
+ const acceptedTrees = [...new Set([input.treeSha, ...(input.reviewedCandidateTreeShas ?? [])])];
263
+ const matches = await Promise.all(acceptedTrees.map((treeSha) => resolved.surface.store.findByCandidateTreeSha(treeSha)));
264
+ const match = matches.find((entry) => entry !== undefined);
265
+ if (match === undefined) return ABSENT;
266
+ const read = await resolved.surface.store.read(match.runId);
267
+ if (!read.ok) return ABSENT;
268
+ const evaluation = evaluateRunJournal(read.events, input.treeSha, input.mandatedLensIds, acceptedTrees.slice(1));
269
+ const alsoMatching = [...new Set(matches.flatMap((entry) => entry === undefined ? [] : [entry.runId, ...entry.alsoMatching]))]
270
+ .filter((runId) => runId !== match.runId);
271
+ return {
272
+ runId: match.runId,
273
+ ...(alsoMatching.length === 0 ? {} : { alsoMatching }),
274
+ status: evaluation.status,
275
+ missing: evaluation.missing,
276
+ ...(evaluation.violations.length === 0 ? {} : { violations: evaluation.violations }),
277
+ ...(acceptedTrees.length === 1 ? {} : { recordTreeSha: input.treeSha, reviewedCandidateTreeShas: acceptedTrees.slice(1) }),
278
+ attestation: "self",
279
+ };
280
+ }
281
+
282
+ /**
283
+ * The row, rendered for a terminal. Every store-derived string goes through
284
+ * `oneLine` for the same reason the viewer's rows do: a run id or a constraint
285
+ * name printed raw is a string from a file anyone who can execute here may
286
+ * write, and this one is printed under a line an operator reads as a verdict.
287
+ */
288
+ export function runJournalRows(row: RunJournalRow): readonly string[] {
289
+ const rows = [` run journal: ${oneLine(row.status, 64)} (${RUN_JOURNAL_ROW_LABELS})`];
290
+ if (row.runId !== undefined) rows.push(` run: ${oneLine(row.runId, 128)}`);
291
+ if (row.alsoMatching !== undefined && row.alsoMatching.length > 0) {
292
+ rows.push(` also matching: ${row.alsoMatching.map((id) => oneLine(id, 128)).join(", ")}`);
293
+ }
294
+ if (row.recordTreeSha !== undefined && row.reviewedCandidateTreeShas !== undefined) {
295
+ rows.push(` record candidate: ${oneLine(row.recordTreeSha, 64)}`);
296
+ rows.push(` reviewed candidate: ${row.reviewedCandidateTreeShas.map((tree) => oneLine(tree, 64)).join(", ")} (verified review-neutral projection)`);
297
+ }
298
+ rows.push(` missing: ${row.missing.length === 0 ? "(none)" : row.missing.map((entry) => oneLine(entry, 64)).join(", ")}`);
299
+ if (row.violations !== undefined && row.violations.length > 0) {
300
+ rows.push(` violations: ${row.violations.map((entry) => oneLine(entry, 64)).join(", ")}`);
301
+ }
302
+ return rows;
303
+ }
304
+
305
+ /** The typed refusal a config-free command returns when it has nothing to work with. */
306
+ export function runSurfaceBlocker(input: {
307
+ readonly code: string;
308
+ readonly summary: string;
309
+ readonly details?: string;
310
+ readonly remediation: { readonly id: string; readonly summary: string };
311
+ }): Blocker {
312
+ return createBlocker({
313
+ code: input.code,
314
+ source: { kind: "command", id: RUN_SURFACE_SOURCE },
315
+ summary: input.summary,
316
+ ...(input.details === undefined ? {} : { details: input.details }),
317
+ remediations: [{ id: input.remediation.id, kind: "manual_action", summary: input.remediation.summary }],
318
+ });
319
+ }
320
+
321
+ /**
322
+ * Whether a `harness.config.ts` sits at this worktree root. `lstat` only: the
323
+ * module is never imported, never read, never parsed. The note this answers is
324
+ * presentational, and the file it looks for is candidate-committed, so its
325
+ * absence is suppressible by anyone who can write the tree — it bounds
326
+ * accident, not tampering.
327
+ */
328
+ export function harnessConfigPresentAt(rootDir: string): boolean {
329
+ try {
330
+ return lstatSync(path.join(rootDir, "harness.config.ts")).isFile();
331
+ } catch {
332
+ return false;
333
+ }
334
+ }
@@ -0,0 +1,77 @@
1
+ import { withRetainedRecord } from "./run-view-record.ts";
2
+ import { projectRunView } from "./run-view.ts";
3
+ import {
4
+ resolveRunSurface,
5
+ oneLine,
6
+ runSurfaceBlocker,
7
+ } from "./run-surface.ts";
8
+ import type { ConfigFreeCommandContext, CommandResult } from "./boundary.ts";
9
+ export async function runViewCommand(
10
+ context: ConfigFreeCommandContext,
11
+ args: readonly string[],
12
+ ): Promise<CommandResult> {
13
+ const runId = args[0];
14
+ let json = false;
15
+ let recordPath: string | undefined;
16
+ for (let i = 1; i < args.length; i++) {
17
+ if (args[i] === "--json" && !json) json = true;
18
+ else if (args[i] === "--record" && recordPath === undefined && args[i + 1])
19
+ recordPath = args[++i];
20
+ else
21
+ return {
22
+ kind: "usage",
23
+ message:
24
+ "Usage: runs view <run-id> [--json] [--record <repository-relative-path>]",
25
+ };
26
+ }
27
+ if (!runId)
28
+ return {
29
+ kind: "usage",
30
+ message:
31
+ "Usage: runs view <run-id> [--json] [--record <repository-relative-path>]",
32
+ };
33
+ const resolved = await resolveRunSurface(context.rootDir);
34
+ const read = resolved.ok
35
+ ? await resolved.surface.store.read(args[0]!)
36
+ : undefined;
37
+ if (!read?.ok)
38
+ return {
39
+ kind: "blocked",
40
+ blockers: [
41
+ runSurfaceBlocker({
42
+ code: "run_view_unavailable",
43
+ summary: "Run observations are unavailable.",
44
+ details: resolved.ok
45
+ ? "The selected run could not be read."
46
+ : oneLine(resolved.reason),
47
+ remediation: {
48
+ id: "inspect-run",
49
+ summary: "Check the selected repository and run identifier.",
50
+ },
51
+ }),
52
+ ],
53
+ };
54
+ const view = await withRetainedRecord(
55
+ projectRunView(read.events, { now: new Date().toISOString() }),
56
+ context.rootDir,
57
+ recordPath,
58
+ );
59
+ if (json) context.write(JSON.stringify(view));
60
+ else {
61
+ context.write(view.authority);
62
+ for (const section of view.sections) {
63
+ context.write(section.title);
64
+ if (!section.items.length) context.write(` ${section.empty}`);
65
+ for (const item of section.items) {
66
+ context.write(` ${oneLine(item.label)}`);
67
+ for (const field of item.fields)
68
+ context.write(` ${oneLine(field.label)}: ${oneLine(field.value, 8192)}`);
69
+ if (item.artifactId)
70
+ context.write(
71
+ ` Report: runs artifact ${oneLine(args[0]!)} ${oneLine(item.artifactId)} --json`,
72
+ );
73
+ }
74
+ }
75
+ }
76
+ return { kind: "ok" };
77
+ }
@@ -0,0 +1,138 @@
1
+ import { oneLine } from "./run-surface.ts";
2
+ import type { RunView, RunViewItem, RunViewSection } from "./run-view.ts";
3
+ import type {
4
+ RunArtifactResult,
5
+ RunArtifactMetadata,
6
+ } from "@agent-delivery-harness/kernel";
7
+ export const escapeViewHtml = (value: string) =>
8
+ value
9
+ .replace(/&/g, "&amp;")
10
+ .replace(/</g, "&lt;")
11
+ .replace(/>/g, "&gt;")
12
+ .replace(/"/g, "&quot;")
13
+ .replace(/'/g, "&#39;");
14
+ const text = (value: string) => escapeViewHtml(oneLine(value, 8192));
15
+ export const OPERATIONAL_STYLE = `
16
+ :root{color-scheme:light dark;--paper:#f5f5f7;--surface:#fff;--ink:#1d1d1f;--muted:#626267;--line:#d8d8de;--link:#0067c0;--wash:#eaf2fa}
17
+ *{box-sizing:border-box}body{font:400 1rem/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-optical-sizing:auto;letter-spacing:-.01em;color:var(--ink);background:var(--paper);max-width:76rem;margin:0 auto;padding:2.5rem 2rem 5rem;overflow-wrap:anywhere}
18
+ h1{font-size:2.25rem;line-height:1.12;letter-spacing:-.04em;margin:0 0 1.25rem;font-weight:700}h2{font-size:1.35rem;line-height:1.3;letter-spacing:-.025em;margin:2rem 0 .5rem}h3{font-size:1.3rem;line-height:1.3;letter-spacing:-.025em;color:var(--ink);margin:0 0 1rem}h4{font-size:1.05rem;line-height:1.35;margin:0 0 1rem;letter-spacing:-.015em}
19
+ p{margin:.6rem 0 1rem}.meta,.ended{color:var(--muted);font-size:.875rem}.labels{font-size:.875rem;color:var(--muted);background:var(--surface);border:1px solid var(--line);border-radius:.75rem;padding:1rem 1.25rem;margin:1rem 0}.live{color:var(--ink)}.open{color:var(--muted)}
20
+ a{color:var(--link);text-underline-offset:.2em;border-radius:.3rem}a:hover{text-decoration-thickness:2px}a:active,summary:active{background:var(--wash);color:var(--ink)}a:focus-visible,summary:focus-visible{outline:3px solid var(--link);outline-offset:3px}nav{display:flex;flex-wrap:wrap;gap:.4rem;margin:1.5rem 0 2.5rem}nav a{display:inline-flex;align-items:center;min-height:2.75rem;padding:.5rem .85rem;border:1px solid var(--line);border-radius:2rem;background:var(--surface);font-size:.875rem;text-decoration:none}nav a:hover{border-color:var(--link)}
21
+ section{scroll-margin-top:1.5rem;margin:2.5rem 0}section>p{color:var(--muted);font-size:.9375rem}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,23rem),1fr));gap:1rem}.card{background:var(--surface);border:1px solid var(--line);border-radius:1rem;padding:1.5rem;min-width:0}.card>a{display:inline-flex;align-items:center;min-height:2.75rem;margin-top:.5rem}dl{margin:.5rem 0}dt{font-size:.8125rem;color:var(--muted);font-weight:500;margin-top:.75rem}dd{margin:.15rem 0 .75rem;overflow-wrap:anywhere}details{margin:1rem 0;border-top:1px solid var(--line);padding-top:.5rem}summary{cursor:pointer;min-height:2.75rem;padding:.5rem 0;font-size:.875rem;color:var(--muted);border-radius:.3rem}
22
+ pre{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--surface);border:1px solid var(--line);border-radius:1rem;padding:1.5rem;font: .875rem/1.65 ui-monospace,SFMono-Regular,Menlo,monospace}.table-scroll{max-width:100%;overflow-x:auto;margin:1rem 0 2rem;border:1px solid var(--line);border-radius:.75rem}table{border-collapse:collapse;display:table;min-width:48rem;width:100%;font-size:.8125rem;margin:0}th,td{border:0;border-bottom:1px solid var(--line);padding:.75rem;text-align:left;vertical-align:top}th{background:var(--surface);font-weight:600}tr:last-child td{border-bottom:0}
23
+ .back{margin:.5rem 0}.run-identifiers,.view-provenance{margin:.25rem 0;border:0;padding:0}.section-navigation{margin:.25rem 0 1rem;border:0;padding:0}.section-navigation nav{margin:.5rem 0}body>details:first-of-type{margin:.5rem 0;border:0;padding:0}body>h1{margin-bottom:.5rem}section:first-of-type{margin-top:1.25rem}
24
+ @media(max-width:600px){body{padding:1.5rem 1rem 3rem;margin:0}h1{font-size:1.875rem}.cards{grid-template-columns:1fr}.card{padding:1.125rem}nav{gap:.375rem}section{margin:2rem 0}}
25
+ @media(prefers-color-scheme:dark){:root{--paper:#161618;--surface:#212124;--ink:#f5f5f7;--muted:#b1b1b8;--line:#45454b;--link:#8ac7ff;--wash:#263b50}}
26
+ @media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
27
+ @media(prefers-reduced-transparency:reduce){.card,nav a,.labels{background:var(--surface);backdrop-filter:none}}
28
+ @media(prefers-contrast:more){:root{--muted:var(--ink);--line:var(--ink)}a{text-decoration:underline}nav a{border-width:2px}}
29
+ body{max-width:68rem;padding-top:2rem}main{min-width:0}.page-header{margin:2rem 0}.page-header h1{margin:.4rem 0 .75rem}.eyebrow{font-size:.75rem;letter-spacing:.1em;text-transform:uppercase;font-weight:600;color:var(--muted);margin:0 0 .5rem}.toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:1rem;font-size:.8125rem;margin:1rem 0 2rem}.toolbar a{min-height:2.75rem;display:inline-flex;align-items:center}.toolbar .meta{margin-right:auto}.status{display:inline-flex;padding:.3rem .65rem;background:var(--wash);border-radius:2rem;font-size:.8125rem;font-weight:600}.run-list{display:grid;gap:.75rem}.run-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.75rem 2rem;background:var(--surface);border:1px solid var(--line);border-radius:1rem;padding:1.25rem 1.5rem}.run-row h3{font-size:1.125rem;margin:0}.run-row h3 a{display:inline-flex;align-items:center;min-height:2.75rem}.run-row p{margin:.25rem 0}.run-row .run-caption{grid-column:1/-1;font-size:.8125rem;color:var(--muted)}.now-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}.now-grid section{margin:0}.now-grid section h2{margin-top:.5rem}.now-grid .cards{grid-template-columns:1fr}.empty-note{margin:0;padding:1rem 0;font-size:.875rem}.supporting{margin-top:3rem}.supporting>summary{font-size:1rem;font-weight:600;color:var(--ink)}summary span{display:block;font-size:.8125rem;font-weight:400;color:var(--muted);margin:.25rem 0}.section-detail>section{margin-top:.5rem}.card{padding:1.25rem}.card h3{font-size:1rem;margin-bottom:.75rem}.card dl{display:grid;grid-template-columns:7rem minmax(0,1fr);gap:.35rem 1rem;margin:0}.card dt,.card dd{margin:0;font-size:.875rem}.card details{margin-bottom:0}.card details dl{display:block}.card details dt{margin-top:.75rem}.card details dd{margin-top:.2rem}.report-card{border-top:3px solid var(--line)}.report-link{font-weight:500;gap:.5rem}.notice{border-left:3px solid var(--line);padding:.75rem 1rem;background:var(--surface);border-radius:.25rem}.report-page{max-width:54rem}.report-document{background:var(--surface);border:1px solid var(--line);border-radius:1.25rem;padding:2.5rem}.report-document header h2{font-size:1.8rem;margin:.5rem 0 1rem}.report-document p{line-height:1.7;white-space:pre-wrap}.report-document pre{padding:0;border:0}.report-document section{margin:2rem 0}.report-document h3{font-size:1.2rem;line-height:1.4}.finding{border-top:1px solid var(--line);padding:1.75rem 0 .5rem}.finding h4{margin:1.5rem 0 .5rem}.count{font-size:.875rem;font-weight:400;color:var(--muted);margin-left:.5rem}.document-fields>dt{color:var(--ink);font-weight:600;font-size:.875rem;margin-top:1.5rem}.document-fields dd p{margin-top:.25rem}.document-list{padding-left:1.25rem}.document-list li{margin:.75rem 0}.report-tools{margin:1.5rem 0}.report-tools a{display:inline-flex;align-items:center;min-height:2.75rem}
30
+ @media(max-width:600px){.now-grid{grid-template-columns:1fr;gap:.5rem}.run-row{padding:1rem;gap:.5rem}.run-row .status{align-self:start}.card dl{grid-template-columns:5.5rem minmax(0,1fr)}.card dt{margin-top:0}.report-document{padding:1.25rem}.page-header{margin:1.5rem 0}.toolbar{gap:.5rem 1rem}}
31
+ `;
32
+ // Presentation consumes the shared projection; it never decides admission.
33
+ const field = (item: RunViewItem, label: string) => item.fields.find(f => f.label === label)?.value;
34
+ const friendlyLabels = new Map([
35
+ ["lens.outcome-correctness", "Outcome review"],
36
+ ["lens.adversarial-testing", "Testing review"],
37
+ ["review", "Review report"],
38
+ ["reduction", "Review summary"],
39
+ ["clarification", "Review clarification"],
40
+ ["partial-output", "Partial report"],
41
+ ]);
42
+ const friendly = (value: string) => friendlyLabels.get(value) ?? value;
43
+ const primaryLabels: Record<string, readonly string[]> = {
44
+ waiting: ["Owner", "Human action required", "Next action", "Freshness"],
45
+ work: ["Owner", "State", "Freshness", "Next step"],
46
+ reviews: ["Round", "Owner", "State", "Verdict", "Freshness", "Next step"],
47
+ reports: ["Availability", "Reason"],
48
+ finish: ["State", "Owner"],
49
+ findings: ["State", "Severity", "Deferred issue"],
50
+ };
51
+ function fieldsHtml(fields: RunViewItem["fields"]): string {
52
+ return fields.map(f => `<dt>${text(f.label)}</dt><dd>${text(f.value)}</dd>`).join("");
53
+ }
54
+ function card(item: RunViewItem, section: string, baseHref: string): string {
55
+ const labels = primaryLabels[section];
56
+ const primary = item.fields.filter(f => labels ? labels.includes(f.label) && f.value !== "Unreported" && !(section === "reports" && f.label === "Availability" && f.value === "referenced") : true);
57
+ const supporting = item.fields.filter(f => !primary.includes(f));
58
+ const title = section === "reports"
59
+ ? friendly(field(item, "Lens") === "Unreported" ? item.label : field(item, "Lens") ?? item.label)
60
+ : section === "work" ? label(field(item, "Phase") ?? "Delivery activity") : friendly(item.label);
61
+ return `<article data-key="${text(item.id)}" class="card ${section === "reports" ? "report-card" : ""}"><h3>${text(title)}</h3><dl>${fieldsHtml(primary)}</dl>${item.artifactId === undefined ? "" : `<a class="report-link" href="${text(baseHref)}/artifacts/${encodeURIComponent(item.artifactId)}">Read report <span aria-hidden="true">↗</span></a>`}${supporting.length ? `<details data-key="support-${text(item.id)}"><summary>Supporting details</summary><dl>${section === "work" ? `<dt>Activity</dt><dd>${text(item.label)}</dd>` : ""}${fieldsHtml(supporting)}</dl></details>` : ""}</article>`;
62
+ }
63
+ const sectionTitles: Record<string, string> = { waiting: "Needs attention", work: "In progress", reviews: "Reviews", reports: "Review reports", finish: "Delivery milestones" };
64
+ const emptyCopy: Record<string, string> = { waiting: "No waits reported.", reviews: "No current reviews reported.", reports: "Review reports will appear here when they are recorded.", finish: "Delivery milestones have not been reported." };
65
+ function sectionHtml(section: RunViewSection, baseHref: string): string {
66
+ return `<section id="${text(section.id)}"><h2>${text(sectionTitles[section.id] ?? section.title)}</h2>${section.items.length ? `<div class="cards">${section.items.map(i => card(i, section.id === "earlier-report-list" ? "reports" : section.id, baseHref)).join("")}</div>` : `<p class="empty-note">${text(emptyCopy[section.id] ?? section.empty)}</p>`}</section>`;
67
+ }
68
+ export function renderOperationalView(view: RunView, baseHref: string): string {
69
+ const sections = new Map(view.sections.map(s => [s.id, s]));
70
+ const render = (id: string) => sections.has(id) ? sectionHtml(sections.get(id)!, baseHref) : "";
71
+ const reviews = sections.get("reviews");
72
+ const work = sections.get("work");
73
+ const currentReviews = reviews?.items.filter(i => i.label !== "Declared review round" && field(i, "History") !== "Superseded attempt") ?? [];
74
+ const reviewIds = new Set(currentReviews.map(i => i.id));
75
+ const reviewCards = currentReviews.map(i => ({ ...i, fields: [...i.fields, ...(work?.items.find(w => w.id === i.id)?.fields.filter(f => ["Freshness", "Next step"].includes(f.label)) ?? [])] }));
76
+ if (work) sections.set("work", { ...work, items: work.items.filter(i => !reviewIds.has(i.id)), empty: work.items.some(i => reviewIds.has(i.id)) ? "Reviews are shown below." : work.empty });
77
+ const reports = sections.get("reports");
78
+ const currentReports = reports?.items.filter(i => field(i, "History") === "Latest observed attempt") ?? [];
79
+ const earlierReports = reports?.items.filter(i => !currentReports.includes(i)) ?? [];
80
+ const pastReviews = reviews?.items.filter(i => !currentReviews.includes(i)) ?? [];
81
+ return `${view.historical ? '<p class="notice">Historical archive · Observations retained at export.</p>' : ""}
82
+ <div class="now-grid">${render("waiting")}${render("work")}</div>
83
+ ${sections.get("findings")?.items.length ? render("findings") : ""}
84
+ ${reviews ? sectionHtml({ ...reviews, items: reviewCards }, baseHref) : ""}
85
+ ${reports ? sectionHtml({ ...reports, items: currentReports }, baseHref) : ""}
86
+ ${earlierReports.length && reports ? `<details id="earlier-reports"><summary>Earlier reports <span>${earlierReports.length} reports from earlier attempts or incomplete references</span></summary>${sectionHtml({ ...reports, id: "earlier-report-list", title: "Earlier reports", items: earlierReports }, baseHref)}</details>` : ""}${render("finish")}
87
+ <details id="supporting-evidence" class="supporting"><summary>Evidence, cost and history<span>Supporting facts and earlier observations</span></summary>
88
+ ${pastReviews.length && reviews ? sectionHtml({ ...reviews, id: "review-history", title: "Earlier reviews and round details", items: pastReviews }, baseHref) : ""}
89
+ ${["evidence", "cost", "activity-history", "finding-history"].map(id => `<details id="details-${id}" class="section-detail"><summary>${text(sections.get(id)?.title ?? id)} <span>${sections.get(id)?.items.length ?? 0} entries</span></summary>${render(id)}</details>`).join("")}
90
+ ${!sections.get("findings")?.items.length ? render("findings") : ""}</details>`;
91
+ }
92
+ const record = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
93
+ const label = (key: string) => key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").replace(/^./, c => c.toUpperCase());
94
+ // A bounded reading view for arbitrary report schemas. All content remains inert;
95
+ // the retained source and download are the authority for exact report bytes.
96
+ function documentValue(value: unknown, depth = 0): string {
97
+ if (depth > 4) return '<p class="meta">Further detail is available in the original report.</p>';
98
+ if (Array.isArray(value)) return value.length ? `<ul class="document-list">${value.slice(0, 40).map(v => `<li>${documentValue(v, depth + 1)}</li>`).join("")}</ul>${value.length > 40 ? '<p class="meta">Additional entries are available in the original report.</p>' : ""}` : '<p class="meta">None reported.</p>';
99
+ if (record(value)) return `<dl class="document-fields">${Object.entries(value).slice(0, 40).map(([k,v]) => `<dt>${text(label(k))}</dt><dd>${documentValue(v, depth + 1)}</dd>`).join("")}</dl>${Object.keys(value).length > 40 ? '<p class="meta">Additional fields are available in the original report.</p>' : ""}`;
100
+ return `<p>${text(value === null ? "Not reported" : String(value))}</p>`;
101
+ }
102
+ function structuredReport(source: string): string | undefined {
103
+ let parsed: unknown;
104
+ try { parsed = JSON.parse(source); } catch { return undefined; }
105
+ if (!record(parsed)) return undefined;
106
+ const result = parsed["outcome"] ?? parsed["verdict"] ?? parsed["decision"];
107
+ const findings = parsed["findings"];
108
+ const headline = result === "aligned" ? "Aligned" : (result === "dissent" || result === "changes-requested" || result === "CHANGES_REQUESTED") ? "Changes requested" : typeof result === "string" ? result : "Report contents";
109
+ const summary = parsed["summary"] ?? parsed["assessment"];
110
+ const rest = Object.fromEntries(Object.entries(parsed).filter(([k]) => !["outcome", "verdict", "decision", "findings", "summary", "assessment"].includes(k)));
111
+ return `<article class="report-document"><header><p class="eyebrow">Reported result</p><h2>${text(headline)}</h2>${summary === undefined ? "" : documentValue(summary)}</header>
112
+ ${Array.isArray(findings) ? `<section><h2>Findings <span class="count">${findings.length}</span></h2>${findings.length === 0 ? '<p>No findings reported in this document.</p>' : findings.slice(0, 40).map((finding, index) => {
113
+ if (!record(finding)) return documentValue(finding);
114
+ const title = typeof finding["title"] === "string" ? finding["title"] : `Finding ${index + 1}`;
115
+ const why = finding["why_it_matters"] ?? finding["description"];
116
+ const fix = finding["suggested_fix"];
117
+ const extra = Object.fromEntries(Object.entries(finding).filter(([k]) => !["title", "severity", "why_it_matters", "description", "suggested_fix"].includes(k)));
118
+ return `<article class="finding"><p class="eyebrow">${text(typeof finding["severity"] === "string" ? finding["severity"] : "Severity unreported")}</p><h3>${text(title)}</h3>${why === undefined ? "" : documentValue(why)}${fix === undefined ? "" : `<h4>Recommended change</h4>${documentValue(fix)}`}<details><summary>Evidence and context</summary>${documentValue(extra)}</details></article>`;
119
+ }).join("")}${findings.length > 40 ? '<p>Additional findings are available in the original report.</p>' : ""}</section>` : findings === undefined ? "" : `<section><h2>Findings</h2>${documentValue(findings)}</section>`}
120
+ <details><summary>Review context and supporting evidence</summary>${documentValue(rest)}</details></article>`;
121
+ }
122
+ export function renderArtifactDetail(input: {
123
+ runId: string;
124
+ artifactId: string;
125
+ backHref: string;
126
+ result: RunArtifactResult;
127
+ historical: boolean;
128
+ metadata?: RunArtifactMetadata;
129
+ }): string {
130
+ const { result } = input;
131
+ const m = result.ok ? result.metadata : input.metadata;
132
+ const source = result.ok ? Buffer.from(result.base64, "base64").toString("utf8") : "";
133
+ const document = result.ok ? structuredReport(source) : undefined;
134
+ const title = friendly(m?.lensId ?? "Review report");
135
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${text(title)} · Delivery runs</title><style>${OPERATIONAL_STYLE}</style></head><body class="report-page"><main><a class="back" href="${text(input.backHref)}#reports">Return to run</a><header class="page-header"><p class="eyebrow">${input.historical ? "Historical archive" : "Retained report"}${m?.round === undefined ? "" : ` · Round ${m.round}`}</p><h1>${text(title)}</h1><p class="meta">Reported review output; not approval evidence.</p></header>
136
+ ${result.ok ? `${document ?? `<article class="report-document"><h2>Report contents</h2><pre>${escapeViewHtml(source)}</pre></article>`}<div class="report-tools"><a href="${text(input.backHref)}/artifacts/${encodeURIComponent(input.artifactId)}/download">Download original report</a></div>${document ? `<details><summary>Original report source</summary><pre>${escapeViewHtml(source)}</pre></details>` : ""}` : `<section class="notice" role="status"><h2>Report unavailable</h2><p>${text(result.reason)}</p><p class="meta">${text(result.code)}</p></section>`}
137
+ <details><summary>Report details and provenance</summary><dl><dt>Run</dt><dd>${text(input.runId)}</dd><dt>Artifact</dt><dd>${text(input.artifactId)}</dd>${m ? `<dt>Lens</dt><dd>${text(m.lensId ?? "Unreported")}</dd><dt>Attempt</dt><dd>${text(m.attemptId)}</dd><dt>Candidate</dt><dd>${text(m.candidateTreeSha)}</dd><dt>Digest</dt><dd>${text(m.digest)}</dd><dt>Size</dt><dd>${m.sizeBytes} bytes</dd>` : ""}</dl></details></main></body></html>`;
138
+ }