@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/commands/record.ts
CHANGED
|
@@ -13,21 +13,64 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import {
|
|
15
15
|
buildDeliveryRecord,
|
|
16
|
+
candidateTreeEvidenceReader,
|
|
17
|
+
computePreparationFingerprint, capturePortableEvidenceContext, repositoryEvidenceReader, capturePortableVerificationInputs, verifyDeliveryRecord,
|
|
16
18
|
deliveryRecordBytes,
|
|
17
19
|
deliveryRecordPathFor,
|
|
18
20
|
discoverRecords,
|
|
19
21
|
type EvidenceRecord,
|
|
22
|
+
readCompiledRepositoryPolicy,
|
|
20
23
|
} from "@agent-delivery-harness/kernel";
|
|
21
24
|
import path from "node:path";
|
|
22
25
|
import { commandBlocker } from "../boundary.ts";
|
|
23
26
|
import type { CommandContext, CommandDescriptor, CommandResult } from "../boundary.ts";
|
|
27
|
+
import { oneLine, resolveRunSpan } from "../run-surface.ts";
|
|
28
|
+
import { applyDeliveryRecordRetention } from "../record-retention.ts";
|
|
24
29
|
import { runProviderBackedAdmission } from "./gate.ts";
|
|
25
30
|
|
|
31
|
+
interface RetentionOptions {
|
|
32
|
+
readonly scope: string;
|
|
33
|
+
readonly keepSuperseded: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseRetentionOptions(args: readonly string[]): RetentionOptions | undefined | string {
|
|
37
|
+
if (args.length === 0) return undefined;
|
|
38
|
+
let scope: string | undefined;
|
|
39
|
+
let keepSuperseded: number | undefined;
|
|
40
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
41
|
+
const flag = args[index];
|
|
42
|
+
const value = args[index + 1];
|
|
43
|
+
if (value === undefined) return `${flag ?? "record option"} requires a value`;
|
|
44
|
+
if (flag === "--retention-scope" && scope === undefined) scope = value;
|
|
45
|
+
else if (flag === "--keep-superseded" && keepSuperseded === undefined) {
|
|
46
|
+
if (!/^\d+$/.test(value)) return "--keep-superseded must be an integer from 0 through 100";
|
|
47
|
+
keepSuperseded = Number(value);
|
|
48
|
+
} else return `${oneLine(flag ?? "record option", 64)} is not a valid record retention option`;
|
|
49
|
+
}
|
|
50
|
+
if (scope === undefined || keepSuperseded === undefined) {
|
|
51
|
+
return "--retention-scope and --keep-superseded must be supplied together";
|
|
52
|
+
}
|
|
53
|
+
if (!Number.isSafeInteger(keepSuperseded) || keepSuperseded > 100) {
|
|
54
|
+
return "--keep-superseded must be an integer from 0 through 100";
|
|
55
|
+
}
|
|
56
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(scope)) {
|
|
57
|
+
return "--retention-scope must be a plain 1-128 character delivery key";
|
|
58
|
+
}
|
|
59
|
+
return { scope, keepSuperseded };
|
|
60
|
+
}
|
|
61
|
+
|
|
26
62
|
export const recordCommand: CommandDescriptor = {
|
|
27
63
|
name: "record",
|
|
28
64
|
sourceId: "delivery-harness.cli.record",
|
|
29
65
|
summary: "Write the tracked delivery record for an admitted gate.",
|
|
66
|
+
usage: "Usage: delivery-harness record [--retention-scope <delivery-key> --keep-superseded <0-100>]\nRetention is opt-in; run gate first if a waiver is needed.",
|
|
30
67
|
async run(context: CommandContext): Promise<CommandResult> {
|
|
68
|
+
const observedAt = `${new Date().toISOString().slice(0, 19)}Z`;
|
|
69
|
+
// Arguments before anything is wired, admitted, or written.
|
|
70
|
+
const retention = parseRetentionOptions(context.args);
|
|
71
|
+
if (typeof retention === "string") {
|
|
72
|
+
return { kind: "usage", message: `${retention}.\n${recordCommand.usage}` };
|
|
73
|
+
}
|
|
31
74
|
const wiring = await context.wire();
|
|
32
75
|
|
|
33
76
|
// The gate is run without a prompt: `record` is not the waiver surface. If a
|
|
@@ -79,15 +122,76 @@ export const recordCommand: CommandDescriptor = {
|
|
|
79
122
|
evidenceRecords.push(...discovery.records);
|
|
80
123
|
}
|
|
81
124
|
|
|
82
|
-
const
|
|
125
|
+
const evidenceReader = repositoryEvidenceReader(context.rootDir, context.artifacts);
|
|
126
|
+
const evidenceContext = await capturePortableEvidenceContext(context.config, evidenceReader,
|
|
127
|
+
await computePreparationFingerprint(context.rootDir, context.config));
|
|
128
|
+
const compiledPolicy = context.policyBinding?.compiledPolicy ??
|
|
129
|
+
await readCompiledRepositoryPolicy(await candidateTreeEvidenceReader(context.rootDir, recheck.candidate.treeSha));
|
|
130
|
+
// The delivery's own span, read off the run journal rather than off a
|
|
131
|
+
// clock: `startedAt` is the journal's first instant and `endedAt` the last
|
|
132
|
+
// it had reached by this write, which is the time this delivery had spent
|
|
133
|
+
// when it recorded. A delivery with no journal records exactly as it always
|
|
134
|
+
// did — the member is simply absent — because the record is evidence about
|
|
135
|
+
// a candidate and this is a cycle-time reading beside it, never a condition
|
|
136
|
+
// on writing one.
|
|
137
|
+
const runSpan = await resolveRunSpan({ cwd: context.rootDir, treeSha: recheck.candidate.treeSha, allowCurrentRun: true });
|
|
138
|
+
const built = buildDeliveryRecord({ config: context.config, decision, evidenceRecords, context: evidenceContext,
|
|
139
|
+
...(runSpan === undefined ? {} : { runSpan: { startedAt: runSpan.startedAt, endedAt: runSpan.endedAt } }),
|
|
140
|
+
...(compiledPolicy === null ? {} : { compiledPolicy, observedAt }) });
|
|
83
141
|
if (!built.ok) {
|
|
84
142
|
return { kind: "blocked", blockers: [...built.blockers] };
|
|
85
143
|
}
|
|
86
144
|
|
|
145
|
+
const verificationInputs = await capturePortableVerificationInputs(context.rootDir, context.config, recheck.candidate, built.record);
|
|
146
|
+
const checked = verifyDeliveryRecord(context.config, built.record,
|
|
147
|
+
{ deliverableDigest: recheck.candidate.deliverable.digest, identityToken: recheck.candidate.deliverable.identity }, recheck.candidate.base,
|
|
148
|
+
{ ...verificationInputs, observedAt, liveResults: admission.observedLiveResults ?? [], executionContext: context.classifyContext() });
|
|
149
|
+
if (!checked.ok) return { kind: "blocked", blockers: [...checked.blockers] };
|
|
87
150
|
const relativePath = deliveryRecordPathFor(context.config, decision.candidate.deliverable.digest);
|
|
88
151
|
const absolutePath = path.join(context.rootDir, relativePath);
|
|
89
|
-
|
|
152
|
+
const bytes = deliveryRecordBytes(built.record);
|
|
153
|
+
if (retention === undefined) {
|
|
154
|
+
await context.artifacts.writeTextFile(absolutePath, bytes);
|
|
155
|
+
return { kind: "ok", summary: `recorded ${relativePath}` };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const retained = await applyDeliveryRecordRetention({
|
|
159
|
+
rootDir: context.rootDir,
|
|
160
|
+
storageNamespace: context.config.storageNamespace,
|
|
161
|
+
scope: retention.scope,
|
|
162
|
+
keepSuperseded: retention.keepSuperseded,
|
|
163
|
+
recordBasePath: context.config.deliveryRecordPath,
|
|
164
|
+
current: {
|
|
165
|
+
relativePath,
|
|
166
|
+
deliverableDigest: decision.candidate.deliverable.digest,
|
|
167
|
+
bytes,
|
|
168
|
+
},
|
|
169
|
+
writeCurrent: () => context.artifacts.writeTextFile(absolutePath, bytes),
|
|
170
|
+
});
|
|
171
|
+
if (!retained.ok) {
|
|
172
|
+
return {
|
|
173
|
+
kind: "blocked",
|
|
174
|
+
blockers: [
|
|
175
|
+
commandBlocker({
|
|
176
|
+
code: retained.code,
|
|
177
|
+
sourceId: "delivery-harness.cli.record",
|
|
178
|
+
summary: "Tracked delivery-record retention did not complete.",
|
|
179
|
+
details: retained.detail,
|
|
180
|
+
remediations: [
|
|
181
|
+
{
|
|
182
|
+
id: "repair-record-retention",
|
|
183
|
+
kind: "manual_action",
|
|
184
|
+
summary: "Preserve the exact owned record bytes in Git, repair the named ownership conflict, then retry record with the same scope and bound.",
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
}),
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
90
191
|
|
|
91
|
-
return {
|
|
192
|
+
return {
|
|
193
|
+
kind: "ok",
|
|
194
|
+
summary: `recorded ${relativePath}${retained.pruned.length === 0 ? "" : `; pruned Git-preserved ${retained.pruned.join(", ")}`}`,
|
|
195
|
+
};
|
|
92
196
|
},
|
|
93
197
|
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { classifyCandidateDrift, digestCanonical, evaluatePreparationReceipt, runAdmission, type CandidateBinding } from "@agent-delivery-harness/kernel";
|
|
2
|
+
import { installedRelease, policyDigest, reconciliationActions, recoveryBlocker, recoveryRun } from "../ordinary-context.ts";
|
|
3
|
+
import type { CommandDescriptor } from "../boundary.ts";
|
|
4
|
+
|
|
5
|
+
const USAGE = "Usage: delivery-harness resume [--run <run-id>]";
|
|
6
|
+
|
|
7
|
+
export const resumeCommand: CommandDescriptor = {
|
|
8
|
+
name: "resume", sourceId: "delivery-harness.cli.resume",
|
|
9
|
+
summary: "Read saved ordinary context and recheck evidence without executing or replaying work.",
|
|
10
|
+
usage: USAGE,
|
|
11
|
+
async run(context) {
|
|
12
|
+
if (context.args.length !== 0 && (context.args.length !== 2 || context.args[0] !== "--run")) return { kind: "usage", message: USAGE };
|
|
13
|
+
const run = await recoveryRun(context.rootDir, context.args[1]);
|
|
14
|
+
if (!run.ok) return { kind: "blocked", blockers: [run.blocker] };
|
|
15
|
+
const saved = [...run.events].reverse().find(event => event.kind === "context.saved");
|
|
16
|
+
if (!saved) return { kind: "blocked", blockers: [recoveryBlocker("resume_context_missing", "This run has no saved ordinary context. Recover the contract from its work item and save it explicitly.")] };
|
|
17
|
+
const p = saved.payload;
|
|
18
|
+
const actions = reconciliationActions(run.events);
|
|
19
|
+
const blockers = actions.some(action => action.outcome === "unknown" || action.inconsistent)
|
|
20
|
+
? [recoveryBlocker("resume_action_unreconciled", "An external action has no consistent observed outcome. Reconcile its reference with host tools before any repeat.")] : [];
|
|
21
|
+
const wiring = await context.wire();
|
|
22
|
+
const capture = await wiring.captureCandidate();
|
|
23
|
+
const drift: string[] = [];
|
|
24
|
+
if (p["policyDigest"] !== policyDigest(context)) drift.push("policy_changed");
|
|
25
|
+
try { if (digestCanonical(p["release"]) !== digestCanonical(await installedRelease(context.rootDir))) drift.push("release_changed"); }
|
|
26
|
+
catch { drift.push("release_unreadable"); }
|
|
27
|
+
if (capture.ok) {
|
|
28
|
+
const binding = p["candidateBinding"] as Record<string, string>;
|
|
29
|
+
const expected: CandidateBinding = { treeSha: saved.candidateTreeSha!, deliverable: { digest: binding["deliverableDigest"]!, identity: binding["identity"]! },
|
|
30
|
+
base: { ref: binding["baseRef"]!, tipSha: binding["baseTipSha"]!, mergeBaseSha: binding["mergeBaseSha"]! }, workspaceId: binding["workspaceId"]! };
|
|
31
|
+
drift.push(...classifyCandidateDrift(expected, capture.candidate));
|
|
32
|
+
if (expected.deliverable.identity !== capture.candidate.deliverable.identity) drift.push("identity_changed");
|
|
33
|
+
if (expected.base.ref !== capture.candidate.base.ref) drift.push("base_ref_changed");
|
|
34
|
+
} else blockers.push(...capture.blockers);
|
|
35
|
+
// Reuse the ordinary sensors, with no prompt, live-result injection, or
|
|
36
|
+
// provider invocation. They read receipts/evidence; this journal grants nothing.
|
|
37
|
+
const preparation = capture.ok ? await evaluatePreparationReceipt(context.rootDir, { config: context.config, candidate: capture.candidate }, wiring.storageOptions) : undefined;
|
|
38
|
+
const admission = capture.ok ? await runAdmission({ rootDir: context.rootDir, config: context.config, context: context.classifyContext() },
|
|
39
|
+
{ captureCandidate: wiring.captureCandidate, projectActivation: wiring.projectActivation, ...wiring.storageOptions }) : undefined;
|
|
40
|
+
if (capture.ok && admission?.candidate && classifyCandidateDrift(capture.candidate, admission.candidate).length > 0) drift.push("candidate_observation_changed");
|
|
41
|
+
try { if (digestCanonical(p["release"]) !== digestCanonical(await installedRelease(context.rootDir)) && !drift.includes("release_changed")) drift.push("release_changed"); }
|
|
42
|
+
catch { if (!drift.includes("release_unreadable")) drift.push("release_unreadable"); }
|
|
43
|
+
if (admission) blockers.push(...admission.blockers);
|
|
44
|
+
// Raw-tree movement alone is reported, but existing sensors decide whether
|
|
45
|
+
// the change is neutral. No new identity or freshness rule is introduced.
|
|
46
|
+
if (drift.some(item => item !== "raw_tree_changed")) blockers.push(recoveryBlocker("resume_context_stale", "Saved candidate, policy, or release bindings changed; revalidate the contract and affected work before saving fresh context."));
|
|
47
|
+
const reuseAllowed = blockers.length === 0 && admission?.admitted === true;
|
|
48
|
+
context.write(JSON.stringify({ spec: "ordinary-resume/1", runId: run.runId,
|
|
49
|
+
observationOnly: true, automaticReplay: false, savedContext: p, actions, drift,
|
|
50
|
+
preparation: preparation ?? { prepared: false }, admission: admission ?? { admitted: false }, reuseAllowed }));
|
|
51
|
+
return blockers.length > 0 ? { kind: "blocked", blockers } : { kind: "ok" };
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -8,12 +8,19 @@
|
|
|
8
8
|
import { evaluatePreparationReceipt } from "@agent-delivery-harness/kernel";
|
|
9
9
|
import { commandBlocker } from "../boundary.ts";
|
|
10
10
|
import type { CommandContext, CommandDescriptor, CommandResult } from "../boundary.ts";
|
|
11
|
+
import { buildReviewContext, OutcomeError } from "../review-evidence.ts";
|
|
12
|
+
|
|
13
|
+
const USAGE = "Usage: delivery-harness review-context [--json]";
|
|
11
14
|
|
|
12
15
|
export const reviewContextCommand: CommandDescriptor = {
|
|
13
16
|
name: "review-context",
|
|
14
17
|
sourceId: "delivery-harness.cli.review-context",
|
|
15
18
|
summary: "Show the reviewable-change context for the prepared candidate.",
|
|
19
|
+
usage: USAGE,
|
|
16
20
|
async run(context: CommandContext): Promise<CommandResult> {
|
|
21
|
+
if (context.args.length > 0 && !(context.args.length === 1 && context.args[0] === "--json")) {
|
|
22
|
+
return { kind: "usage", message: `review-context accepts only --json.\n${USAGE}` };
|
|
23
|
+
}
|
|
17
24
|
const wiring = await context.wire();
|
|
18
25
|
const capture = await wiring.captureCandidate();
|
|
19
26
|
if (!capture.ok) {
|
|
@@ -49,6 +56,18 @@ export const reviewContextCommand: CommandDescriptor = {
|
|
|
49
56
|
return { kind: "blocked", blockers };
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
if (context.args[0] === "--json") {
|
|
60
|
+
try {
|
|
61
|
+
const document = await buildReviewContext(context.rootDir, context.config, capture.candidate, evaluation.receipt);
|
|
62
|
+
return { kind: "ok", summary: JSON.stringify(document, null, 2) };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (!(error instanceof OutcomeError)) throw error;
|
|
65
|
+
return { kind: "blocked", blockers: [commandBlocker({
|
|
66
|
+
code: "review_context_unavailable", sourceId: "delivery-harness.cli.review-context", summary: error.message,
|
|
67
|
+
remediations: [{ id: "restore-review-inputs", kind: "manual_action", summary: "Restore the installed workflow release and compiled policy charters before acquiring review." }],
|
|
68
|
+
})] };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
52
71
|
const projection = await wiring.projectActivation(capture.candidate);
|
|
53
72
|
const active = projection.relevantLineCount >= context.config.activationThreshold || projection.hasRelevantBinaryChange;
|
|
54
73
|
return {
|