@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.
Files changed (47) hide show
  1. package/LICENSE +104 -201
  2. package/NOTICE +1 -1
  3. package/package.json +5 -5
  4. package/src/boundary.ts +84 -63
  5. package/src/check-snapshot.ts +167 -0
  6. package/src/command-activity.ts +75 -0
  7. package/src/commands/admit.ts +106 -0
  8. package/src/commands/check.ts +13 -2
  9. package/src/commands/emit-review-evidence.ts +38 -0
  10. package/src/commands/emit.ts +80 -12
  11. package/src/commands/gate.ts +64 -13
  12. package/src/commands/maintain.ts +1 -0
  13. package/src/commands/managed.ts +105 -34
  14. package/src/commands/prepare.ts +148 -22
  15. package/src/commands/record.ts +107 -3
  16. package/src/commands/resume.ts +53 -0
  17. package/src/commands/review-context.ts +19 -0
  18. package/src/commands/runs.ts +461 -30
  19. package/src/commands/save-context.ts +51 -0
  20. package/src/commands/submit-evidence.ts +46 -7
  21. package/src/commands/verify.ts +156 -10
  22. package/src/declared-checks.ts +50 -0
  23. package/src/index.ts +17 -1
  24. package/src/main.ts +56 -65
  25. package/src/ordinary-context.ts +83 -0
  26. package/src/provider-rails.ts +23 -704
  27. package/src/record-retention.ts +325 -0
  28. package/src/review-evidence.ts +284 -0
  29. package/src/run-archive-commands.ts +164 -0
  30. package/src/run-archive.ts +127 -0
  31. package/src/run-artifact-commands.ts +163 -0
  32. package/src/run-attachments.ts +112 -0
  33. package/src/run-export.ts +202 -0
  34. package/src/run-live.ts +102 -0
  35. package/src/run-projection.ts +327 -12
  36. package/src/run-server.ts +403 -149
  37. package/src/run-surface.ts +196 -9
  38. package/src/run-view-command.ts +77 -0
  39. package/src/run-view-html.ts +138 -0
  40. package/src/run-view-record.ts +119 -0
  41. package/src/run-view.ts +343 -0
  42. package/src/scoped-attempts.ts +86 -0
  43. package/src/scoped-candidate.ts +28 -0
  44. package/src/scoped-checks.ts +196 -0
  45. package/src/scoped-diagnostics.ts +124 -0
  46. package/src/scoped-observations.ts +51 -0
  47. package/src/snapshot-inventory-worker.ts +40 -0
