@agent-delivery-harness/cli 0.2.0 → 0.7.3
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 +5 -5
- package/src/boundary.ts +84 -63
- package/src/check-snapshot.ts +167 -0
- 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 +64 -13
- package/src/commands/maintain.ts +1 -0
- package/src/commands/managed.ts +105 -34
- package/src/commands/prepare.ts +148 -22
- package/src/commands/record.ts +107 -3
- package/src/commands/resume.ts +53 -0
- package/src/commands/review-context.ts +19 -0
- package/src/commands/runs.ts +461 -30
- package/src/commands/save-context.ts +51 -0
- package/src/commands/submit-evidence.ts +46 -7
- package/src/commands/verify.ts +156 -10
- package/src/declared-checks.ts +50 -0
- package/src/index.ts +17 -1
- package/src/main.ts +56 -65
- package/src/ordinary-context.ts +83 -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 +202 -0
- package/src/run-live.ts +102 -0
- package/src/run-projection.ts +327 -12
- package/src/run-server.ts +403 -149
- package/src/run-surface.ts +196 -9
- 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/scoped-attempts.ts +86 -0
- package/src/scoped-candidate.ts +28 -0
- package/src/scoped-checks.ts +196 -0
- package/src/scoped-diagnostics.ts +124 -0
- package/src/scoped-observations.ts +51 -0
- package/src/snapshot-inventory-worker.ts +40 -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,7 +47,9 @@ import path from "node:path";
|
|
|
25
47
|
import {
|
|
26
48
|
createBlocker,
|
|
27
49
|
createRunStore,
|
|
50
|
+
sha256Hex,
|
|
28
51
|
evaluateRunJournal,
|
|
52
|
+
explainRunJournal,
|
|
29
53
|
gitNamespaceClearedEnvironment,
|
|
30
54
|
neutralizeForDisplay,
|
|
31
55
|
resolveRunStoreLocation,
|
|
@@ -33,6 +57,7 @@ import {
|
|
|
33
57
|
type Blocker,
|
|
34
58
|
type RunEventInput,
|
|
35
59
|
type RunEventKind,
|
|
60
|
+
type RunJournalRoundBinding,
|
|
36
61
|
type RunJournalRow,
|
|
37
62
|
type RunStore,
|
|
38
63
|
} from "@agent-delivery-harness/kernel";
|
|
@@ -42,7 +67,9 @@ export const RUN_SURFACE_SOURCE = "delivery-harness.cli.run-surface";
|
|
|
42
67
|
|
|
43
68
|
export interface RunSurface {
|
|
44
69
|
readonly store: RunStore;
|
|
70
|
+
/** The repository's OWN git common directory, which is what an event names. */
|
|
45
71
|
readonly commonDir: string;
|
|
72
|
+
/** The store's `runs` directory, under the override when one is named. */
|
|
46
73
|
readonly runsDir: string;
|
|
47
74
|
/** The invoking worktree's pointer key — the identity of "the current run" here. */
|
|
48
75
|
readonly worktreeKey: string;
|
|
@@ -52,10 +79,39 @@ export type RunSurfaceResolution =
|
|
|
52
79
|
| { readonly ok: true; readonly surface: RunSurface }
|
|
53
80
|
| { readonly ok: false; readonly reason: string };
|
|
54
81
|
|
|
82
|
+
/** The environment variable that relocates the run store. See this file's header. */
|
|
83
|
+
export const RUN_STORE_OVERRIDE = "DELIVERY_HARNESS_RUN_STORE";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The directory the store is rooted at for a repository whose common directory
|
|
87
|
+
* is `commonDir`.
|
|
88
|
+
*
|
|
89
|
+
* Blank is unset, following this repository's other environment levers. A
|
|
90
|
+
* value that is set but not absolute FAILS CLOSED — the resolution is refused
|
|
91
|
+
* rather than falling back to the repository's own store, because a caller
|
|
92
|
+
* that mis-spells its override is exactly the caller that must not reach a
|
|
93
|
+
* live store by accident.
|
|
94
|
+
*/
|
|
95
|
+
function storeRootFor(
|
|
96
|
+
commonDir: string,
|
|
97
|
+
override: string | undefined,
|
|
98
|
+
): { readonly ok: true; readonly root: string } | { readonly ok: false; readonly reason: string } {
|
|
99
|
+
const named = override?.trim() ?? "";
|
|
100
|
+
if (named.length === 0) return { ok: true, root: commonDir };
|
|
101
|
+
if (!path.isAbsolute(named)) {
|
|
102
|
+
return { ok: false, reason: `${RUN_STORE_OVERRIDE} must name an absolute directory` };
|
|
103
|
+
}
|
|
104
|
+
return { ok: true, root: path.join(named, sha256Hex(commonDir)) };
|
|
105
|
+
}
|
|
106
|
+
|
|
55
107
|
/**
|
|
56
108
|
* Resolves the run store for the invoking worktree. Never throws: a config-free
|
|
57
109
|
* command turns a failure into a typed blocker, and the boundary wrap turns it
|
|
58
110
|
* into silence.
|
|
111
|
+
*
|
|
112
|
+
* The override is read here, on this one path, because this is the one path
|
|
113
|
+
* every CLI reach for a run store goes through: `emit`, `runs`, `runs serve`,
|
|
114
|
+
* the boundary wrap's automatic completion, and `verify`'s journal row.
|
|
59
115
|
*/
|
|
60
116
|
export async function resolveRunSurface(cwd: string): Promise<RunSurfaceResolution> {
|
|
61
117
|
const location = await resolveRunStoreLocation({
|
|
@@ -64,12 +120,15 @@ export async function resolveRunSurface(cwd: string): Promise<RunSurfaceResoluti
|
|
|
64
120
|
env: gitNamespaceClearedEnvironment(),
|
|
65
121
|
});
|
|
66
122
|
if (!location.ok) return { ok: false, reason: location.reason };
|
|
123
|
+
const rooted = storeRootFor(location.commonDir, process.env[RUN_STORE_OVERRIDE]);
|
|
124
|
+
if (!rooted.ok) return { ok: false, reason: rooted.reason };
|
|
125
|
+
const store = createRunStore(rooted.root);
|
|
67
126
|
return {
|
|
68
127
|
ok: true,
|
|
69
128
|
surface: {
|
|
70
|
-
store
|
|
129
|
+
store,
|
|
71
130
|
commonDir: location.commonDir,
|
|
72
|
-
runsDir:
|
|
131
|
+
runsDir: store.runsDir,
|
|
73
132
|
worktreeKey: location.worktreeKey,
|
|
74
133
|
},
|
|
75
134
|
};
|
|
@@ -77,8 +136,11 @@ export async function resolveRunSurface(cwd: string): Promise<RunSurfaceResoluti
|
|
|
77
136
|
|
|
78
137
|
/**
|
|
79
138
|
* 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
|
|
139
|
+
* the config-presence note names a root, and BOTH surfaces resolve it here.
|
|
140
|
+
* `runs serve` renders a root the operator named rather than the one it was
|
|
141
|
+
* invoked in; `runs show` renders the root of the worktree it was invoked in
|
|
142
|
+
* rather than the directory, which may be any depth below it. One question
|
|
143
|
+
* about one repository has to have one answer whichever surface asks it.
|
|
82
144
|
*
|
|
83
145
|
* `--show-toplevel` is a plumbing query that reads no index and runs no hook,
|
|
84
146
|
* alias, or pager, and it runs here with the `GIT_` namespace cleared for the
|
|
@@ -122,6 +184,8 @@ export function buildRunEvent(input: {
|
|
|
122
184
|
readonly kind: string;
|
|
123
185
|
readonly role: "cli" | "executor";
|
|
124
186
|
readonly payload: unknown;
|
|
187
|
+
readonly version?: "run-event/1" | "run-event/2";
|
|
188
|
+
readonly eventId?: string;
|
|
125
189
|
}): RunEventInput {
|
|
126
190
|
const payload = typeof input.payload === "object" && input.payload !== null ? (input.payload as Record<string, unknown>) : undefined;
|
|
127
191
|
const mirrored: Record<string, unknown> = {};
|
|
@@ -133,7 +197,8 @@ export function buildRunEvent(input: {
|
|
|
133
197
|
}
|
|
134
198
|
}
|
|
135
199
|
return {
|
|
136
|
-
version: "run-event/1",
|
|
200
|
+
version: input.version ?? "run-event/1",
|
|
201
|
+
...(input.eventId === undefined ? {} : { eventId: input.eventId }),
|
|
137
202
|
runId: input.runId,
|
|
138
203
|
at: runInstant(),
|
|
139
204
|
repo: { commonDir: input.commonDir },
|
|
@@ -191,25 +256,133 @@ const ABSENT: RunJournalRow = { status: "absent", missing: [], attestation: "sel
|
|
|
191
256
|
export async function resolveRunJournalRow(input: {
|
|
192
257
|
readonly cwd: string;
|
|
193
258
|
readonly treeSha: string;
|
|
259
|
+
readonly reviewedCandidateTreeShas?: readonly string[];
|
|
194
260
|
readonly mandatedLensIds?: readonly string[];
|
|
195
261
|
}): Promise<RunJournalRow> {
|
|
196
262
|
const resolved = await resolveRunSurface(input.cwd);
|
|
197
263
|
if (!resolved.ok) return ABSENT;
|
|
198
|
-
const
|
|
264
|
+
const acceptedTrees = [...new Set([input.treeSha, ...(input.reviewedCandidateTreeShas ?? [])])];
|
|
265
|
+
const matches = await Promise.all(acceptedTrees.map((treeSha) => resolved.surface.store.findByCandidateTreeSha(treeSha)));
|
|
266
|
+
const match = matches.find((entry) => entry !== undefined);
|
|
199
267
|
if (match === undefined) return ABSENT;
|
|
200
268
|
const read = await resolved.surface.store.read(match.runId);
|
|
201
269
|
if (!read.ok) return ABSENT;
|
|
202
|
-
const evaluation = evaluateRunJournal(read.events, input.treeSha, input.mandatedLensIds);
|
|
270
|
+
const evaluation = evaluateRunJournal(read.events, input.treeSha, input.mandatedLensIds, acceptedTrees.slice(1));
|
|
271
|
+
// The same arguments, so the explanations describe exactly these violations.
|
|
272
|
+
const diagnostics = explainRunJournal(read.events, input.treeSha, input.mandatedLensIds, acceptedTrees.slice(1));
|
|
273
|
+
const alsoMatching = [...new Set(matches.flatMap((entry) => entry === undefined ? [] : [entry.runId, ...entry.alsoMatching]))]
|
|
274
|
+
.filter((runId) => runId !== match.runId);
|
|
203
275
|
return {
|
|
204
276
|
runId: match.runId,
|
|
205
|
-
...(
|
|
277
|
+
...(alsoMatching.length === 0 ? {} : { alsoMatching }),
|
|
206
278
|
status: evaluation.status,
|
|
207
279
|
missing: evaluation.missing,
|
|
208
|
-
...(evaluation.violations.length === 0 ? {} : { violations: evaluation.violations }),
|
|
280
|
+
...(evaluation.violations.length === 0 ? {} : { violations: evaluation.violations, explanations: diagnostics.explanations }),
|
|
281
|
+
...(acceptedTrees.length === 1 ? {} : { recordTreeSha: input.treeSha, reviewedCandidateTreeShas: acceptedTrees.slice(1) }),
|
|
282
|
+
...(diagnostics.roundBinding === undefined ? {} : { roundBinding: diagnostics.roundBinding }),
|
|
209
283
|
attestation: "self",
|
|
210
284
|
};
|
|
211
285
|
}
|
|
212
286
|
|
|
287
|
+
/**
|
|
288
|
+
* The instants a delivery record stamps: the journal's own first and last.
|
|
289
|
+
*
|
|
290
|
+
* `endedAt` is the last instant the journal had REACHED when it was read, not
|
|
291
|
+
* the run's ending — `record` runs long before `run.ended`, and a record that
|
|
292
|
+
* waited for the run to end would never be written. So the pair is the span the
|
|
293
|
+
* delivery had spent by the time it recorded, which is the figure the record is
|
|
294
|
+
* for; `runs show` is where the run's own ending is read.
|
|
295
|
+
*/
|
|
296
|
+
export interface RunSpanRow {
|
|
297
|
+
readonly runId: string;
|
|
298
|
+
readonly startedAt: string;
|
|
299
|
+
readonly endedAt: string;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The span of the journal that describes this candidate, or nothing.
|
|
304
|
+
*
|
|
305
|
+
* FOUND BY TREE SHA FIRST, for the reason `resolveRunJournalRow` is: a journal
|
|
306
|
+
* bound to this candidate is one that provably describes it, and by verify time
|
|
307
|
+
* the run has usually ended and cleared the worktree pointer. The pointer is a
|
|
308
|
+
* FALLBACK and only where the caller says so, because the two callers need
|
|
309
|
+
* different things from a miss. `record` is stamping: it runs mid-run, before
|
|
310
|
+
* anything need have journaled this exact tree, and the worktree's current run
|
|
311
|
+
* is the run it is recording from — a best-effort stamp is better than none.
|
|
312
|
+
* `verify` is refusing: a refusal must rest on a journal that binds the
|
|
313
|
+
* candidate the record binds, never on whichever run happens to be current in
|
|
314
|
+
* the checkout someone verified from, so it asks without the fallback and
|
|
315
|
+
* reports the miss as unchecked.
|
|
316
|
+
*
|
|
317
|
+
* MORE THAN ONE RUN CAN BIND ONE CANDIDATE, and the store says so: a delivery
|
|
318
|
+
* that recorded in run A and is verified from a later run B in the same
|
|
319
|
+
* worktree has two journals naming that tree, and `findByCandidateTreeSha`
|
|
320
|
+
* returns the most recently started one with the rest in `alsoMatching`.
|
|
321
|
+
* Refusing on whichever sorted first would block an honest record whose span
|
|
322
|
+
* run A reports exactly. So a caller that already knows the span it is checking
|
|
323
|
+
* passes `preferStartedAt`, and the matching run whose journal starts there is
|
|
324
|
+
* the one returned; only where NO matching run starts there does the
|
|
325
|
+
* top-ranked run stand, which is the case a refusal may rest on.
|
|
326
|
+
*
|
|
327
|
+
* Every failure is nothing found: no store, no match, an unreadable journal, an
|
|
328
|
+
* empty one. Nothing here decides what a miss means.
|
|
329
|
+
*/
|
|
330
|
+
export async function resolveRunSpan(input: {
|
|
331
|
+
readonly cwd: string;
|
|
332
|
+
readonly treeSha: string;
|
|
333
|
+
readonly allowCurrentRun?: boolean;
|
|
334
|
+
readonly preferStartedAt?: string;
|
|
335
|
+
}): Promise<RunSpanRow | undefined> {
|
|
336
|
+
const resolved = await resolveRunSurface(input.cwd);
|
|
337
|
+
if (!resolved.ok) return undefined;
|
|
338
|
+
const matched = await resolved.surface.store.findByCandidateTreeSha(input.treeSha);
|
|
339
|
+
const runIds: string[] = matched === undefined ? [] : [matched.runId, ...matched.alsoMatching];
|
|
340
|
+
if (runIds.length === 0 && input.allowCurrentRun === true) {
|
|
341
|
+
const current = await resolved.surface.store.current(resolved.surface.worktreeKey);
|
|
342
|
+
if (current.ok && current.runId !== undefined) runIds.push(current.runId);
|
|
343
|
+
}
|
|
344
|
+
let fallback: RunSpanRow | undefined;
|
|
345
|
+
for (const runId of runIds) {
|
|
346
|
+
const read = await resolved.surface.store.read(runId);
|
|
347
|
+
if (!read.ok) continue;
|
|
348
|
+
const startedAt = read.events[0]?.at;
|
|
349
|
+
const endedAt = read.events[read.events.length - 1]?.at;
|
|
350
|
+
if (startedAt === undefined || endedAt === undefined) continue;
|
|
351
|
+
const row: RunSpanRow = { runId, startedAt, endedAt };
|
|
352
|
+
if (input.preferStartedAt === undefined || startedAt === input.preferStartedAt) return row;
|
|
353
|
+
fallback ??= row;
|
|
354
|
+
}
|
|
355
|
+
return fallback;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* What the round binding means to someone deciding whether to act on the row.
|
|
360
|
+
*
|
|
361
|
+
* The `reviewed-tree` sentence is the one this ticket's readout exists for: it
|
|
362
|
+
* names WHERE the acceptance came from — the record's own verified evidence —
|
|
363
|
+
* and says in the same breath that the two trees differ, because the row prints
|
|
364
|
+
* both and an operator who reads "accepted" as "equal" would then wonder why.
|
|
365
|
+
*/
|
|
366
|
+
const ROUND_BINDING_ROWS: Readonly<Record<RunJournalRoundBinding, string>> = {
|
|
367
|
+
"record-tree": "the governing closed round binds the record's own candidate tree",
|
|
368
|
+
"reviewed-tree":
|
|
369
|
+
"the governing closed round binds a reviewed candidate above, accepted from this record's verified review-neutral projection; the two trees differ and neither is claimed to equal the other",
|
|
370
|
+
unbound: "no closed round binds the record's candidate tree or any reviewed candidate it accepts",
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* The one true sentence about authority, printed wherever a violation is.
|
|
375
|
+
*
|
|
376
|
+
* An operator reading a list of violated constraints under a line that says
|
|
377
|
+
* `verified` is being asked a question the row cannot answer by itself: does
|
|
378
|
+
* this stop anything? It does not. The journal is appended to by anything the
|
|
379
|
+
* owner can execute, so no admission, gate, or record decision reads it — and
|
|
380
|
+
* the only thing that can be blocked by it is this command, behind the local
|
|
381
|
+
* opt-in the operator typed themselves.
|
|
382
|
+
*/
|
|
383
|
+
export const RUN_JOURNAL_ADMISSION_ROW =
|
|
384
|
+
"none of the above blocks admission: the record's own evidence decides that, and no gate, admission, or record decision reads a journal. Only --require-run-journal blocks, and only this verify invocation.";
|
|
385
|
+
|
|
213
386
|
/**
|
|
214
387
|
* The row, rendered for a terminal. Every store-derived string goes through
|
|
215
388
|
* `oneLine` for the same reason the viewer's rows do: a run id or a constraint
|
|
@@ -222,9 +395,23 @@ export function runJournalRows(row: RunJournalRow): readonly string[] {
|
|
|
222
395
|
if (row.alsoMatching !== undefined && row.alsoMatching.length > 0) {
|
|
223
396
|
rows.push(` also matching: ${row.alsoMatching.map((id) => oneLine(id, 128)).join(", ")}`);
|
|
224
397
|
}
|
|
398
|
+
if (row.recordTreeSha !== undefined && row.reviewedCandidateTreeShas !== undefined) {
|
|
399
|
+
rows.push(` record candidate: ${oneLine(row.recordTreeSha, 64)}`);
|
|
400
|
+
rows.push(` reviewed candidate: ${row.reviewedCandidateTreeShas.map((tree) => oneLine(tree, 64)).join(", ")} (verified review-neutral projection)`);
|
|
401
|
+
}
|
|
402
|
+
if (row.roundBinding !== undefined) rows.push(` round binding: ${ROUND_BINDING_ROWS[row.roundBinding]}`);
|
|
225
403
|
rows.push(` missing: ${row.missing.length === 0 ? "(none)" : row.missing.map((entry) => oneLine(entry, 64)).join(", ")}`);
|
|
226
404
|
if (row.violations !== undefined && row.violations.length > 0) {
|
|
227
405
|
rows.push(` violations: ${row.violations.map((entry) => oneLine(entry, 64)).join(", ")}`);
|
|
406
|
+
// One indented line per violation, in the order they were raised, each
|
|
407
|
+
// saying why it exists and — where it is only another warning restated —
|
|
408
|
+
// which warning that is. Product-authored text, bounded like every other
|
|
409
|
+
// store-derived row for the same reason.
|
|
410
|
+
for (const explanation of row.explanations ?? []) {
|
|
411
|
+
const consequence = explanation.consequenceOf === undefined ? "" : `; a consequence of ${oneLine(explanation.consequenceOf, 64)}, not a separate mistake`;
|
|
412
|
+
rows.push(` ${oneLine(explanation.violation, 64)}: ${oneLine(explanation.because, 400)}${consequence}`);
|
|
413
|
+
}
|
|
414
|
+
rows.push(` admission: ${RUN_JOURNAL_ADMISSION_ROW}`);
|
|
228
415
|
}
|
|
229
416
|
return rows;
|
|
230
417
|
}
|
|
@@ -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
|
+
}
|