@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.
- package/LICENSE +104 -201
- package/NOTICE +1 -1
- package/package.json +4 -4
- package/src/boundary.ts +77 -61
- package/src/command-activity.ts +75 -0
- package/src/commands/admit.ts +106 -0
- package/src/commands/check.ts +13 -2
- package/src/commands/emit-review-evidence.ts +38 -0
- package/src/commands/emit.ts +80 -12
- package/src/commands/gate.ts +33 -8
- package/src/commands/maintain.ts +1 -0
- package/src/commands/managed.ts +1 -0
- package/src/commands/prepare.ts +123 -22
- package/src/commands/record.ts +98 -3
- package/src/commands/resume.ts +53 -0
- package/src/commands/review-context.ts +19 -0
- package/src/commands/runs.ts +415 -30
- package/src/commands/save-context.ts +51 -0
- package/src/commands/submit-evidence.ts +46 -7
- package/src/commands/verify.ts +29 -8
- package/src/declared-checks.ts +50 -0
- package/src/index.ts +14 -1
- package/src/main.ts +20 -47
- package/src/ordinary-context.ts +99 -0
- package/src/provider-rails.ts +23 -704
- package/src/record-retention.ts +325 -0
- package/src/review-evidence.ts +284 -0
- package/src/run-archive-commands.ts +164 -0
- package/src/run-archive.ts +127 -0
- package/src/run-artifact-commands.ts +163 -0
- package/src/run-attachments.ts +112 -0
- package/src/run-export.ts +164 -0
- package/src/run-live.ts +102 -0
- package/src/run-projection.ts +129 -11
- package/src/run-server.ts +394 -149
- package/src/run-surface.ts +81 -8
- package/src/run-view-command.ts +77 -0
- package/src/run-view-html.ts +138 -0
- package/src/run-view-record.ts +119 -0
- package/src/run-view.ts +343 -0
package/src/run-surface.ts
CHANGED
|
@@ -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
|
|
127
|
+
store,
|
|
71
128
|
commonDir: location.commonDir,
|
|
72
|
-
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
|
|
81
|
-
* operator named rather than the one it was
|
|
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
|
|
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
|
-
...(
|
|
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, "&")
|
|
10
|
+
.replace(/</g, "<")
|
|
11
|
+
.replace(/>/g, ">")
|
|
12
|
+
.replace(/"/g, """)
|
|
13
|
+
.replace(/'/g, "'");
|
|
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
|
+
}
|