@@ -0,0 +1,202 @@
1
+ import {
2
+ validateRunAttachments,
3
+ type RunAttachments,
4
+ } from "./run-attachments.ts";
5
+ /** Retained observability, projected and checked by the same product code. */
6
+ import {
7
+ canonicalize,
8
+ MAX_PORTABLE_RECORD_BYTES,
9
+ applySecretDiscipline,
10
+ evaluateRunJournal,
11
+ RUN_STORE_ID,
12
+ validateRunEvent,
13
+ runActionReconciliationError,
14
+ type RunEvent,
15
+ } from "@agent-delivery-harness/kernel";
16
+ import {
17
+ READOUT_LABELS,
18
+ projectCosts,
19
+ projectRunProgress,
20
+ readoutOf,
21
+ summarize,
22
+ } from "./run-projection.ts";
23
+
24
+ export function buildRunExport(input: {
25
+ readonly runId: string;
26
+ readonly events: readonly RunEvent[];
27
+ readonly rootDir?: string;
28
+ readonly refusedAppends?: readonly unknown[];
29
+ }) {
30
+ const v2 = input.events[0]?.version === "run-event/2";
31
+ return {
32
+ spec: v2
33
+ ? ("delivery-run-export/2" as const)
34
+ : ("delivery-run-export/1" as const),
35
+ labels: READOUT_LABELS,
36
+ runId: input.runId,
37
+ events: input.events,
38
+ summary: summarize(input.events),
39
+ costs: projectCosts(input.events),
40
+ readout: readoutOf(
41
+ input.events,
42
+ evaluateRunJournal(input.events),
43
+ input.rootDir,
44
+ ),
45
+ refusedAppends: input.refusedAppends ?? [],
46
+ // Exported progress is a historical projection at the last observation,
47
+ // never a claim about whether the producer is executing now.
48
+ ...(v2
49
+ ? {
50
+ progress: projectRunProgress(
51
+ input.events,
52
+ input.events.at(-1)?.at ?? "",
53
+ ),
54
+ }
55
+ : {}),
56
+ };
57
+ }
58
+
59
+ /**
60
+ * The readout members a stored export is CHECKED against.
61
+ *
62
+ * The per-violation sentences are derived from the same events by the same
63
+ * pass on every read, so an export never has to carry them and is never
64
+ * refused for lacking them: an archive written before the readout explained
65
+ * itself stays valid, and one carrying invented sentences gains nothing,
66
+ * because the value returned below is rebuilt from the recomputation either
67
+ * way. Compare what the events fix; recompute what they derive.
68
+ */
69
+ function comparableReadout(readout: object): Record<string, unknown> {
70
+ const comparable: Record<string, unknown> = { ...readout };
71
+ delete comparable["explanations"];
72
+ return comparable;
73
+ }
74
+
75
+ /**
76
+ * The summary members a stored export is CHECKED against.
77
+ *
78
+ * `phases` is derived from the same events by the same pass on every read, for
79
+ * exactly the reason the readout's sentences are: an archive written before the
80
+ * phase breakdown existed stays valid, and one carrying invented phases gains
81
+ * nothing, because the value returned below is rebuilt from the recomputation
82
+ * either way. Compare what the events fix; recompute what they derive.
83
+ */
84
+ function comparableSummary(summary: object): Record<string, unknown> {
85
+ const comparable: Record<string, unknown> = { ...summary };
86
+ delete comparable["phases"];
87
+ return comparable;
88
+ }
89
+
90
+ export type DeliveryRunExport = ReturnType<typeof buildRunExport> & {
91
+ readonly attachments?: RunAttachments;
92
+ };
93
+ export type RunExportParseResult =
94
+ | { readonly ok: true; readonly value: DeliveryRunExport }
95
+ | { readonly ok: false; readonly code: "run_export_invalid" };
96
+
97
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
98
+ value !== null && typeof value === "object" && !Array.isArray(value);
99
+
100
+ /** Validate observations and recompute totals; this never establishes admission. */
101
+ export function parseRunExport(text: string): RunExportParseResult {
102
+ const invalid = { ok: false, code: "run_export_invalid" } as const;
103
+ try {
104
+ if (Buffer.byteLength(text, "utf8") > MAX_PORTABLE_RECORD_BYTES)
105
+ return invalid;
106
+ const value: unknown = JSON.parse(text);
107
+ if (
108
+ !isRecord(value) ||
109
+ !["delivery-run-export/1", "delivery-run-export/2"].includes(
110
+ String(value["spec"]),
111
+ ) ||
112
+ value["labels"] !== READOUT_LABELS ||
113
+ typeof value["runId"] !== "string" ||
114
+ value["runId"].length > 128 ||
115
+ !RUN_STORE_ID.test(value["runId"]) ||
116
+ !Array.isArray(value["events"]) ||
117
+ !Array.isArray(value["refusedAppends"]) ||
118
+ !isRecord(value["readout"])
119
+ )
120
+ return invalid;
121
+ const v2 = value["spec"] === "delivery-run-export/2";
122
+ const hasAttachments = Object.prototype.hasOwnProperty.call(
123
+ value,
124
+ "attachments",
125
+ );
126
+ if (hasAttachments && (!v2 || !applySecretDiscipline(value, new Set()).ok))
127
+ return invalid;
128
+ if (
129
+ Object.keys(value)
130
+ .filter((key) => key !== "attachments")
131
+ .sort()
132
+ .join(",") !==
133
+ (v2
134
+ ? "costs,events,labels,progress,readout,refusedAppends,runId,spec,summary"
135
+ : "costs,events,labels,readout,refusedAppends,runId,spec,summary")
136
+ )
137
+ return invalid;
138
+ const prefix: RunEvent[] = [];
139
+ for (const [index, event] of value["events"].entries()) {
140
+ if (
141
+ !validateRunEvent(event).ok ||
142
+ !isRecord(event) ||
143
+ event["runId"] !== value["runId"] ||
144
+ event["seq"] !== index + 1 ||
145
+ event["version"] !== (v2 ? "run-event/2" : "run-event/1")
146
+ )
147
+ return invalid;
148
+ const validated = event as unknown as RunEvent;
149
+ if (runActionReconciliationError(prefix, validated)) return invalid;
150
+ prefix.push(validated);
151
+ }
152
+ if (
153
+ hasAttachments &&
154
+ !validateRunAttachments(
155
+ value["attachments"],
156
+ value["events"] as RunEvent[],
157
+ )
158
+ )
159
+ return invalid;
160
+ const expected = buildRunExport({
161
+ runId: value["runId"],
162
+ events: value["events"] as RunEvent[],
163
+ });
164
+ if (
165
+ expected.spec !== value["spec"] ||
166
+ canonicalize(value["progress"] ?? null) !==
167
+ canonicalize(expected.progress ?? null)
168
+ )
169
+ return invalid;
170
+ if (
171
+ !isRecord(value["summary"]) ||
172
+ canonicalize(comparableSummary(value["summary"])) !== canonicalize(comparableSummary(expected.summary)) ||
173
+ canonicalize(value["costs"]) !== canonicalize(expected.costs)
174
+ )
175
+ return invalid;
176
+ // The optional note describes config presence in the exporting workspace;
177
+ // it cannot be recomputed in a different checkout and grants no authority.
178
+ const { note, ...readout } = value["readout"];
179
+ if (
180
+ (note !== undefined && typeof note !== "string") ||
181
+ canonicalize(comparableReadout(readout)) !==
182
+ canonicalize(comparableReadout(expected.readout))
183
+ )
184
+ return invalid;
185
+ return {
186
+ ok: true,
187
+ value: {
188
+ ...expected,
189
+ ...(hasAttachments
190
+ ? { attachments: value["attachments"] as RunAttachments }
191
+ : {}),
192
+ refusedAppends: value["refusedAppends"],
193
+ readout: {
194
+ ...expected.readout,
195
+ ...(note === undefined ? {} : { note }),
196
+ },
197
+ },
198
+ };
199
+ } catch {
200
+ return invalid;
201
+ }
202
+ }
@@ -0,0 +1,102 @@
1
+ /** Fixed, hash-authorized enhancement. No executor content enters this script. */
2
+ export const RUN_LIVE_SCRIPT = String.raw`(() => {
3
+ const root = document.querySelector('main');
4
+ const toggle = document.querySelector('[data-live-toggle]');
5
+ const status = document.querySelector('[data-live-status]');
6
+ if (!root || !toggle || !status) return;
7
+ let paused = false;
8
+ let timer;
9
+ let interactingUntil = 0;
10
+ const interval = Number(root.dataset.pollSeconds) * 1000;
11
+ const interact = () => { interactingUntil = Date.now() + 1500; };
12
+ for (const event of ['pointerdown', 'keydown', 'wheel', 'touchstart']) {
13
+ document.addEventListener(event, interact, { passive: true });
14
+ }
15
+ toggle.addEventListener('click', event => {
16
+ event.preventDefault();
17
+ paused = !paused;
18
+ toggle.textContent = paused ? 'Resume live updates' : 'Pause updates';
19
+ status.textContent = paused ? 'Updates paused' : 'Live';
20
+ });
21
+ // Preserve existing nodes wherever possible, including focused controls and
22
+ // native disclosure state. Stable keys keep newly inserted cards from taking
23
+ // the identity of an earlier card the operator is reading.
24
+ const key = node => node.nodeType === 1 ? node.id || node.getAttribute('data-key') : null;
25
+ function patch(current, next) {
26
+ if (current.nodeType !== next.nodeType || current.nodeName !== next.nodeName) {
27
+ current.replaceWith(next.cloneNode(true));
28
+ return;
29
+ }
30
+ if (current.nodeType !== 1) {
31
+ if (current.nodeValue !== next.nodeValue) current.nodeValue = next.nodeValue;
32
+ return;
33
+ }
34
+ if (current.hasAttribute('data-live-controls')) return;
35
+ for (const attr of [...current.attributes]) {
36
+ if (current.tagName === 'DETAILS' && attr.name === 'open') continue;
37
+ if (!next.hasAttribute(attr.name)) current.removeAttribute(attr.name);
38
+ }
39
+ for (const attr of next.attributes) {
40
+ if (current.tagName !== 'DETAILS' || attr.name !== 'open') current.setAttribute(attr.name, attr.value);
41
+ }
42
+ const children = [...current.childNodes];
43
+ const used = new Set();
44
+ let cursor = current.firstChild;
45
+ for (const incoming of next.childNodes) {
46
+ const incomingKey = key(incoming);
47
+ const existing = incomingKey
48
+ ? children.find(child => !used.has(child) && key(child) === incomingKey)
49
+ : children.find(child => !used.has(child) && !key(child) && child.nodeType === incoming.nodeType && child.nodeName === incoming.nodeName);
50
+ if (existing) {
51
+ used.add(existing);
52
+ if (existing !== cursor) current.insertBefore(existing, cursor);
53
+ patch(existing, incoming);
54
+ cursor = existing.nextSibling;
55
+ } else {
56
+ const added = incoming.cloneNode(true);
57
+ current.insertBefore(added, cursor);
58
+ }
59
+ }
60
+ for (const child of children) if (!used.has(child)) child.remove();
61
+ }
62
+ const busy = () => Date.now() < interactingUntil || !window.getSelection()?.isCollapsed;
63
+ async function poll() {
64
+ if (paused || document.hidden || busy()) {
65
+ timer = setTimeout(poll, interval);
66
+ return;
67
+ }
68
+ try {
69
+ const response = await fetch(location.pathname, { cache: 'no-store', signal: AbortSignal.timeout(10000) });
70
+ if (!response.ok) throw Error('read');
71
+ const page = new DOMParser().parseFromString(await response.text(), 'text/html');
72
+ const next = page.querySelector('main');
73
+ if (!next) throw Error('read');
74
+ if (paused || document.hidden || busy()) { timer = setTimeout(poll, interval); return; }
75
+ const focused = document.activeElement;
76
+ if (focused && focused !== document.body && !focused.closest('[data-live-controls]')) {
77
+ const context = focused.closest('[id], [data-key]');
78
+ const replacement = context && [...next.querySelectorAll('[id], [data-key]')].find(element => key(element) === key(context));
79
+ // A completed attempt may move into collapsed history. Keep the current
80
+ // reading snapshot until focus leaves it, rather than removing the
81
+ // focused control or presenting a stale duplicate as current work.
82
+ if (!replacement || context.closest('section')?.id !== replacement.closest('section')?.id) {
83
+ status.textContent = 'New update ready · Finish reading to apply';
84
+ timer = setTimeout(poll, interval);
85
+ return;
86
+ }
87
+ }
88
+ const anchor = [...root.querySelectorAll('[id], [data-key]')].find(element => element.getBoundingClientRect().top >= 0 && element.getBoundingClientRect().top < window.innerHeight && element.getBoundingClientRect().height > 0);
89
+ const offset = anchor?.getBoundingClientRect().top;
90
+ const scroll = window.scrollY;
91
+ patch(root, next);
92
+ if (focused?.isConnected && document.activeElement !== focused) focused.focus({ preventScroll: true });
93
+ if (anchor?.isConnected && offset !== undefined) window.scrollBy(0, anchor.getBoundingClientRect().top - offset);
94
+ else window.scrollTo(0, scroll);
95
+ status.textContent = next.dataset.live === 'true' ? 'Live' : 'Saved observations';
96
+ if (next.dataset.live !== 'true') { toggle.hidden = true; return; }
97
+ } catch { status.textContent = 'Update unavailable · Retrying'; }
98
+ timer = setTimeout(poll, interval);
99
+ }
100
+ timer = setTimeout(poll, interval);
101
+ window.addEventListener('pagehide', () => clearTimeout(timer), { once: true });
102
+ })();`;