@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.
@@ -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
+ };
@@ -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: `prepared ${context.config.gateId}: tree ${capture.candidate.treeSha} (${capture.candidate.mode}); receipt ${published.path}`,
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
  };
@@ -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 runAdmission(
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
  }