@agent-delivery-harness/cli 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/boundary.ts +16 -5
- package/src/check-snapshot.ts +264 -35
- package/src/command-activity.ts +15 -1
- package/src/commands/admit.ts +5 -4
- package/src/commands/emit-review-evidence.ts +1 -0
- package/src/commands/gate.ts +59 -24
- package/src/commands/prepare.ts +22 -13
- package/src/commands/record.ts +136 -122
- package/src/commands/resume.ts +3 -0
- package/src/commands/review-context.ts +1 -0
- package/src/commands/save-context.ts +9 -2
- package/src/commands/submit-evidence.ts +18 -4
- package/src/commands/verify.ts +5 -0
- package/src/declared-checks.ts +4 -4
- package/src/delivery-posture.ts +39 -0
- package/src/installed-data-resources.ts +57 -0
- package/src/installed-data-worker.ts +83 -0
- package/src/review-evidence.ts +1 -1
- package/src/scoped-attempts.ts +34 -8
- package/src/scoped-budget.ts +20 -0
- package/src/scoped-checks.ts +561 -59
- package/src/scoped-diagnostics.ts +82 -4
- package/src/scoped-installed-data.ts +8 -0
- package/src/snapshot-inventory-worker.ts +160 -18
package/src/commands/gate.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ScopedChecks } from "../scoped-checks.ts";
|
|
|
2
2
|
import { CheckSnapshotError } from "../check-snapshot.ts";
|
|
3
3
|
import { commandBlocker } from "../boundary.ts";
|
|
4
4
|
import { runDeclaredCheck } from "../declared-checks.ts";
|
|
5
|
+
import { refuseOverBudget } from "../scoped-budget.ts";
|
|
5
6
|
/**
|
|
6
7
|
* `gate` — evaluate the delivery gate and, under a TTY, offer a scoped waiver.
|
|
7
8
|
*
|
|
@@ -15,8 +16,11 @@ import { runDeclaredCheck } from "../declared-checks.ts";
|
|
|
15
16
|
*/
|
|
16
17
|
import { computeDeliverableIdentity, runAdmission, type AdmissionResult, type Blocker, type LiveProviderResult } from "@agent-delivery-harness/kernel";
|
|
17
18
|
import { CliInterruption, type CommandContext, type CommandDescriptor, type CommandResult } from "../boundary.ts";
|
|
19
|
+
import { focusedOutcome } from "../delivery-posture.ts";
|
|
18
20
|
import { oneLine } from "../run-surface.ts";
|
|
19
21
|
|
|
22
|
+
const scopedCheckBlocker = (error: CheckSnapshotError): Blocker => commandBlocker({ code: error.code, sourceId: "delivery-harness.cli.gate", summary: error.message, remediations: [{ id: "repair-scoped-check", kind: "manual_action", summary: "Repair the declared scoped check or its execution profile and run the gate again." }] });
|
|
23
|
+
|
|
20
24
|
/**
|
|
21
25
|
* Runs the ordinary admission first, invokes only configured providers that can
|
|
22
26
|
* answer the resulting missing-evidence/live-result blocks, then re-evaluates
|
|
@@ -28,7 +32,7 @@ async function providerAdmission(
|
|
|
28
32
|
options: { readonly allowPrompt: boolean; readonly includeInjectedLiveResults: boolean },
|
|
29
33
|
session?: ScopedChecks,
|
|
30
34
|
): Promise<AdmissionResult & { readonly observedLiveResults?: readonly LiveProviderResult[] }> {
|
|
31
|
-
const admit = async (input: Parameters<typeof runAdmission>[0], options: Parameters<typeof runAdmission>[1]) => runAdmission(input, { ...options, ...(session ? { scopedPlan: await session.plan(), readOutput: session.readOutput } : {}) });
|
|
35
|
+
const admit = async (input: Parameters<typeof runAdmission>[0], options: Parameters<typeof runAdmission>[1]) => runAdmission(input, { ...options, ...(session ? { scopedPlan: await session.plan(), readOutput: session.readOutput, resourceBindings: await session.resourceBindings() } : {}) });
|
|
32
36
|
if (session) {
|
|
33
37
|
await session.fenceNonReusable(context.config.providers.filter(p => p.check?.scope).map(p => p.id));
|
|
34
38
|
}
|
|
@@ -58,8 +62,11 @@ async function providerAdmission(
|
|
|
58
62
|
const attemptBlockers: Blocker[] = [];
|
|
59
63
|
let admission = await admit(input, admissionOptions);
|
|
60
64
|
await session?.explainReuse((admission.decision?.resolutions ?? []).flatMap(r => r.kind === "satisfied_evidence" ? [r.providerId] : []));
|
|
65
|
+
// Under `scopedExecution.failFast` the first failing scoped check ends the run of checks: nothing further starts.
|
|
66
|
+
const failFast = context.config.scopedExecution?.failFast === true;
|
|
67
|
+
let failedFast = false;
|
|
61
68
|
|
|
62
|
-
while (!admission.admitted && admission.decision !== undefined && admission.candidate !== undefined) {
|
|
69
|
+
while (!failedFast && !admission.admitted && admission.decision !== undefined && admission.candidate !== undefined) {
|
|
63
70
|
const requested = new Map<string, { obligationIds: string[]; requiresEvidence: boolean; needsLiveResult: boolean }>();
|
|
64
71
|
for (const resolution of admission.decision.resolutions) {
|
|
65
72
|
if (resolution.kind !== "blocked") continue;
|
|
@@ -70,7 +77,7 @@ async function providerAdmission(
|
|
|
70
77
|
if (finding.code !== missingCode || finding.providerId === undefined || attempted.has(finding.providerId)) continue;
|
|
71
78
|
const registration = context.config.providers.find((provider) => provider.id === finding.providerId);
|
|
72
79
|
if (registration === undefined || (registration.command === undefined && registration.check === undefined) || !obligation.providers.includes(registration.id)) continue;
|
|
73
|
-
if (registration.check !== undefined && (obligation.freshness !== "exact_candidate" || !obligation.acceptedPayloadSpecs.includes("checks.passed/1") || admission.decision.resolutions.some(resolution => resolution.kind !== "satisfied_evidence" && resolution.kind !== "not_applicable" && resolution.kind !== "waived" && context.config.obligations.find(entry => entry.id === resolution.obligationId)?.acceptedPayloadSpecs.
|
|
80
|
+
if (registration.check !== undefined && (obligation.freshness !== "exact_candidate" || !obligation.acceptedPayloadSpecs.includes("checks.passed/1") || admission.decision.resolutions.some(resolution => resolution.kind !== "satisfied_evidence" && resolution.kind !== "not_applicable" && resolution.kind !== "waived" && context.config.obligations.find(entry => entry.id === resolution.obligationId)?.acceptedPayloadSpecs.some(spec => spec === "review.green/1" || spec === "review.green/2")))) continue;
|
|
74
81
|
const entry = requested.get(registration.id) ?? { obligationIds: [], requiresEvidence: false, needsLiveResult: false };
|
|
75
82
|
entry.obligationIds.push(obligation.id);
|
|
76
83
|
entry.requiresEvidence ||= obligation.freshness === "exact_candidate";
|
|
@@ -79,6 +86,16 @@ async function providerAdmission(
|
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
// A pool runs every requested scoped check at once; a single worker keeps the one-at-a-time loop below.
|
|
90
|
+
const scoped = session !== undefined && session.concurrency > 1 ? [...requested].filter(([id]) => context.config.providers.find(p => p.id === id)!.check?.scope) : [];
|
|
91
|
+
if (session !== undefined && scoped.length > 0) {
|
|
92
|
+
for (const [id] of scoped) attempted.add(id);
|
|
93
|
+
const failures = await session.executeAll(scoped.map(([id, request]) => ({ provider: context.config.providers.find(p => p.id === id)!, obligationIds: request.obligationIds })));
|
|
94
|
+
attemptBlockers.push(...failures.map(scopedCheckBlocker));
|
|
95
|
+
failedFast = failFast && failures.length > 0;
|
|
96
|
+
admission = await admit({ ...input, ...(liveResults.length === 0 ? {} : { liveResults }) }, admissionOptions);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
82
99
|
const next = requested.entries().next().value as
|
|
83
100
|
| [string, { obligationIds: string[]; requiresEvidence: boolean; needsLiveResult: boolean }]
|
|
84
101
|
| undefined;
|
|
@@ -91,9 +108,13 @@ async function providerAdmission(
|
|
|
91
108
|
try { await session.execute(registration, request.obligationIds); }
|
|
92
109
|
catch (error) {
|
|
93
110
|
if (!(error instanceof CheckSnapshotError)) throw error;
|
|
94
|
-
attemptBlockers.push(
|
|
111
|
+
attemptBlockers.push(scopedCheckBlocker(error));
|
|
112
|
+
if (failFast) {
|
|
113
|
+
failedFast = true;
|
|
114
|
+
await session.cancelUnstarted([...requested.keys()].filter(id => !attempted.has(id) && context.config.providers.find(p => p.id === id)!.check?.scope), providerId);
|
|
115
|
+
}
|
|
95
116
|
}
|
|
96
|
-
} else attemptBlockers.push(...await runDeclaredCheck(context, registration, request.obligationIds, admission.candidate));
|
|
117
|
+
} else attemptBlockers.push(...await runDeclaredCheck(context, registration, request.obligationIds, admission.candidate, session ? { scopedPlan: await session.plan(), readOutput: session.readOutput, resourceBindings: await session.resourceBindings() } : {}));
|
|
97
118
|
admission = await admit({ ...input, ...(liveResults.length === 0 ? {} : { liveResults }) }, admissionOptions);
|
|
98
119
|
continue;
|
|
99
120
|
}
|
|
@@ -135,6 +156,8 @@ async function providerAdmission(
|
|
|
135
156
|
}
|
|
136
157
|
|
|
137
158
|
export async function runProviderBackedAdmission(context: CommandContext, options: { readonly allowPrompt: boolean; readonly includeInjectedLiveResults: boolean }): Promise<AdmissionResult & { readonly observedLiveResults?: readonly LiveProviderResult[] }> {
|
|
159
|
+
// Before any capture or check: a selection over its declared budget runs nothing.
|
|
160
|
+
refuseOverBudget(context.config);
|
|
138
161
|
let session: ScopedChecks | undefined;
|
|
139
162
|
try {
|
|
140
163
|
if (context.config.providers.some(p => p.check?.scope)) {
|
|
@@ -145,7 +168,35 @@ export async function runProviderBackedAdmission(context: CommandContext, option
|
|
|
145
168
|
} finally { await session?.cleanup(); }
|
|
146
169
|
}
|
|
147
170
|
|
|
171
|
+
/**
|
|
172
|
+
* The gate itself, with no posture. `admit` runs this directly, because it keeps
|
|
173
|
+
* its behaviour under either posture: only the `gate` command honours `focused`.
|
|
174
|
+
*/
|
|
175
|
+
export async function runGate(context: CommandContext): Promise<CommandResult> {
|
|
176
|
+
const result = await runProviderBackedAdmission(context, { allowPrompt: true, includeInjectedLiveResults: true });
|
|
177
|
+
|
|
178
|
+
if (result.admitted) {
|
|
179
|
+
const waiverNote =
|
|
180
|
+
result.waiver === "accepted"
|
|
181
|
+
? ` (waived: ${result.waivedObligationIds.join(", ")})`
|
|
182
|
+
: "";
|
|
183
|
+
const kinds = (result.decision?.resolutions ?? []).map((resolution) => `${resolution.obligationId}=${resolution.kind}`);
|
|
184
|
+
// This observation describes the admitted immutable tree. Failure to
|
|
185
|
+
// observe it cannot change admission; the run journal is not evidence.
|
|
186
|
+
let digest: string | undefined;
|
|
187
|
+
if (result.candidate !== undefined) {
|
|
188
|
+
try {
|
|
189
|
+
digest = await computeDeliverableIdentity({ rootDir: context.rootDir, treeSha: result.candidate.treeSha,
|
|
190
|
+
config: { ...context.config, computingIdentityVersion: "validation-tree/v1", reviewNeutral: context.config.recordNeutral } });
|
|
191
|
+
} catch { /* Best-effort observability, like the completion append. */ }
|
|
192
|
+
}
|
|
193
|
+
return { kind: "ok", summary: `admitted${waiverNote}: ${kinds.join(", ")}`, ...(digest === undefined ? {} : { digest }) };
|
|
194
|
+
}
|
|
195
|
+
return { kind: "blocked", blockers: [...result.blockers] };
|
|
196
|
+
}
|
|
197
|
+
|
|
148
198
|
export const gateCommand: CommandDescriptor = {
|
|
199
|
+
capturesCandidate: true,
|
|
149
200
|
name: "gate",
|
|
150
201
|
sourceId: "delivery-harness.cli.gate",
|
|
151
202
|
summary: "Evaluate the delivery gate for the current candidate.",
|
|
@@ -157,25 +208,9 @@ export const gateCommand: CommandDescriptor = {
|
|
|
157
208
|
if (unexpected !== undefined) {
|
|
158
209
|
return { kind: "usage", message: `gate takes no arguments, and ${oneLine(unexpected, 64)} is one.\n${gateCommand.usage}` };
|
|
159
210
|
}
|
|
160
|
-
const
|
|
211
|
+
const focused = await focusedOutcome(context);
|
|
212
|
+
if (focused !== undefined) return focused;
|
|
161
213
|
|
|
162
|
-
|
|
163
|
-
const waiverNote =
|
|
164
|
-
result.waiver === "accepted"
|
|
165
|
-
? ` (waived: ${result.waivedObligationIds.join(", ")})`
|
|
166
|
-
: "";
|
|
167
|
-
const kinds = (result.decision?.resolutions ?? []).map((resolution) => `${resolution.obligationId}=${resolution.kind}`);
|
|
168
|
-
// This observation describes the admitted immutable tree. Failure to
|
|
169
|
-
// observe it cannot change admission; the run journal is not evidence.
|
|
170
|
-
let digest: string | undefined;
|
|
171
|
-
if (result.candidate !== undefined) {
|
|
172
|
-
try {
|
|
173
|
-
digest = await computeDeliverableIdentity({ rootDir: context.rootDir, treeSha: result.candidate.treeSha,
|
|
174
|
-
config: { ...context.config, computingIdentityVersion: "validation-tree/v1", reviewNeutral: context.config.recordNeutral } });
|
|
175
|
-
} catch { /* Best-effort observability, like the completion append. */ }
|
|
176
|
-
}
|
|
177
|
-
return { kind: "ok", summary: `admitted${waiverNote}: ${kinds.join(", ")}`, ...(digest === undefined ? {} : { digest }) };
|
|
178
|
-
}
|
|
179
|
-
return { kind: "blocked", blockers: [...result.blockers] };
|
|
214
|
+
return runGate(context);
|
|
180
215
|
},
|
|
181
216
|
};
|
package/src/commands/prepare.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ScopedChecks } from "../scoped-checks.ts";
|
|
2
2
|
import { CheckSnapshotError } from "../check-snapshot.ts";
|
|
3
|
+
import { refuseOverBudget } from "../scoped-budget.ts";
|
|
3
4
|
/**
|
|
4
5
|
* `prepare` — capture the candidate and publish its preparation receipt.
|
|
5
6
|
*
|
|
@@ -24,17 +25,19 @@ import { CliInterruption, type CommandContext, type CommandDescriptor, type Comm
|
|
|
24
25
|
const USAGE = "Usage: delivery-harness prepare [--refresh-record-neutral]";
|
|
25
26
|
|
|
26
27
|
export const prepareCommand: CommandDescriptor = {
|
|
28
|
+
capturesCandidate: true,
|
|
27
29
|
name: "prepare",
|
|
28
30
|
sourceId: "delivery-harness.cli.prepare",
|
|
29
31
|
summary: "Run preparation checks; --refresh-record-neutral permits proven artifact-only receipt refresh.",
|
|
30
32
|
// The text prepare has always answered `--help` with, unchanged; the
|
|
31
33
|
// boundary is what prints it now, for every command rather than this one.
|
|
32
|
-
usage: `${USAGE}\nOrdinary prepare always runs mechanical checks. The refresh flag reuses prior success only when strict validation, policy, wiring and base are unchanged; otherwise it runs the checks.`,
|
|
34
|
+
usage: `${USAGE}\nOrdinary prepare always runs mechanical checks. The refresh flag reuses prior success only when strict validation, policy, wiring and base are unchanged; otherwise it runs the checks.\nScoped mechanical checks run the first declared check first and the rest cheapest first by recorded duration, one dependency setup at a time, stopping at the first failure.`,
|
|
33
35
|
async run(context: CommandContext): Promise<CommandResult> {
|
|
34
36
|
if (context.args.length > 1 || (context.args.length === 1 && context.args[0] !== "--refresh-record-neutral")) {
|
|
35
37
|
return { kind: "usage", message: USAGE };
|
|
36
38
|
}
|
|
37
39
|
if (context.config.providers.some(p => p.check?.scope) && !context.config.scopedExecution) throw new CheckSnapshotError("scoped_executor_required", "Scoped execution profiles are required before preparation commands may run.");
|
|
40
|
+
refuseOverBudget(context.config);
|
|
38
41
|
let scoped: ScopedChecks | undefined;
|
|
39
42
|
const refreshRecordNeutral = context.args[0] === "--refresh-record-neutral";
|
|
40
43
|
const wiring = await context.wire();
|
|
@@ -48,7 +51,16 @@ export const prepareCommand: CommandDescriptor = {
|
|
|
48
51
|
let reusableFingerprint: string | undefined;
|
|
49
52
|
let attemptId: string;
|
|
50
53
|
try {
|
|
51
|
-
|
|
54
|
+
capture = await wiring.captureCandidate();
|
|
55
|
+
if (capture.ok && refreshRecordNeutral) {
|
|
56
|
+
const previous = await evaluatePreparationReceipt(context.rootDir,
|
|
57
|
+
{ config: context.config, candidate: capture.candidate }, { ...wiring.storageOptions, allowValidationEquivalent: true });
|
|
58
|
+
if (previous.prepared && previous.receipt.attemptId !== undefined) {
|
|
59
|
+
reusableFingerprint = previous.receipt.preparationFingerprint;
|
|
60
|
+
retainedAttemptId = previous.receipt.attemptId;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (retainedAttemptId === undefined && context.config.scopedExecution?.repairCommands?.length) {
|
|
52
64
|
ownedAttemptId = await invalidatePreparationReceipt(context.rootDir, context.config, wiring.storageOptions);
|
|
53
65
|
const beforeRepair = await computePreparationFingerprint(context.rootDir, context.config, wiring.storageOptions);
|
|
54
66
|
for (const repair of context.config.scopedExecution.repairCommands) {
|
|
@@ -59,15 +71,7 @@ export const prepareCommand: CommandDescriptor = {
|
|
|
59
71
|
if (result.code !== 0) throw new CheckSnapshotError("preparation_repair_failed", `Source repair ${repair.id} failed; fix it and prepare again.`);
|
|
60
72
|
}
|
|
61
73
|
if (beforeRepair !== await computePreparationFingerprint(context.rootDir, context.config, wiring.storageOptions)) throw new CheckSnapshotError("preparation_candidate_changed", "Repair changed preparation wiring; reload the configuration and prepare again.");
|
|
62
|
-
|
|
63
|
-
capture = await wiring.captureCandidate();
|
|
64
|
-
if (capture.ok && refreshRecordNeutral) {
|
|
65
|
-
const previous = await evaluatePreparationReceipt(context.rootDir,
|
|
66
|
-
{ config: context.config, candidate: capture.candidate }, { ...wiring.storageOptions, allowValidationEquivalent: true });
|
|
67
|
-
if (previous.prepared && previous.receipt.attemptId !== undefined) {
|
|
68
|
-
reusableFingerprint = previous.receipt.preparationFingerprint;
|
|
69
|
-
retainedAttemptId = previous.receipt.attemptId;
|
|
70
|
-
}
|
|
74
|
+
capture = await wiring.captureCandidate();
|
|
71
75
|
}
|
|
72
76
|
} finally {
|
|
73
77
|
attemptId = retainedAttemptId ?? ownedAttemptId ?? await invalidatePreparationReceipt(context.rootDir, context.config, wiring.storageOptions);
|
|
@@ -86,6 +90,7 @@ export const prepareCommand: CommandDescriptor = {
|
|
|
86
90
|
attemptId = await invalidatePreparationReceipt(context.rootDir, context.config, wiring.storageOptions);
|
|
87
91
|
ownedAttemptId = attemptId;
|
|
88
92
|
retainedAttemptId = undefined;
|
|
93
|
+
if (context.config.scopedExecution?.repairCommands?.length) throw new CheckSnapshotError("preparation_candidate_changed", "Preparation wiring changed after repairs were skipped; reload the configuration and prepare again.");
|
|
89
94
|
}
|
|
90
95
|
let validationDigest = await computeDeliverableIdentity({ rootDir: context.rootDir, treeSha: capture.candidate.treeSha,
|
|
91
96
|
config: { ...context.config, computingIdentityVersion: "validation-tree/v1", reviewNeutral: context.config.recordNeutral } });
|
|
@@ -130,8 +135,12 @@ export const prepareCommand: CommandDescriptor = {
|
|
|
130
135
|
if (context.config.scopedExecution) {
|
|
131
136
|
capture = finalCapture;
|
|
132
137
|
validationDigest = await computeDeliverableIdentity({ rootDir: context.rootDir, treeSha: capture.candidate.treeSha, config: { ...context.config, computingIdentityVersion: "validation-tree/v1", reviewNeutral: context.config.recordNeutral } });
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
if (context.config.scopedExecution.mechanicalProviders.length === 0) {
|
|
139
|
+
await ScopedChecks.validatePreparation(context, capture.candidate);
|
|
140
|
+
} else {
|
|
141
|
+
scoped = await ScopedChecks.create(context, capture.candidate);
|
|
142
|
+
await scoped?.satisfyMechanical();
|
|
143
|
+
}
|
|
135
144
|
}
|
|
136
145
|
const published = await publishPreparationReceipt(
|
|
137
146
|
context.rootDir,
|
package/src/commands/record.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "@agent-delivery-harness/kernel";
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import { commandBlocker } from "../boundary.ts";
|
|
26
|
+
import { focusedOutcome } from "../delivery-posture.ts";
|
|
26
27
|
import type { CommandContext, CommandDescriptor, CommandResult } from "../boundary.ts";
|
|
27
28
|
import { oneLine, resolveRunSpan } from "../run-surface.ts";
|
|
28
29
|
import { applyDeliveryRecordRetention } from "../record-retention.ts";
|
|
@@ -59,139 +60,152 @@ function parseRetentionOptions(args: readonly string[]): RetentionOptions | unde
|
|
|
59
60
|
return { scope, keepSuperseded };
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The write itself, with no posture. `admit` runs this directly, because it
|
|
65
|
+
* keeps its behaviour under either posture: only the `record` command honours
|
|
66
|
+
* `focused`.
|
|
67
|
+
*/
|
|
68
|
+
export async function writeRecord(context: CommandContext, retention?: RetentionOptions): Promise<CommandResult> {
|
|
69
|
+
const observedAt = `${new Date().toISOString().slice(0, 19)}Z`;
|
|
70
|
+
const wiring = await context.wire();
|
|
71
|
+
|
|
72
|
+
// The gate is run without a prompt: `record` is not the waiver surface. If a
|
|
73
|
+
// waiver is needed, the operator runs `gate` first; here a non-admitting gate
|
|
74
|
+
// is simply a refusal.
|
|
75
|
+
const admission = await runProviderBackedAdmission(context, { allowPrompt: false, includeInjectedLiveResults: false });
|
|
76
|
+
if (!admission.admitted || admission.decision === undefined) {
|
|
77
|
+
return { kind: "blocked", blockers: [...admission.blockers] };
|
|
78
|
+
}
|
|
79
|
+
const decision = admission.decision;
|
|
80
|
+
|
|
81
|
+
// Refuse when the gate result no longer describes the current deliverable
|
|
82
|
+
// identity: a re-capture adjacent to the write.
|
|
83
|
+
const recheck = await wiring.captureCandidate();
|
|
84
|
+
if (!recheck.ok) {
|
|
85
|
+
return { kind: "blocked", blockers: [...recheck.blockers] };
|
|
86
|
+
}
|
|
87
|
+
if (recheck.candidate.deliverable.digest !== decision.candidate.deliverable.digest) {
|
|
88
|
+
return {
|
|
89
|
+
kind: "blocked",
|
|
90
|
+
blockers: [
|
|
91
|
+
commandBlocker({
|
|
92
|
+
code: "record_identity_changed",
|
|
93
|
+
sourceId: "delivery-harness.cli.record",
|
|
94
|
+
summary: "The deliverable identity changed after the gate; nothing was recorded.",
|
|
95
|
+
details: `gate ${decision.candidate.deliverable.digest} but current ${recheck.candidate.deliverable.digest}`,
|
|
96
|
+
remediations: [
|
|
97
|
+
{
|
|
98
|
+
id: "reprepare-and-record",
|
|
99
|
+
kind: "command",
|
|
100
|
+
command: ["delivery-harness", "prepare"],
|
|
101
|
+
summary: "Re-prepare the candidate and re-run the gate before recording.",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Gather the evidence records backing the decision so each evidence claim can
|
|
110
|
+
// be stamped with its manifest digest.
|
|
111
|
+
const evidenceRecords: EvidenceRecord[] = [];
|
|
112
|
+
for (const obligation of context.config.obligations) {
|
|
113
|
+
const discovery = await discoverRecords(context.rootDir, {
|
|
114
|
+
gateId: context.config.gateId,
|
|
115
|
+
obligationId: obligation.id,
|
|
116
|
+
...wiring.storageOptions,
|
|
117
|
+
});
|
|
118
|
+
evidenceRecords.push(...discovery.records);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const evidenceReader = repositoryEvidenceReader(context.rootDir, context.artifacts);
|
|
122
|
+
const evidenceContext = await capturePortableEvidenceContext(context.config, evidenceReader,
|
|
123
|
+
await computePreparationFingerprint(context.rootDir, context.config), recheck.candidate.deliverable.digest);
|
|
124
|
+
const compiledPolicy = context.policyBinding?.compiledPolicy ??
|
|
125
|
+
await readCompiledRepositoryPolicy(await candidateTreeEvidenceReader(context.rootDir, recheck.candidate.treeSha));
|
|
126
|
+
// The delivery's own span, read off the run journal rather than off a
|
|
127
|
+
// clock: `startedAt` is the journal's first instant and `endedAt` the last
|
|
128
|
+
// it had reached by this write, which is the time this delivery had spent
|
|
129
|
+
// when it recorded. A delivery with no journal records exactly as it always
|
|
130
|
+
// did — the member is simply absent — because the record is evidence about
|
|
131
|
+
// a candidate and this is a cycle-time reading beside it, never a condition
|
|
132
|
+
// on writing one.
|
|
133
|
+
const runSpan = await resolveRunSpan({ cwd: context.rootDir, treeSha: recheck.candidate.treeSha, allowCurrentRun: true });
|
|
134
|
+
const built = buildDeliveryRecord({ config: context.config, decision, evidenceRecords, context: evidenceContext,
|
|
135
|
+
...(runSpan === undefined ? {} : { runSpan: { startedAt: runSpan.startedAt, endedAt: runSpan.endedAt } }),
|
|
136
|
+
...(compiledPolicy === null ? {} : { compiledPolicy, observedAt }) });
|
|
137
|
+
if (!built.ok) {
|
|
138
|
+
return { kind: "blocked", blockers: [...built.blockers] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const verificationInputs = await capturePortableVerificationInputs(context.rootDir, context.config, recheck.candidate, built.record);
|
|
142
|
+
const checked = verifyDeliveryRecord(context.config, built.record,
|
|
143
|
+
{ deliverableDigest: recheck.candidate.deliverable.digest, identityToken: recheck.candidate.deliverable.identity }, recheck.candidate.base,
|
|
144
|
+
{ ...verificationInputs, observedAt, liveResults: admission.observedLiveResults ?? [], executionContext: context.classifyContext() });
|
|
145
|
+
if (!checked.ok) return { kind: "blocked", blockers: [...checked.blockers] };
|
|
146
|
+
const relativePath = deliveryRecordPathFor(context.config, decision.candidate.deliverable.digest);
|
|
147
|
+
const absolutePath = path.join(context.rootDir, relativePath);
|
|
148
|
+
const bytes = deliveryRecordBytes(built.record);
|
|
149
|
+
if (retention === undefined) {
|
|
150
|
+
await context.artifacts.writeTextFile(absolutePath, bytes);
|
|
151
|
+
return { kind: "ok", summary: `recorded ${relativePath}` };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const retained = await applyDeliveryRecordRetention({
|
|
155
|
+
rootDir: context.rootDir,
|
|
156
|
+
storageNamespace: context.config.storageNamespace,
|
|
157
|
+
scope: retention.scope,
|
|
158
|
+
keepSuperseded: retention.keepSuperseded,
|
|
159
|
+
recordBasePath: context.config.deliveryRecordPath,
|
|
160
|
+
current: {
|
|
161
|
+
relativePath,
|
|
162
|
+
deliverableDigest: decision.candidate.deliverable.digest,
|
|
163
|
+
bytes,
|
|
164
|
+
},
|
|
165
|
+
writeCurrent: () => context.artifacts.writeTextFile(absolutePath, bytes),
|
|
166
|
+
});
|
|
167
|
+
if (!retained.ok) {
|
|
168
|
+
return {
|
|
169
|
+
kind: "blocked",
|
|
170
|
+
blockers: [
|
|
171
|
+
commandBlocker({
|
|
172
|
+
code: retained.code,
|
|
173
|
+
sourceId: "delivery-harness.cli.record",
|
|
174
|
+
summary: "Tracked delivery-record retention did not complete.",
|
|
175
|
+
details: retained.detail,
|
|
176
|
+
remediations: [
|
|
177
|
+
{
|
|
178
|
+
id: "repair-record-retention",
|
|
179
|
+
kind: "manual_action",
|
|
180
|
+
summary: "Preserve the exact owned record bytes in Git, repair the named ownership conflict, then retry record with the same scope and bound.",
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
}),
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
kind: "ok",
|
|
190
|
+
summary: `recorded ${relativePath}${retained.pruned.length === 0 ? "" : `; pruned Git-preserved ${retained.pruned.join(", ")}`}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
62
194
|
export const recordCommand: CommandDescriptor = {
|
|
195
|
+
capturesCandidate: true,
|
|
63
196
|
name: "record",
|
|
64
197
|
sourceId: "delivery-harness.cli.record",
|
|
65
198
|
summary: "Write the tracked delivery record for an admitted gate.",
|
|
66
199
|
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.",
|
|
67
200
|
async run(context: CommandContext): Promise<CommandResult> {
|
|
68
|
-
const observedAt = `${new Date().toISOString().slice(0, 19)}Z`;
|
|
69
201
|
// Arguments before anything is wired, admitted, or written.
|
|
70
202
|
const retention = parseRetentionOptions(context.args);
|
|
71
203
|
if (typeof retention === "string") {
|
|
72
204
|
return { kind: "usage", message: `${retention}.\n${recordCommand.usage}` };
|
|
73
205
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
// The gate is run without a prompt: `record` is not the waiver surface. If a
|
|
77
|
-
// waiver is needed, the operator runs `gate` first; here a non-admitting gate
|
|
78
|
-
// is simply a refusal.
|
|
79
|
-
const admission = await runProviderBackedAdmission(context, { allowPrompt: false, includeInjectedLiveResults: false });
|
|
80
|
-
if (!admission.admitted || admission.decision === undefined) {
|
|
81
|
-
return { kind: "blocked", blockers: [...admission.blockers] };
|
|
82
|
-
}
|
|
83
|
-
const decision = admission.decision;
|
|
84
|
-
|
|
85
|
-
// Refuse when the gate result no longer describes the current deliverable
|
|
86
|
-
// identity: a re-capture adjacent to the write.
|
|
87
|
-
const recheck = await wiring.captureCandidate();
|
|
88
|
-
if (!recheck.ok) {
|
|
89
|
-
return { kind: "blocked", blockers: [...recheck.blockers] };
|
|
90
|
-
}
|
|
91
|
-
if (recheck.candidate.deliverable.digest !== decision.candidate.deliverable.digest) {
|
|
92
|
-
return {
|
|
93
|
-
kind: "blocked",
|
|
94
|
-
blockers: [
|
|
95
|
-
commandBlocker({
|
|
96
|
-
code: "record_identity_changed",
|
|
97
|
-
sourceId: "delivery-harness.cli.record",
|
|
98
|
-
summary: "The deliverable identity changed after the gate; nothing was recorded.",
|
|
99
|
-
details: `gate ${decision.candidate.deliverable.digest} but current ${recheck.candidate.deliverable.digest}`,
|
|
100
|
-
remediations: [
|
|
101
|
-
{
|
|
102
|
-
id: "reprepare-and-record",
|
|
103
|
-
kind: "command",
|
|
104
|
-
command: ["delivery-harness", "prepare"],
|
|
105
|
-
summary: "Re-prepare the candidate and re-run the gate before recording.",
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
|
-
}),
|
|
109
|
-
],
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Gather the evidence records backing the decision so each evidence claim can
|
|
114
|
-
// be stamped with its manifest digest.
|
|
115
|
-
const evidenceRecords: EvidenceRecord[] = [];
|
|
116
|
-
for (const obligation of context.config.obligations) {
|
|
117
|
-
const discovery = await discoverRecords(context.rootDir, {
|
|
118
|
-
gateId: context.config.gateId,
|
|
119
|
-
obligationId: obligation.id,
|
|
120
|
-
...wiring.storageOptions,
|
|
121
|
-
});
|
|
122
|
-
evidenceRecords.push(...discovery.records);
|
|
123
|
-
}
|
|
206
|
+
const focused = await focusedOutcome(context);
|
|
207
|
+
if (focused !== undefined) return focused;
|
|
124
208
|
|
|
125
|
-
|
|
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 }) });
|
|
141
|
-
if (!built.ok) {
|
|
142
|
-
return { kind: "blocked", blockers: [...built.blockers] };
|
|
143
|
-
}
|
|
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] };
|
|
150
|
-
const relativePath = deliveryRecordPathFor(context.config, decision.candidate.deliverable.digest);
|
|
151
|
-
const absolutePath = path.join(context.rootDir, relativePath);
|
|
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
|
-
}
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
kind: "ok",
|
|
194
|
-
summary: `recorded ${relativePath}${retained.pruned.length === 0 ? "" : `; pruned Git-preserved ${retained.pruned.join(", ")}`}`,
|
|
195
|
-
};
|
|
209
|
+
return writeRecord(context, retention);
|
|
196
210
|
},
|
|
197
211
|
};
|
package/src/commands/resume.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { classifyCandidateDrift, digestCanonical, evaluatePreparationReceipt, runAdmission, type CandidateBinding } from "@agent-delivery-harness/kernel";
|
|
2
2
|
import { installedRelease, policyDigest, reconciliationActions, recoveryBlocker, recoveryRun } from "../ordinary-context.ts";
|
|
3
|
+
import { focusedOutcome } from "../delivery-posture.ts";
|
|
3
4
|
import type { CommandDescriptor } from "../boundary.ts";
|
|
4
5
|
|
|
5
6
|
const USAGE = "Usage: delivery-harness resume [--run <run-id>]";
|
|
@@ -10,6 +11,8 @@ export const resumeCommand: CommandDescriptor = {
|
|
|
10
11
|
usage: USAGE,
|
|
11
12
|
async run(context) {
|
|
12
13
|
if (context.args.length !== 0 && (context.args.length !== 2 || context.args[0] !== "--run")) return { kind: "usage", message: USAGE };
|
|
14
|
+
const focused = await focusedOutcome(context);
|
|
15
|
+
if (focused !== undefined) return focused;
|
|
13
16
|
const run = await recoveryRun(context.rootDir, context.args[1]);
|
|
14
17
|
if (!run.ok) return { kind: "blocked", blockers: [run.blocker] };
|
|
15
18
|
const saved = [...run.events].reverse().find(event => event.kind === "context.saved");
|
|
@@ -13,6 +13,7 @@ import { buildReviewContext, OutcomeError } from "../review-evidence.ts";
|
|
|
13
13
|
const USAGE = "Usage: delivery-harness review-context [--json]";
|
|
14
14
|
|
|
15
15
|
export const reviewContextCommand: CommandDescriptor = {
|
|
16
|
+
capturesCandidate: true,
|
|
16
17
|
name: "review-context",
|
|
17
18
|
sourceId: "delivery-harness.cli.review-context",
|
|
18
19
|
summary: "Show the reviewable-change context for the prepared candidate.",
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
import { describeRunEventPayload, RUN_EVENT_SPEC, type RunEventPayloadMemberGrammar } from "@agent-delivery-harness/kernel";
|
|
1
2
|
import { buildRunEvent } from "../run-surface.ts";
|
|
2
3
|
import { installedRelease, ordinaryEventWriter, policyDigest, recoveryBlocker, recoveryRun, rejectionDetails } from "../ordinary-context.ts";
|
|
3
4
|
import type { CommandDescriptor } from "../boundary.ts";
|
|
4
5
|
|
|
5
6
|
const USAGE = "Usage: delivery-harness save-context --json '{\"contract\":{\"objective\":\"...\",\"acceptanceCriteria\":[\"...\"],\"finishLine\":\"merge-ready\"},\"stage\":\"work\"}'";
|
|
7
|
+
// Read from the store's own grammar, so the stated limits are the enforced ones.
|
|
8
|
+
const members = describeRunEventPayload("context.saved", RUN_EVENT_SPEC)!.members;
|
|
9
|
+
const bound = (name: string, member: RunEventPayloadMemberGrammar) => ` ${name}: ${member.constraint}${member.items ? `, each ${member.items.constraint}` : ""}`;
|
|
10
|
+
const BOUNDS = ["Bounds:",
|
|
11
|
+
...members.find(m => m.name === "contract")!.members!.map(m => bound(`contract.${m.name}`, m)),
|
|
12
|
+
bound("stage", members.find(m => m.name === "stage")!)].join("\n");
|
|
6
13
|
|
|
7
14
|
export const saveContextCommand: CommandDescriptor = {
|
|
8
15
|
name: "save-context", sourceId: "delivery-harness.cli.save-context",
|
|
9
16
|
summary: "Save a bounded delivery contract and stage observation in the current run.",
|
|
10
|
-
usage: USAGE
|
|
17
|
+
usage: `${USAGE}\n${BOUNDS}`,
|
|
11
18
|
async run(context) {
|
|
12
19
|
if (context.args.length !== 2 || context.args[0] !== "--json") return { kind: "usage", message: USAGE };
|
|
13
20
|
let input: { contract?: unknown; stage?: unknown };
|
|
@@ -44,7 +51,7 @@ export const saveContextCommand: CommandDescriptor = {
|
|
|
44
51
|
{ reuseExistingTimestamp: true },
|
|
45
52
|
);
|
|
46
53
|
if (!appended.ok) return { kind: "blocked", blockers: [recoveryBlocker("resume_context_invalid",
|
|
47
|
-
"Context was refused by the bounded run-event contract; inspect contract, stage, and secret-free inputs.",
|
|
54
|
+
"Context was refused by the bounded run-event contract; inspect contract, stage, and secret-free inputs. `save-context --help` lists every bound.",
|
|
48
55
|
`run ${run.runId}: ${rejectionDetails(appended.rejections)}`)] };
|
|
49
56
|
return { kind: "ok", summary: `saved ordinary context for ${run.runId}; stage is an observation, not evidence` };
|
|
50
57
|
},
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { submitManifest } from "@agent-delivery-harness/kernel";
|
|
11
11
|
import type { CommandContext, CommandDescriptor, CommandResult } from "../boundary.ts";
|
|
12
12
|
import { oneLine } from "../run-surface.ts";
|
|
13
|
+
import { ScopedChecks } from "../scoped-checks.ts";
|
|
13
14
|
|
|
14
15
|
function manifestPathsFrom(args: readonly string[]): string[] | undefined {
|
|
15
16
|
const paths: string[] = [];
|
|
@@ -44,6 +45,7 @@ function unknownFlagIn(args: readonly string[]): string | undefined {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export const submitEvidenceCommand: CommandDescriptor = {
|
|
48
|
+
capturesCandidate: true,
|
|
47
49
|
name: "submit-evidence",
|
|
48
50
|
sourceId: "delivery-harness.cli.submit-evidence",
|
|
49
51
|
summary: "Validate a provider manifest and publish its evidence records.",
|
|
@@ -68,10 +70,22 @@ export const submitEvidenceCommand: CommandDescriptor = {
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
const wiring = await context.wire();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
// Scoped check bindings are captured for every manifest, so a scoped
|
|
74
|
+
// provider needs the plan rebuilt for the current candidate and base, as
|
|
75
|
+
// the gate builds it. The kernel still refuses a plan that is not current.
|
|
76
|
+
let session: ScopedChecks | undefined;
|
|
77
|
+
let outcome: Awaited<ReturnType<typeof submitManifest>>;
|
|
78
|
+
try {
|
|
79
|
+
if (context.config.providers.some(p => p.check?.scope)) {
|
|
80
|
+
const capture = await wiring.captureCandidate();
|
|
81
|
+
if (capture.ok) session = await ScopedChecks.create(context, capture.candidate);
|
|
82
|
+
}
|
|
83
|
+
outcome = await submitManifest(
|
|
84
|
+
{ rootDir: context.rootDir, manifestPath, config: context.config },
|
|
85
|
+
{ captureCandidate: wiring.captureCandidate, artifacts: context.artifacts, ...wiring.storageOptions,
|
|
86
|
+
...(session ? { scopedPlan: await session.plan(), readOutput: session.readOutput } : {}) },
|
|
87
|
+
);
|
|
88
|
+
} finally { await session?.cleanup(); }
|
|
75
89
|
|
|
76
90
|
if (outcome.status === "accepted") {
|
|
77
91
|
const lines = outcome.records.map(
|
package/src/commands/verify.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
type RunJournalRow,
|
|
44
44
|
} from "@agent-delivery-harness/kernel";
|
|
45
45
|
import { commandBlocker } from "../boundary.ts";
|
|
46
|
+
import { focusedOutcome } from "../delivery-posture.ts";
|
|
46
47
|
import type { CommandContext, CommandDescriptor, CommandResult } from "../boundary.ts";
|
|
47
48
|
import { RUN_JOURNAL_ADMISSION_ROW, oneLine, resolveRunJournalRow, resolveRunSpan, runJournalRows } from "../run-surface.ts";
|
|
48
49
|
import { durationLabel } from "../run-projection.ts";
|
|
@@ -233,6 +234,7 @@ async function runSpanRows(
|
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
export const verifyCommand: CommandDescriptor = {
|
|
237
|
+
capturesCandidate: true,
|
|
236
238
|
name: "verify",
|
|
237
239
|
sourceId: "delivery-harness.cli.verify",
|
|
238
240
|
summary: "Verify the tracked delivery record against the current candidate.",
|
|
@@ -244,6 +246,9 @@ export const verifyCommand: CommandDescriptor = {
|
|
|
244
246
|
const parsedArgs = parseArgs(context.args);
|
|
245
247
|
if (!parsedArgs.ok) return { kind: "usage", message: parsedArgs.message };
|
|
246
248
|
|
|
249
|
+
const focused = await focusedOutcome(context);
|
|
250
|
+
if (focused !== undefined) return focused;
|
|
251
|
+
|
|
247
252
|
const wiring = await context.wire();
|
|
248
253
|
const capture = await wiring.captureCandidate();
|
|
249
254
|
if (!capture.ok) {
|