@agent-delivery-harness/cli 0.1.0 → 0.2.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 +8 -3
- package/src/boundary.ts +282 -5
- package/src/commands/emit.ts +351 -0
- package/src/commands/gate.ts +101 -17
- package/src/commands/maintain.ts +202 -0
- package/src/commands/managed.ts +477 -0
- package/src/commands/prepare.ts +8 -1
- package/src/commands/record.ts +2 -5
- package/src/commands/runs.ts +255 -0
- package/src/commands/verify.ts +150 -3
- package/src/index.ts +27 -4
- package/src/main.ts +23 -0
- package/src/provider-rails.ts +705 -0
- package/src/run-projection.ts +278 -0
- package/src/run-server.ts +660 -0
- package/src/run-surface.ts +261 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `managed` — the host-facing slice of the managed-delivery facade: the
|
|
3
|
+
* typed status/resume surface and the checkpoint operations an active host
|
|
4
|
+
* task drives (`status`, `next`, stage results, the trusted sensor, review
|
|
5
|
+
* reduction, admission, the tracked record, the finish line, and
|
|
6
|
+
* `explain-blocker`).
|
|
7
|
+
*
|
|
8
|
+
* WHAT IS DELIBERATELY NOT HERE. The operator confirmations — contract
|
|
9
|
+
* confirmation and takeover authorization — are served only by the facade
|
|
10
|
+
* module's binding-owned channel, outside the model-visible tool and shell
|
|
11
|
+
* surface; this command exposes no `confirm` operation at all, so a session
|
|
12
|
+
* cannot even name one here. Worktree creation stays with the host, and the
|
|
13
|
+
* command launches no agent process.
|
|
14
|
+
*
|
|
15
|
+
* The facade is resolved from the product namespace's pointer (written at
|
|
16
|
+
* delivery registration, under the common git directory — never a
|
|
17
|
+
* candidate-writable path), and the walking skeleton carries one delivery per
|
|
18
|
+
* repository: zero or several registered deliveries is a typed refusal.
|
|
19
|
+
*/
|
|
20
|
+
import { execFile } from "node:child_process";
|
|
21
|
+
import { readFile, readdir, realpath } from "node:fs/promises";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import {
|
|
24
|
+
FACADE_OPERATIONS,
|
|
25
|
+
compiledAdopterPolicyBindingDigest,
|
|
26
|
+
createManagedDeliveryFacade,
|
|
27
|
+
type CompiledAdopterPolicyBinding,
|
|
28
|
+
type ManagedDeliveryFacade,
|
|
29
|
+
} from "@agent-delivery-harness/kernel";
|
|
30
|
+
import { commandBlocker } from "../boundary.ts";
|
|
31
|
+
import type { CommandContext, CommandDescriptor, CommandResult } from "../boundary.ts";
|
|
32
|
+
|
|
33
|
+
const SOURCE_ID = "delivery-harness.cli.managed";
|
|
34
|
+
|
|
35
|
+
/** Every operation this command answers, in the order its usage lists them. */
|
|
36
|
+
const MANAGED_OPERATIONS: readonly string[] = Object.freeze([
|
|
37
|
+
"status",
|
|
38
|
+
"next",
|
|
39
|
+
"operations",
|
|
40
|
+
"blockers",
|
|
41
|
+
"explain-blocker",
|
|
42
|
+
"submit-plan",
|
|
43
|
+
"checkpoint",
|
|
44
|
+
"run-sensor",
|
|
45
|
+
"reduce-review",
|
|
46
|
+
"compound",
|
|
47
|
+
"admit",
|
|
48
|
+
"prepare-record",
|
|
49
|
+
"confirm-record",
|
|
50
|
+
"finish",
|
|
51
|
+
"propose-approval",
|
|
52
|
+
"request-cancellation",
|
|
53
|
+
"finalize-cancellation",
|
|
54
|
+
"recover",
|
|
55
|
+
"export",
|
|
56
|
+
"delete",
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const blocked = (code: string, summary: string, remediation: string): CommandResult => ({
|
|
60
|
+
kind: "blocked",
|
|
61
|
+
blockers: [
|
|
62
|
+
commandBlocker({
|
|
63
|
+
code,
|
|
64
|
+
sourceId: SOURCE_ID,
|
|
65
|
+
summary,
|
|
66
|
+
remediations: [{ id: `${code.replaceAll("_", "-")}-remediation`, kind: "manual_action", summary: remediation }],
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const gitCommonDir = (cwd: string): Promise<string | undefined> =>
|
|
72
|
+
new Promise((resolve) => {
|
|
73
|
+
execFile("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd, encoding: "utf8" }, (error, stdout) => {
|
|
74
|
+
resolve(error === null ? stdout.trim() : undefined);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function nowInstant(): string {
|
|
79
|
+
return `${new Date().toISOString().slice(0, 19)}Z`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const flag = (args: readonly string[], name: string): string | undefined => {
|
|
83
|
+
const index = args.indexOf(name);
|
|
84
|
+
return index === -1 ? undefined : args[index + 1];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
interface ResolvedManaged {
|
|
88
|
+
readonly facade: ManagedDeliveryFacade;
|
|
89
|
+
readonly deliveryId: string;
|
|
90
|
+
/**
|
|
91
|
+
* The invoking task's fence, derived from the worktree this command runs
|
|
92
|
+
* in — never a caller-supplied flag. Fresh-worktree-only resume makes
|
|
93
|
+
* worktree and fence one-to-one, so a command invoked from the currently
|
|
94
|
+
* bound worktree carries the current fence, and a command invoked from a
|
|
95
|
+
* superseded or foreign worktree carries none: the fence-carrying
|
|
96
|
+
* operations then fail closed instead of recording a stale task's output.
|
|
97
|
+
*/
|
|
98
|
+
readonly fence: number | undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolves the delivery this invocation addresses.
|
|
103
|
+
*
|
|
104
|
+
* `requested` exists for the retention operations alone. The skeleton's rule —
|
|
105
|
+
* one delivery in flight per repository — is right for every checkpoint
|
|
106
|
+
* operation, but it makes the retention lane unusable the moment a repository
|
|
107
|
+
* has finished more than one delivery: `active[0] ?? deliveries.at(-1)` then
|
|
108
|
+
* addresses only the newest, and every earlier terminal delivery's durable
|
|
109
|
+
* detail becomes unreachable from the CLI even though the facade can export and
|
|
110
|
+
* delete it. Naming one explicitly is the whole remedy, and it is deliberately
|
|
111
|
+
* NOT offered to the checkpoint operations: those bind an invocation fence
|
|
112
|
+
* derived from the worktree, and a delivery named by flag would not be the one
|
|
113
|
+
* that worktree is bound to.
|
|
114
|
+
*/
|
|
115
|
+
async function resolveManaged(context: CommandContext, requested?: string): Promise<ResolvedManaged | CommandResult> {
|
|
116
|
+
const common = await gitCommonDir(context.rootDir);
|
|
117
|
+
if (common === undefined) {
|
|
118
|
+
return blocked("not_a_repository", "The working directory is not a git repository.", "Run from the delivery worktree.");
|
|
119
|
+
}
|
|
120
|
+
const namespace = path.join(common, "managed-delivery");
|
|
121
|
+
let pointer: { installationPath: string; receiptDir: string; hostVersion: string; policyBindingDigest: string };
|
|
122
|
+
try {
|
|
123
|
+
pointer = JSON.parse(await readFile(path.join(namespace, "facade.json"), "utf8")) as typeof pointer;
|
|
124
|
+
} catch {
|
|
125
|
+
return blocked(
|
|
126
|
+
"no_managed_delivery",
|
|
127
|
+
"No managed delivery is registered for this repository.",
|
|
128
|
+
"Register a delivery through the facade's contract handoff first.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
let deliveries: string[];
|
|
132
|
+
try {
|
|
133
|
+
deliveries = (await readdir(path.join(namespace, "deliveries"))).sort();
|
|
134
|
+
} catch {
|
|
135
|
+
deliveries = [];
|
|
136
|
+
}
|
|
137
|
+
const active: string[] = [];
|
|
138
|
+
for (const candidate of deliveries) {
|
|
139
|
+
// The walking skeleton addresses the one delivery still in flight; a
|
|
140
|
+
// terminal journal stays durable but is not the CLI's subject. Terminal
|
|
141
|
+
// means a committed transition into a terminal state — never a substring
|
|
142
|
+
// of some model-authored payload.
|
|
143
|
+
try {
|
|
144
|
+
const journal = await readFile(path.join(namespace, "deliveries", candidate, "journal.jsonl"), "utf8");
|
|
145
|
+
const terminal = journal
|
|
146
|
+
.split("\n")
|
|
147
|
+
.filter((line) => line.length > 0)
|
|
148
|
+
.some((line) => {
|
|
149
|
+
try {
|
|
150
|
+
const entry = JSON.parse(line) as { kind?: string; payload?: { to?: string } };
|
|
151
|
+
return (
|
|
152
|
+
entry.kind === "transition.committed" &&
|
|
153
|
+
["completed", "cancelled", "failed"].includes(entry.payload?.to ?? "")
|
|
154
|
+
);
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
if (!terminal) active.push(candidate);
|
|
160
|
+
} catch {
|
|
161
|
+
active.push(candidate);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
let deliveryId: string | undefined;
|
|
165
|
+
if (requested !== undefined) {
|
|
166
|
+
if (!deliveries.includes(requested)) {
|
|
167
|
+
return blocked(
|
|
168
|
+
"delivery_unresolved",
|
|
169
|
+
`No delivery ${requested} is registered for this repository.`,
|
|
170
|
+
"Name a delivery this repository registered; `managed status` reports the current one.",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
deliveryId = requested;
|
|
174
|
+
} else {
|
|
175
|
+
deliveryId = active[0] ?? deliveries[deliveries.length - 1];
|
|
176
|
+
if (deliveryId === undefined || active.length > 1) {
|
|
177
|
+
return blocked(
|
|
178
|
+
"delivery_unresolved",
|
|
179
|
+
active.length > 1 ? "Several deliveries are in flight; the skeleton drives one." : "No registered delivery exists.",
|
|
180
|
+
"Register exactly one delivery for this repository, or name one with --delivery for export and delete.",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let policyBinding: CompiledAdopterPolicyBinding | undefined = context.policyBinding;
|
|
185
|
+
if (policyBinding === undefined) {
|
|
186
|
+
try {
|
|
187
|
+
policyBinding = JSON.parse(await readFile(path.join(namespace, "policy-binding.json"), "utf8")) as CompiledAdopterPolicyBinding;
|
|
188
|
+
} catch {
|
|
189
|
+
return blocked(
|
|
190
|
+
"policy_binding_missing",
|
|
191
|
+
"No compiled adopter policy binding is retained for this delivery.",
|
|
192
|
+
"Register the delivery through an adopter binding, or pass one through the embedding runtime.",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (pointer.policyBindingDigest !== compiledAdopterPolicyBindingDigest(policyBinding)) {
|
|
197
|
+
return blocked(
|
|
198
|
+
"policy_binding_mismatch",
|
|
199
|
+
"The retained compiled adopter policy binding does not match the product namespace pointer.",
|
|
200
|
+
"Restore the exact binding captured at registration; drift requires a new owner-approved delivery.",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const facade = createManagedDeliveryFacade({
|
|
204
|
+
repoDir: context.rootDir,
|
|
205
|
+
policyBinding,
|
|
206
|
+
installation: { installationPath: pointer.installationPath, receiptDir: pointer.receiptDir },
|
|
207
|
+
hostVersion: pointer.hostVersion,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
let fence: number | undefined;
|
|
211
|
+
try {
|
|
212
|
+
const workspace = JSON.parse(
|
|
213
|
+
await readFile(path.join(namespace, "deliveries", deliveryId, "workspace.json"), "utf8"),
|
|
214
|
+
) as { worktreeDir?: string; fence?: number };
|
|
215
|
+
if (typeof workspace.worktreeDir === "string" && typeof workspace.fence === "number") {
|
|
216
|
+
const [boundReal, hereReal] = await Promise.all([realpath(workspace.worktreeDir), realpath(context.rootDir)]);
|
|
217
|
+
if (boundReal === hereReal) fence = workspace.fence;
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
fence = undefined; // no bound workspace yet; fence-carrying operations fail closed
|
|
221
|
+
}
|
|
222
|
+
return { facade, deliveryId, fence };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const isCommandResult = (value: ResolvedManaged | CommandResult): value is CommandResult => "kind" in value;
|
|
226
|
+
|
|
227
|
+
export const managedCommand: CommandDescriptor = {
|
|
228
|
+
name: "managed",
|
|
229
|
+
sourceId: SOURCE_ID,
|
|
230
|
+
summary: "Drive the managed delivery's next checkpoint (status, stages, sensor, review, admission, record, finish).",
|
|
231
|
+
async run(context: CommandContext): Promise<CommandResult> {
|
|
232
|
+
const [operation, ...rest] = context.args;
|
|
233
|
+
if (operation === undefined) {
|
|
234
|
+
return {
|
|
235
|
+
kind: "usage",
|
|
236
|
+
message:
|
|
237
|
+
`managed requires an operation: ${MANAGED_OPERATIONS.join(" | ")}`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
// An unknown operation is a usage error about the CALL, and it is answered
|
|
241
|
+
// before anything about the repository is consulted: a typo answered with
|
|
242
|
+
// "this is not a repository" sends a reader to fix the wrong thing.
|
|
243
|
+
if (!MANAGED_OPERATIONS.includes(operation)) {
|
|
244
|
+
return { kind: "usage", message: `Unknown managed operation: ${operation}.` };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// The inspectable contract: every operation, what it costs, whether it
|
|
248
|
+
// binds the fence, and which surfaces reach it. It describes the product,
|
|
249
|
+
// not a delivery, so it answers before any delivery has to resolve —
|
|
250
|
+
// otherwise the one operation that explains the surface would be
|
|
251
|
+
// unavailable exactly when a reader most needs it.
|
|
252
|
+
if (operation === "operations") {
|
|
253
|
+
context.write(`${JSON.stringify(FACADE_OPERATIONS, null, 2)}\n`);
|
|
254
|
+
return { kind: "ok", summary: `${FACADE_OPERATIONS.length} facade operations` };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Only the retention operations may name a delivery; everything else binds
|
|
258
|
+
// the fence of the worktree it runs in.
|
|
259
|
+
const RETENTION_OPERATIONS = ["export", "delete"];
|
|
260
|
+
// Every flag on this surface is space-separated, and this one is refused in
|
|
261
|
+
// the GNU `--flag=value` form rather than parsed: accepting it here alone
|
|
262
|
+
// would make the convention inconsistent, and letting it through unread is
|
|
263
|
+
// the silent-retarget hazard below by another spelling — `indexOf` never
|
|
264
|
+
// matches `--delivery=id`, so it would fall back to the implicitly resolved
|
|
265
|
+
// delivery and point a destructive operation somewhere the operator did not
|
|
266
|
+
// name.
|
|
267
|
+
if (rest.some((argument) => argument.startsWith("--delivery="))) {
|
|
268
|
+
return { kind: "usage", message: "--delivery takes its value as a separate argument: --delivery <id>." };
|
|
269
|
+
}
|
|
270
|
+
const namesDelivery = rest.includes("--delivery");
|
|
271
|
+
if (namesDelivery && !RETENTION_OPERATIONS.includes(operation)) {
|
|
272
|
+
return { kind: "usage", message: `--delivery is accepted only by: ${RETENTION_OPERATIONS.join(", ")}.` };
|
|
273
|
+
}
|
|
274
|
+
const requestedDelivery = RETENTION_OPERATIONS.includes(operation) ? flag(rest, "--delivery") : undefined;
|
|
275
|
+
// A trailing `--delivery` with no value reads as absent, and absent falls
|
|
276
|
+
// back to the implicitly resolved delivery. On `delete` that is a typo
|
|
277
|
+
// silently retargeting a destructive operation at a different delivery, so
|
|
278
|
+
// the flag's presence without a value is a refusal rather than a default.
|
|
279
|
+
if (namesDelivery && requestedDelivery === undefined) {
|
|
280
|
+
return { kind: "usage", message: "--delivery requires a delivery id." };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const resolved = await resolveManaged(context, requestedDelivery);
|
|
284
|
+
if (isCommandResult(resolved)) return resolved;
|
|
285
|
+
const { facade, deliveryId, fence } = resolved;
|
|
286
|
+
const requireFence = (): number | CommandResult =>
|
|
287
|
+
fence ??
|
|
288
|
+
blocked(
|
|
289
|
+
"workspace_superseded",
|
|
290
|
+
"This worktree is not the delivery's currently bound workspace, so it carries no invocation fence.",
|
|
291
|
+
"Drive checkpoints from the bound worktree; a superseded task's outputs are permanently rejected.",
|
|
292
|
+
);
|
|
293
|
+
const emit = (value: unknown): void => context.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The typed stage-result document a checkpoint submits. Prose cannot
|
|
297
|
+
* advance the reducer, so these operations read a
|
|
298
|
+
* `workflow-stage-result/1` file rather than accepting free text.
|
|
299
|
+
*/
|
|
300
|
+
const resultFileArg = async (operation: string): Promise<string | CommandResult> => {
|
|
301
|
+
const file = flag(rest, "--result-file");
|
|
302
|
+
if (file === undefined) {
|
|
303
|
+
return {
|
|
304
|
+
kind: "usage",
|
|
305
|
+
message: `${operation} requires --result-file <path> containing a typed workflow-stage-result/1 document; prose cannot advance a checkpoint.`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
return await readFile(path.resolve(context.rootDir, file), "utf8");
|
|
310
|
+
} catch (error) {
|
|
311
|
+
return { kind: "usage", message: `${operation} could not read ${file}: ${error instanceof Error ? error.message : String(error)}` };
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
switch (operation) {
|
|
316
|
+
case "status": {
|
|
317
|
+
const status = await facade.status({ deliveryId, observedAt: nowInstant() });
|
|
318
|
+
if (!status.ok) return { kind: "blocked", blockers: [...status.blockers] };
|
|
319
|
+
emit(status.status);
|
|
320
|
+
return {
|
|
321
|
+
kind: "ok",
|
|
322
|
+
summary: `state ${status.status.delivery.state}; host ${status.status.hostActivity}; next ${status.status.nextCheckpoint.kind}`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
case "blockers": {
|
|
326
|
+
const inventory = await facade.blockerInventory({ deliveryId });
|
|
327
|
+
if (!inventory.ok) return { kind: "blocked", blockers: [...inventory.blockers] };
|
|
328
|
+
emit(inventory.entries);
|
|
329
|
+
return { kind: "ok", summary: `${inventory.entries.length} blocker(s) journaled` };
|
|
330
|
+
}
|
|
331
|
+
case "propose-approval": {
|
|
332
|
+
const requestKind = flag(rest, "--kind");
|
|
333
|
+
const criterionId = flag(rest, "--criterion");
|
|
334
|
+
const actorId = flag(rest, "--actor");
|
|
335
|
+
const reason = flag(rest, "--reason");
|
|
336
|
+
if ((requestKind !== "waiver" && requestKind !== "amendment") || criterionId === undefined || actorId === undefined || reason === undefined) {
|
|
337
|
+
return {
|
|
338
|
+
kind: "usage",
|
|
339
|
+
message: "propose-approval requires --kind <waiver|amendment> --criterion <id> --actor <id> --reason <text>.",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const invokingFence = requireFence();
|
|
343
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
344
|
+
// Proposing is all this does. The approval half is the sensitive lane's
|
|
345
|
+
// and needs a fresh assertion the proposer cannot mint.
|
|
346
|
+
const proposed = await facade.recordApprovalRequest({
|
|
347
|
+
deliveryId,
|
|
348
|
+
requestKind,
|
|
349
|
+
criterionId,
|
|
350
|
+
actorId,
|
|
351
|
+
reason,
|
|
352
|
+
fence: invokingFence,
|
|
353
|
+
});
|
|
354
|
+
if (!proposed.ok) return { kind: "blocked", blockers: [...proposed.blockers] };
|
|
355
|
+
return { kind: "ok", summary: `${requestKind} proposed for ${criterionId}; delivery is ${proposed.state}` };
|
|
356
|
+
}
|
|
357
|
+
case "request-cancellation": {
|
|
358
|
+
const requested = await facade.requestCancellation({ deliveryId });
|
|
359
|
+
if (!requested.ok) return { kind: "blocked", blockers: [...requested.blockers] };
|
|
360
|
+
return { kind: "ok", summary: `cancellation requested; delivery is ${requested.state}` };
|
|
361
|
+
}
|
|
362
|
+
case "finalize-cancellation": {
|
|
363
|
+
const finalized = await facade.finalizeCancellation({ deliveryId });
|
|
364
|
+
if (!finalized.ok) return { kind: "blocked", blockers: [...finalized.blockers] };
|
|
365
|
+
return { kind: "ok", summary: `prior workspace quarantined; delivery is ${finalized.state}` };
|
|
366
|
+
}
|
|
367
|
+
case "export": {
|
|
368
|
+
const exported = await facade.exportDelivery({ deliveryId });
|
|
369
|
+
if (!exported.ok) return { kind: "blocked", blockers: [...exported.blockers] };
|
|
370
|
+
emit({ exportPath: exported.exportPath, artifactDigest: exported.artifactDigest });
|
|
371
|
+
return { kind: "ok", summary: `exported to ${exported.exportPath}` };
|
|
372
|
+
}
|
|
373
|
+
case "delete": {
|
|
374
|
+
const deleted = await facade.deleteDelivery({ deliveryId });
|
|
375
|
+
if (!deleted.ok) return { kind: "blocked", blockers: [...deleted.blockers] };
|
|
376
|
+
emit({ preservedAuditRecords: deleted.preservedAuditRecords });
|
|
377
|
+
return { kind: "ok", summary: `deleted; ${deleted.preservedAuditRecords.length} audit record(s) preserved` };
|
|
378
|
+
}
|
|
379
|
+
case "recover": {
|
|
380
|
+
const target = flag(rest, "--generation");
|
|
381
|
+
const recovered = await facade.recoverSecurityBlocked({
|
|
382
|
+
deliveryId,
|
|
383
|
+
now: nowInstant(),
|
|
384
|
+
...(target === undefined ? {} : { targetGenerationDigest: target }),
|
|
385
|
+
});
|
|
386
|
+
if (!recovered.ok) return { kind: "blocked", blockers: [...recovered.blockers] };
|
|
387
|
+
return { kind: "ok", summary: `${recovered.mode}; delivery is ${recovered.state}` };
|
|
388
|
+
}
|
|
389
|
+
case "next": {
|
|
390
|
+
const next = await facade.nextCheckpoint({ deliveryId });
|
|
391
|
+
if (!next.ok) return { kind: "blocked", blockers: [...next.blockers] };
|
|
392
|
+
emit(next.checkpoint);
|
|
393
|
+
return { kind: "ok", summary: `next checkpoint: ${next.checkpoint.kind}` };
|
|
394
|
+
}
|
|
395
|
+
case "submit-plan": {
|
|
396
|
+
const invokingFence = requireFence();
|
|
397
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
398
|
+
const resultBytes = await resultFileArg("submit-plan");
|
|
399
|
+
if (typeof resultBytes !== "string") return resultBytes;
|
|
400
|
+
const submitted = await facade.submitStageResult({ deliveryId, stageId: "plan", resultBytes, fence: invokingFence });
|
|
401
|
+
if (!submitted.ok) return { kind: "blocked", blockers: [...submitted.blockers] };
|
|
402
|
+
return { kind: "ok", summary: `plan accepted; delivery is ${submitted.state}` };
|
|
403
|
+
}
|
|
404
|
+
case "checkpoint": {
|
|
405
|
+
const invokingFence = requireFence();
|
|
406
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
407
|
+
const resultBytes = await resultFileArg("checkpoint");
|
|
408
|
+
if (typeof resultBytes !== "string") return resultBytes;
|
|
409
|
+
const checkpointed = await facade.checkpointCandidate({ deliveryId, resultBytes, fence: invokingFence });
|
|
410
|
+
if (!checkpointed.ok) return { kind: "blocked", blockers: [...checkpointed.blockers] };
|
|
411
|
+
return { kind: "ok", summary: `candidate ${checkpointed.treeSha} checkpointed; delivery is ${checkpointed.state}` };
|
|
412
|
+
}
|
|
413
|
+
case "run-sensor": {
|
|
414
|
+
const invokingFence = requireFence();
|
|
415
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
416
|
+
const sensed = await facade.runSensor({ deliveryId, fence: invokingFence });
|
|
417
|
+
if (!sensed.ok) return { kind: "blocked", blockers: [...sensed.blockers] };
|
|
418
|
+
return { kind: "ok", summary: `sensor ${sensed.outcome}; delivery is ${sensed.state}` };
|
|
419
|
+
}
|
|
420
|
+
case "reduce-review": {
|
|
421
|
+
const invokingFence = requireFence();
|
|
422
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
423
|
+
const reduced = await facade.reduceReview({ deliveryId, fence: invokingFence });
|
|
424
|
+
if (!reduced.ok) return { kind: "blocked", blockers: [...reduced.blockers] };
|
|
425
|
+
return { kind: "ok", summary: `review reduced; delivery is ${reduced.state}` };
|
|
426
|
+
}
|
|
427
|
+
case "compound": {
|
|
428
|
+
const invokingFence = requireFence();
|
|
429
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
430
|
+
const resultBytes = await resultFileArg("compound");
|
|
431
|
+
if (typeof resultBytes !== "string") return resultBytes;
|
|
432
|
+
const compounded = await facade.submitStageResult({ deliveryId, stageId: "compound", resultBytes, fence: invokingFence });
|
|
433
|
+
if (!compounded.ok) return { kind: "blocked", blockers: [...compounded.blockers] };
|
|
434
|
+
return { kind: "ok", summary: `compound recorded; delivery is ${compounded.state}` };
|
|
435
|
+
}
|
|
436
|
+
case "admit": {
|
|
437
|
+
const invokingFence = requireFence();
|
|
438
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
439
|
+
const admitted = await facade.admit({ deliveryId, recordedAtInstant: nowInstant(), env: context.env, fence: invokingFence });
|
|
440
|
+
if (!admitted.ok) return { kind: "blocked", blockers: [...admitted.blockers] };
|
|
441
|
+
return { kind: "ok", summary: `admitted; delivery is ${admitted.state}` };
|
|
442
|
+
}
|
|
443
|
+
case "prepare-record": {
|
|
444
|
+
const invokingFence = requireFence();
|
|
445
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
446
|
+
const prepared = await facade.prepareTrackedRecord({ deliveryId, env: context.env, fence: invokingFence });
|
|
447
|
+
if (!prepared.ok) return { kind: "blocked", blockers: [...prepared.blockers] };
|
|
448
|
+
return { kind: "ok", summary: `tracked record written at ${prepared.relativePath}; commit it through native git tooling` };
|
|
449
|
+
}
|
|
450
|
+
case "confirm-record": {
|
|
451
|
+
const invokingFence = requireFence();
|
|
452
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
453
|
+
const confirmed = await facade.confirmTrackedRecord({ deliveryId, fence: invokingFence });
|
|
454
|
+
if (!confirmed.ok) return { kind: "blocked", blockers: [...confirmed.blockers] };
|
|
455
|
+
return { kind: "ok", summary: `tracked record verified; delivery is ${confirmed.state}` };
|
|
456
|
+
}
|
|
457
|
+
case "finish": {
|
|
458
|
+
const invokingFence = requireFence();
|
|
459
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
460
|
+
const finished = await facade.completeFinishLine({ deliveryId, fence: invokingFence });
|
|
461
|
+
if (!finished.ok) return { kind: "blocked", blockers: [...finished.blockers] };
|
|
462
|
+
return { kind: "ok", summary: `merge-ready; delivery is ${finished.state} (result ${finished.resultDigest})` };
|
|
463
|
+
}
|
|
464
|
+
case "explain-blocker": {
|
|
465
|
+
const explained = await facade.explainBlocker({ deliveryId });
|
|
466
|
+
if (!explained.ok) return { kind: "blocked", blockers: [...explained.blockers] };
|
|
467
|
+
emit(explained.blocker ?? { blocker: null });
|
|
468
|
+
return { kind: "ok", summary: explained.blocker === undefined ? "no blocker recorded" : `blocker ${explained.blocker.code}` };
|
|
469
|
+
}
|
|
470
|
+
default:
|
|
471
|
+
// Unreachable: the operation was checked against MANAGED_OPERATIONS
|
|
472
|
+
// before the delivery resolved. Kept so adding a name to that list
|
|
473
|
+
// without a case here fails loudly rather than silently doing nothing.
|
|
474
|
+
return { kind: "usage", message: `Unknown managed operation: ${operation}.` };
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
};
|
package/src/commands/prepare.ts
CHANGED
|
@@ -23,9 +23,16 @@ export const prepareCommand: CommandDescriptor = {
|
|
|
23
23
|
{ config: context.config, candidate: capture.candidate },
|
|
24
24
|
wiring.storageOptions,
|
|
25
25
|
);
|
|
26
|
+
// The labelled line exists so a reader — an operator or a review round
|
|
27
|
+
// about to bind itself to this candidate — has one unambiguous token to
|
|
28
|
+
// copy. It is the same value the record carries as `treeSha` and the same
|
|
29
|
+
// value `review-context` reports as `candidate tree`.
|
|
26
30
|
return {
|
|
27
31
|
kind: "ok",
|
|
28
|
-
summary:
|
|
32
|
+
summary: [
|
|
33
|
+
`prepared ${context.config.gateId}: tree ${capture.candidate.treeSha} (${capture.candidate.mode}); receipt ${published.path}`,
|
|
34
|
+
` treeSha ${capture.candidate.treeSha}`,
|
|
35
|
+
].join("\n"),
|
|
29
36
|
};
|
|
30
37
|
},
|
|
31
38
|
};
|
package/src/commands/record.ts
CHANGED
|
@@ -16,12 +16,12 @@ import {
|
|
|
16
16
|
deliveryRecordBytes,
|
|
17
17
|
deliveryRecordPathFor,
|
|
18
18
|
discoverRecords,
|
|
19
|
-
runAdmission,
|
|
20
19
|
type EvidenceRecord,
|
|
21
20
|
} from "@agent-delivery-harness/kernel";
|
|
22
21
|
import path from "node:path";
|
|
23
22
|
import { commandBlocker } from "../boundary.ts";
|
|
24
23
|
import type { CommandContext, CommandDescriptor, CommandResult } from "../boundary.ts";
|
|
24
|
+
import { runProviderBackedAdmission } from "./gate.ts";
|
|
25
25
|
|
|
26
26
|
export const recordCommand: CommandDescriptor = {
|
|
27
27
|
name: "record",
|
|
@@ -33,10 +33,7 @@ export const recordCommand: CommandDescriptor = {
|
|
|
33
33
|
// The gate is run without a prompt: `record` is not the waiver surface. If a
|
|
34
34
|
// waiver is needed, the operator runs `gate` first; here a non-admitting gate
|
|
35
35
|
// is simply a refusal.
|
|
36
|
-
const admission = await
|
|
37
|
-
{ rootDir: context.rootDir, config: context.config, context: context.classifyContext() },
|
|
38
|
-
{ captureCandidate: wiring.captureCandidate, projectActivation: wiring.projectActivation, ...wiring.storageOptions },
|
|
39
|
-
);
|
|
36
|
+
const admission = await runProviderBackedAdmission(context, { allowPrompt: false, includeInjectedLiveResults: false });
|
|
40
37
|
if (!admission.admitted || admission.decision === undefined) {
|
|
41
38
|
return { kind: "blocked", blockers: [...admission.blockers] };
|
|
42
39
|
}
|