@agent-delivery-harness/cli 0.2.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 +4 -4
  4. package/src/boundary.ts +77 -61
  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 +80 -12
  10. package/src/commands/gate.ts +33 -8
  11. package/src/commands/maintain.ts +1 -0
  12. package/src/commands/managed.ts +1 -0
  13. package/src/commands/prepare.ts +123 -22
  14. package/src/commands/record.ts +98 -3
  15. package/src/commands/resume.ts +53 -0
  16. package/src/commands/review-context.ts +19 -0
  17. package/src/commands/runs.ts +415 -30
  18. package/src/commands/save-context.ts +51 -0
  19. package/src/commands/submit-evidence.ts +46 -7
  20. package/src/commands/verify.ts +29 -8
  21. package/src/declared-checks.ts +50 -0
  22. package/src/index.ts +14 -1
  23. package/src/main.ts +20 -47
  24. package/src/ordinary-context.ts +99 -0
  25. package/src/provider-rails.ts +23 -704
  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 +129 -11
  35. package/src/run-server.ts +394 -149
  36. package/src/run-surface.ts +81 -8
  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
@@ -3,6 +3,28 @@
3
3
  * found, how an event envelope is built, and how a store-derived string is
4
4
  * made safe to print.
5
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
+ *
6
28
  * WHY THE STORE IS RESOLVED HERE AND NOT WIRED. The record store is wired from
7
29
  * the config, because a delivery record belongs to a configured gate. A run
8
30
  * belongs to the *repository* — it outlives the worktree it ran in and it
