@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
|
@@ -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
|
+
}
|
package/src/run-live.ts
ADDED
|
@@ -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
|
+
})();`;
|