@@ -25,6 +47,7 @@ import path from "node:path";
25
47
  import {
26
48
  createBlocker,
27
49
  createRunStore,
50
+ sha256Hex,
28
51
  evaluateRunJournal,
29
52
  gitNamespaceClearedEnvironment,
30
53
  neutralizeForDisplay,
@@ -42,7 +65,9 @@ export const RUN_SURFACE_SOURCE = "delivery-harness.cli.run-surface";
42
65
 
43
66
  export interface RunSurface {
44
67
  readonly store: RunStore;
68
+ /** The repository's OWN git common directory, which is what an event names. */
45
69
  readonly commonDir: string;
70
+ /** The store's `runs` directory, under the override when one is named. */
46
71
  readonly runsDir: string;
47
72
  /** The invoking worktree's pointer key — the identity of "the current run" here. */
48
73
  readonly worktreeKey: string;
@@ -52,10 +77,39 @@ export type RunSurfaceResolution =
52
77
  | { readonly ok: true; readonly surface: RunSurface }
53
78
  | { readonly ok: false; readonly reason: string };
54
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
+
55
105
  /**
56
106
  * Resolves the run store for the invoking worktree. Never throws: a config-free
57
107
  * command turns a failure into a typed blocker, and the boundary wrap turns it
58
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.
59
113
  */
60
114
  export async function resolveRunSurface(cwd: string): Promise<RunSurfaceResolution> {
61
115
  const location = await resolveRunStoreLocation({
@@ -64,12 +118,15 @@ export async function resolveRunSurface(cwd: string): Promise<RunSurfaceResoluti
64
118
  env: gitNamespaceClearedEnvironment(),
65
119
  });
66
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);
67
124
  return {
68
125
  ok: true,
69
126
  surface: {
70
- store: createRunStore(location.commonDir),
127
+ store,
71
128
  commonDir: location.commonDir,
72
- runsDir: location.runsDir,
129
+ runsDir: store.runsDir,
73
130
  worktreeKey: location.worktreeKey,
74
131
  },
75
132
  };
@@ -77,8 +134,11 @@ export async function resolveRunSurface(cwd: string): Promise<RunSurfaceResoluti
77
134
 
78
135
  /**
79
136
  * The worktree ROOT of a path, for the one thing the run surface needs it for:
80
- * the config-presence note names a root, and `runs serve` renders a root the
81
- * operator named rather than the one it was invoked in.
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.
82
142
  *
83
143
  * `--show-toplevel` is a plumbing query that reads no index and runs no hook,
84
144
  * alias, or pager, and it runs here with the `GIT_` namespace cleared for the
@@ -122,6 +182,8 @@ export function buildRunEvent(input: {
122
182
  readonly kind: string;
123
183
  readonly role: "cli" | "executor";
124
184
  readonly payload: unknown;
185
+ readonly version?: "run-event/1" | "run-event/2";
186
+ readonly eventId?: string;
125
187
  }): RunEventInput {
126
188
  const payload = typeof input.payload === "object" && input.payload !== null ? (input.payload as Record<string, unknown>) : undefined;
127
189
  const mirrored: Record<string, unknown> = {};
@@ -133,7 +195,8 @@ export function buildRunEvent(input: {
133
195
  }
134
196
  }
135
197
  return {
136
- version: "run-event/1",
198
+ version: input.version ?? "run-event/1",
199
+ ...(input.eventId === undefined ? {} : { eventId: input.eventId }),
137
200
  runId: input.runId,
138
201
  at: runInstant(),
139
202
  repo: { commonDir: input.commonDir },
@@ -191,21 +254,27 @@ const ABSENT: RunJournalRow = { status: "absent", missing: [], attestation: "sel
191
254
  export async function resolveRunJournalRow(input: {
192
255
  readonly cwd: string;
193
256
  readonly treeSha: string;
257
+ readonly reviewedCandidateTreeShas?: readonly string[];
194
258
  readonly mandatedLensIds?: readonly string[];
195
259
  }): Promise<RunJournalRow> {
196
260
  const resolved = await resolveRunSurface(input.cwd);
197
261
  if (!resolved.ok) return ABSENT;
198
- const match = await resolved.surface.store.findByCandidateTreeSha(input.treeSha);
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);
199
265
  if (match === undefined) return ABSENT;
200
266
  const read = await resolved.surface.store.read(match.runId);
201
267
  if (!read.ok) return ABSENT;
202
- const evaluation = evaluateRunJournal(read.events, input.treeSha, input.mandatedLensIds);
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);
203
271
  return {
204
272
  runId: match.runId,
205
- ...(match.alsoMatching.length === 0 ? {} : { alsoMatching: match.alsoMatching }),
273
+ ...(alsoMatching.length === 0 ? {} : { alsoMatching }),
206
274
  status: evaluation.status,
207
275
  missing: evaluation.missing,
208
276
  ...(evaluation.violations.length === 0 ? {} : { violations: evaluation.violations }),
277
+ ...(acceptedTrees.length === 1 ? {} : { recordTreeSha: input.treeSha, reviewedCandidateTreeShas: acceptedTrees.slice(1) }),
209
278
  attestation: "self",
210
279
  };
211
280
  }
@@ -222,6 +291,10 @@ export function runJournalRows(row: RunJournalRow): readonly string[] {
222
291
  if (row.alsoMatching !== undefined && row.alsoMatching.length > 0) {
223
292
  rows.push(` also matching: ${row.alsoMatching.map((id) => oneLine(id, 128)).join(", ")}`);
224
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
+ }
225
298
  rows.push(` missing: ${row.missing.length === 0 ? "(none)" : row.missing.map((entry) => oneLine(entry, 64)).join(", ")}`);
226
299
  if (row.violations !== undefined && row.violations.length > 0) {
227
300
  rows.push(` violations: ${row.violations.map((entry) => oneLine(entry, 64)).join(", ")}`);
@@ -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
+ }
@@ -0,0 +1,119 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { readArchiveFile } from "./run-archive-commands.ts";
4
+ /** Explicit retained-record reads only: no executable configuration or verification. */
5
+ import {
6
+ isSafeRelativePath,
7
+ isInsideResolved,
8
+ parseDeliveryRecord,
9
+ digestCanonical,
10
+ } from "@agent-delivery-harness/kernel";
11
+ import type { RunView, RunViewItem } from "./run-view.ts";
12
+ export async function withRetainedRecord(
13
+ view: RunView,
14
+ root: string,
15
+ relativePath?: string,
16
+ ): Promise<RunView> {
17
+ if (relativePath === undefined || view.historical) return view;
18
+ const fields: { label: string; value: string }[] = [];
19
+ const add = (label: string, value: string) => fields.push({ label, value });
20
+ add("Source", relativePath);
21
+ add(
22
+ "Applicability",
23
+ "Unknown — no current identity comparison or fresh verification is performed",
24
+ );
25
+ add(
26
+ "Original verification time",
27
+ "Unavailable — delivery records do not store a verification timestamp",
28
+ );
29
+ try {
30
+ let status = "path_refused";
31
+ let contents: string | null = null;
32
+ if (isSafeRelativePath(relativePath)) {
33
+ const resolvedRoot = await realpath(root);
34
+ const resolvedFile = await realpath(
35
+ path.join(resolvedRoot, relativePath),
36
+ );
37
+ if (!isInsideResolved(resolvedRoot, resolvedFile))
38
+ status = "outside_run_root";
39
+ else {
40
+ contents = await readArchiveFile(resolvedFile);
41
+ status = "readable";
42
+ }
43
+ }
44
+ const read = { status, contents };
45
+ if (read.status !== "readable" || read.contents === null) {
46
+ add("Status", `Unavailable — ${read.status}`);
47
+ } else {
48
+ const parsed = parseDeliveryRecord(read.contents);
49
+ if (!parsed.ok)
50
+ add("Status", "Corrupt — retained record grammar is invalid");
51
+ else {
52
+ const { integrityDigest, ...unsigned } = parsed.record;
53
+ if (parsed.record.version !== "delivery-record/2")
54
+ add(
55
+ "Status",
56
+ "Legacy record — integrity unavailable; no accepted evidence inferred",
57
+ );
58
+ else if (integrityDigest !== digestCanonical(unsigned))
59
+ add("Status", "Corrupt — retained record digest does not match");
60
+ else {
61
+ add(
62
+ "Status",
63
+ "Recorded for the candidate below — retained self-attested record, not current approval",
64
+ );
65
+ add("Candidate", parsed.record.candidateBinding.treeSha);
66
+ add(
67
+ "Deliverable digest",
68
+ parsed.record.candidateBinding.deliverableDigest,
69
+ );
70
+ for (const claim of parsed.record.claims) {
71
+ add(`Recorded claim · ${claim.obligationId}`, claim.outcome);
72
+ for (const evidence of [
73
+ claim.evidence,
74
+ ...(claim.supportingEvidence ?? []),
75
+ ]) {
76
+ if (evidence?.resolution.kind !== "evidence") continue;
77
+ const manifest = evidence.resolution.portable?.manifest;
78
+ if (typeof manifest !== "object" || manifest === null) continue;
79
+ if (
80
+ digestCanonical(manifest) !== evidence.resolution.manifestDigest
81
+ ) {
82
+ add(
83
+ "Retained manifest",
84
+ "Corrupt — digest does not match; timestamp unavailable",
85
+ );
86
+ continue;
87
+ }
88
+ const at = (manifest as Record<string, unknown>)["recordedAt"];
89
+ add(
90
+ `Provider-reported recording time · ${evidence.resolution.providerId}`,
91
+ typeof at === "string" ? at : "Unavailable",
92
+ );
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ } catch (error) {
99
+ add(
100
+ "Status",
101
+ (error as NodeJS.ErrnoException).code === "ENOENT"
102
+ ? "Unavailable — missing"
103
+ : "Unavailable — bounded retained record read refused",
104
+ );
105
+ }
106
+ const retained: RunViewItem = {
107
+ id: "retained-record",
108
+ label: "Retained delivery record",
109
+ fields,
110
+ };
111
+ return {
112
+ ...view,
113
+ sections: view.sections.map((section) =>
114
+ section.id === "evidence"
115
+ ? { ...section, items: [retained, ...section.items] }
116
+ : section,
117
+ ),
118
+ };
119
+ }