@agent-delivery-harness/cli 0.1.0 → 0.5.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.
Files changed (40) hide show
  1. package/LICENSE +104 -201
  2. package/NOTICE +1 -1
  3. package/package.json +10 -5
  4. package/src/boundary.ts +302 -9
  5. package/src/command-activity.ts +75 -0
  6. package/src/commands/admit.ts +106 -0
  7. package/src/commands/check.ts +13 -2
  8. package/src/commands/emit-review-evidence.ts +38 -0
  9. package/src/commands/emit.ts +419 -0
  10. package/src/commands/gate.ts +127 -18
  11. package/src/commands/maintain.ts +203 -0
  12. package/src/commands/managed.ts +478 -0
  13. package/src/commands/prepare.ts +123 -15
  14. package/src/commands/record.ts +100 -8
  15. package/src/commands/resume.ts +53 -0
  16. package/src/commands/review-context.ts +19 -0
  17. package/src/commands/runs.ts +640 -0
  18. package/src/commands/save-context.ts +51 -0
  19. package/src/commands/submit-evidence.ts +46 -7
  20. package/src/commands/verify.ts +173 -5
  21. package/src/declared-checks.ts +50 -0
  22. package/src/index.ts +40 -4
  23. package/src/main.ts +38 -42
  24. package/src/ordinary-context.ts +99 -0
  25. package/src/provider-rails.ts +24 -0
  26. package/src/record-retention.ts +325 -0
  27. package/src/review-evidence.ts +284 -0
  28. package/src/run-archive-commands.ts +164 -0
  29. package/src/run-archive.ts +127 -0
  30. package/src/run-artifact-commands.ts +163 -0
  31. package/src/run-attachments.ts +112 -0
  32. package/src/run-export.ts +164 -0
  33. package/src/run-live.ts +102 -0
  34. package/src/run-projection.ts +396 -0
  35. package/src/run-server.ts +905 -0
  36. package/src/run-surface.ts +334 -0
  37. package/src/run-view-command.ts +77 -0
  38. package/src/run-view-html.ts +138 -0
  39. package/src/run-view-record.ts +119 -0
  40. package/src/run-view.ts +343 -0
@@ -0,0 +1,38 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { commandBlocker, type CommandDescriptor } from "../boundary.ts";
4
+ import { emitReviewEvidence, OutcomeError } from "../review-evidence.ts";
5
+
6
+ export const emitReviewEvidenceCommand: CommandDescriptor = {
7
+ name: "emit-review-evidence",
8
+ sourceId: "delivery-harness.cli.emit-review-evidence",
9
+ summary: "Bind concluded review outcomes to their original prepared context.",
10
+ usage: "Usage: delivery-harness emit-review-evidence --context <review-context.json>\nReads a review-outcome/1 document on stdin.",
11
+ async run(context) {
12
+ if (context.args.length !== 2 || context.args[0] !== "--context" || !context.args[1] || context.args[1].startsWith("-")) {
13
+ return { kind: "usage", message: "emit-review-evidence requires --context <review-context.json> and a review-outcome/1 document on stdin." };
14
+ }
15
+ let original: unknown;
16
+ let document: unknown;
17
+ try {
18
+ original = JSON.parse(await readFile(path.resolve(context.rootDir, context.args[1]), "utf8"));
19
+ const raw = await context.readStdin?.() ?? "";
20
+ if (!raw.trim()) return { kind: "usage", message: "The review-outcome/1 document is required on stdin." };
21
+ document = JSON.parse(raw);
22
+ } catch {
23
+ return { kind: "usage", message: "The original context and review outcome must be readable JSON documents." };
24
+ }
25
+ try {
26
+ const result = await emitReviewEvidence(context, original, document);
27
+ return { kind: "ok", summary: result.manifestPath };
28
+ } catch (error) {
29
+ if (!(error instanceof OutcomeError)) throw error;
30
+ return { kind: "blocked", blockers: [commandBlocker({
31
+ code: "review_outcome_invalid",
32
+ sourceId: "delivery-harness.cli.emit-review-evidence",
33
+ summary: error.message,
34
+ remediations: [{ id: "supply-reviewed-context", kind: "manual_action", summary: "Use the original review-context --json output and concluded outcomes naming its digest; acquire a new review if the deliverable or review inputs changed." }],
35
+ })] };
36
+ }
37
+ },
38
+ };
@@ -0,0 +1,419 @@
1
+ /**
2
+ * `emit` — append one run event to the current delivery run's journal.
3
+ *
4
+ * THE EXECUTOR'S ONE WRITE PATH. Everything an executor knows that the product
5
+ * cannot observe — which lenses it chose and why, what it decided at a fork,
6
+ * what a review round cost — arrives here or is lost to a transcript. The
7
+ * command is deliberately small: a kind, a JSON payload, and the store.
8
+ *
9
+ * WHAT `emit` MAY NOT SET. Not `seq`, which is the store's, taken inside its
10
+ * critical section. Not `at`, which is this process's own instant. Not
11
+ * `actor`, which is always `executor` here — the boundary wrap is the only
12
+ * writer of `cli`, and the store refuses `cli` on any other kind. And not
13
+ * `command.completed` at all: a completion is a claim that the product ran a
14
+ * command, and only the product may make it. That refusal is noted in the
15
+ * run's note exactly like any other refused append, so an executor that tries
16
+ * leaves a trace rather than nothing.
17
+ *
18
+ * ORDER OF OPERATIONS, AND WHY IT IS THIS ORDER.
19
+ * 1. Arguments. A malformed invocation is a usage error and touches nothing.
20
+ * 2. The repository. Outside one there is no store, so there is nowhere to
21
+ * write and nowhere to note.
22
+ * 3. The run. Resolution precedes kind validation, because a refused append
23
+ * is recorded against a run — with no run there is nothing to note
24
+ * against, and inventing one would create a journal for a typo.
25
+ * `run.started` is the single exception: it allocates the run it needs.
26
+ * 4. The store. Kind and payload validation, the secret discipline, and the
27
+ * note on refusal are all the store's, so there is exactly one
28
+ * implementation of each.
29
+ */
30
+ import {
31
+ RUN_STORE_ID,
32
+ reduceToProviderId,
33
+ type RunStore,
34
+ } from "@agent-delivery-harness/kernel";
35
+ import {
36
+ buildRunEvent,
37
+ oneLine,
38
+ resolveRunSurface,
39
+ runSurfaceBlocker,
40
+ type RunSurface,
41
+ } from "../run-surface.ts";
42
+ import type { CommandResult, ConfigFreeCommandContext, ConfigFreeCommandDescriptor } from "../boundary.ts";
43
+
44
+ const USAGE = "Usage: delivery-harness emit <kind> [--run <id>] [--json <payload>] [--force] [--version 1|2] [--event-id <id>]";
45
+
46
+ const PAYLOAD_USAGE =
47
+ "emit needs a JSON payload: use --json <payload>, or omit --json only when piping JSON to stdin until EOF.";
48
+
49
+ interface ParsedArgs {
50
+ readonly kind: string;
51
+ readonly run?: string;
52
+ readonly json?: string;
53
+ readonly force: boolean;
54
+ readonly version?: "run-event/1" | "run-event/2";
55
+ readonly eventId?: string;
56
+ }
57
+
58
+ type ArgParse = { readonly ok: true; readonly args: ParsedArgs } | { readonly ok: false; readonly message: string };
59
+
60
+ function parseArgs(args: readonly string[]): ArgParse {
61
+ let kind: string | undefined;
62
+ let run: string | undefined;
63
+ let json: string | undefined;
64
+ let force = false;
65
+ let version: "run-event/1" | "run-event/2" | undefined;
66
+ let eventId: string | undefined;
67
+
68
+ for (let index = 0; index < args.length; index += 1) {
69
+ const token = args[index]!;
70
+ if (token === "--force") {
71
+ force = true;
72
+ continue;
73
+ }
74
+ if (token === "--run" || token === "--json" || token === "--version" || token === "--event-id") {
75
+ const value = args[index + 1];
76
+ if (value === undefined) return { ok: false, message: `${token} needs a value.\n${USAGE}` };
77
+ if (token === "--run") run = value;
78
+ else if (token === "--json") json = value;
79
+ else if (token === "--event-id") eventId = value;
80
+ else {
81
+ if (value !== "1" && value !== "2") return { ok: false, message: `--version must be 1 or 2.\n${USAGE}` };
82
+ version = value === "1" ? "run-event/1" : "run-event/2";
83
+ }
84
+ index += 1;
85
+ continue;
86
+ }
87
+ if (token.startsWith("--")) return { ok: false, message: `Unknown flag ${oneLine(token, 64)}.\n${USAGE}` };
88
+ if (kind !== undefined) return { ok: false, message: `emit takes one kind.\n${USAGE}` };
89
+ kind = token;
90
+ }
91
+
92
+ if (kind === undefined) return { ok: false, message: `emit needs a kind.\n${USAGE}` };
93
+ return { ok: true, args: { kind, force, ...(run === undefined ? {} : { run }), ...(json === undefined ? {} : { json }),
94
+ ...(version === undefined ? {} : { version }), ...(eventId === undefined ? {} : { eventId }) } };
95
+ }
96
+
97
+ /**
98
+ * The payload as the caller supplied it. Text that is not JSON is passed
99
+ * through as-is rather than rejected here: the store owns payload validation,
100
+ * and routing it there is what puts the refusal in the run's note.
101
+ */
102
+ function parsePayload(text: string): unknown {
103
+ const trimmed = text.trim();
104
+ if (trimmed.length === 0) return {};
105
+ try {
106
+ return JSON.parse(trimmed) as unknown;
107
+ } catch {
108
+ return trimmed;
109
+ }
110
+ }
111
+
112
+ async function suppliedPayload(context: ConfigFreeCommandContext, json: string | undefined): Promise<
113
+ { readonly ok: true; readonly payload: unknown } | { readonly ok: false; readonly message: string }
114
+ > {
115
+ if (json !== undefined) return { ok: true, payload: parsePayload(json) };
116
+ if (context.stdinIsTTY === true) {
117
+ return { ok: false, message: `${PAYLOAD_USAGE}\nstdin is interactive, so emit will not wait for terminal EOF.\n${USAGE}` };
118
+ }
119
+ const text = await context.readStdin();
120
+ if (text.trim() === "") return { ok: false, message: `${PAYLOAD_USAGE}\n${USAGE}` };
121
+ return { ok: true, payload: parsePayload(text) };
122
+ }
123
+
124
+ const noRun = (details: string) =>
125
+ runSurfaceBlocker({
126
+ code: "run_unresolvable",
127
+ summary: "There is no current run to emit against.",
128
+ details,
129
+ remediation: {
130
+ id: "start-a-run",
131
+ summary: "Run `delivery-harness emit run.started` in this worktree, or name an existing run with --run.",
132
+ },
133
+ });
134
+
135
+ /**
136
+ * Resolves the run this event belongs to: `--run` when given, else the
137
+ * invoking worktree's pointer. An explicit `--run` resolves only when the id
138
+ * passes the charset check AND its journal exists, so no arbitrary id ever
139
+ * causes a journal or a note to be created.
140
+ */
141
+ async function resolveRun(store: RunStore, surface: RunSurface, named: string | undefined): Promise<string | undefined> {
142
+ if (named !== undefined) {
143
+ if (named.length > 128 || !RUN_STORE_ID.test(named)) return undefined;
144
+ const read = await store.read(named);
145
+ return read.ok ? named : undefined;
146
+ }
147
+ const current = await store.current(surface.worktreeKey);
148
+ return current.ok ? current.runId : undefined;
149
+ }
150
+
151
+ export const emitCommand: ConfigFreeCommandDescriptor = {
152
+ name: "emit",
153
+ sourceId: "delivery-harness.cli.emit",
154
+ summary: "Append one run event to the current delivery run's journal.",
155
+ usage: USAGE,
156
+ configFree: true,
157
+ async run(context: ConfigFreeCommandContext): Promise<CommandResult> {
158
+ const parsed = parseArgs(context.args);
159
+ if (!parsed.ok) return { kind: "usage", message: parsed.message };
160
+ const { kind, run: named, force } = parsed.args;
161
+
162
+ const resolved = await resolveRunSurface(context.rootDir);
163
+ if (!resolved.ok) {
164
+ return {
165
+ kind: "blocked",
166
+ blockers: [
167
+ runSurfaceBlocker({
168
+ code: "run_store_unresolvable",
169
+ summary: "The run store could not be resolved.",
170
+ details: oneLine(resolved.reason, 200),
171
+ remediation: {
172
+ id: "run-inside-a-repository",
173
+ summary: "Run this command inside a git repository; the run store lives under its common directory.",
174
+ },
175
+ }),
176
+ ],
177
+ };
178
+ }
179
+ const surface = resolved.surface;
180
+ const store = surface.store;
181
+
182
+ if (kind === "run.started") {
183
+ const version = parsed.args.version ?? "run-event/1";
184
+ if ((version === "run-event/2") !== (parsed.args.eventId !== undefined)) {
185
+ return {
186
+ kind: "usage",
187
+ message: version === "run-event/2"
188
+ ? `run.started is creating a version 2 run; every emit needs --event-id.\n${USAGE}`
189
+ : `run.started is creating a version 1 run and does not accept --event-id; drop it.\n${USAGE}`,
190
+ };
191
+ }
192
+ const supplied = await suppliedPayload(context, parsed.args.json);
193
+ if (!supplied.ok) return { kind: "usage", message: supplied.message };
194
+ return startRun(surface, force, supplied.payload, version, parsed.args.eventId);
195
+ }
196
+
197
+ const runId = await resolveRun(store, surface, named);
198
+ if (runId === undefined) {
199
+ return {
200
+ kind: "blocked",
201
+ blockers: [
202
+ noRun(
203
+ named === undefined
204
+ ? "no current run is pointed at from this worktree, and no --run was given"
205
+ : `--run ${oneLine(named, 128)} names no readable journal in this store`,
206
+ ),
207
+ ],
208
+ };
209
+ }
210
+
211
+ const history = await store.read(runId);
212
+ if (!history.ok) return { kind: "blocked", blockers: [noRun("the selected run is unreadable")] };
213
+ const version = history.events[0]?.version ?? "run-event/1";
214
+ if (parsed.args.version !== undefined && parsed.args.version !== version) {
215
+ return { kind: "usage", message: "A run's writer version cannot change. End it and start an explicitly linked successor run." };
216
+ }
217
+ if ((version === "run-event/2") !== (parsed.args.eventId !== undefined)) {
218
+ return {
219
+ kind: "usage",
220
+ message: version === "run-event/2"
221
+ ? `run ${runId} is version 2; every emit needs --event-id.\n${USAGE}`
222
+ : `run ${runId} is version 1 and does not accept --event-id; drop it, and drop the version-2 payload members roundId, bound, grace and reopensRoundId.\n${USAGE}`,
223
+ };
224
+ }
225
+ const supplied = await suppliedPayload(context, parsed.args.json);
226
+ if (!supplied.ok) return { kind: "usage", message: supplied.message };
227
+ const payload = supplied.payload;
228
+ const event = buildRunEvent({ runId, commonDir: surface.commonDir, kind, role: "executor", payload, version,
229
+ ...(parsed.args.eventId === undefined ? {} : { eventId: parsed.args.eventId }) });
230
+
231
+ // `command.completed` is the CLI's to write. The refusal is recorded in the
232
+ // run's note through the store's one bounded note writer, so an attempt
233
+ // leaves the same trace as any other refused append.
234
+ if (kind === "command.completed") {
235
+ await store.noteRefusal(runId, event, {
236
+ code: "unsupported_combination",
237
+ pointer: "/kind",
238
+ message: "command.completed is written by the CLI; emit may not write it",
239
+ });
240
+ return {
241
+ kind: "blocked",
242
+ blockers: [
243
+ runSurfaceBlocker({
244
+ code: "run_event_refused",
245
+ summary: "command.completed is written by the CLI, never by emit.",
246
+ details: `run ${runId}: the product's own commands append their completions; an executor reports a non-product gate with gate.reported`,
247
+ remediation: {
248
+ id: "use-gate-reported",
249
+ summary: "Emit gate.reported when the repository's gate is not a product command.",
250
+ },
251
+ }),
252
+ ],
253
+ };
254
+ }
255
+
256
+ // Resolve generated timestamps inside the append lock so concurrent retries
257
+ // retain the first observation instant while changed content still refuses.
258
+ const appended = await store.append(runId, event, { reuseExistingTimestamp: true });
259
+ if (!appended.ok) {
260
+ const first = appended.rejections[0];
261
+ return {
262
+ kind: "blocked",
263
+ blockers: [
264
+ runSurfaceBlocker({
265
+ code: "run_event_refused",
266
+ // The rejected kind is echoed reduced to the bound the note records
267
+ // it at, so the diagnostic and the durable line say the same thing
268
+ // and neither can carry an escape sequence to the terminal.
269
+ summary: `The run event was refused: ${reduceToProviderId(kind)}`,
270
+ details: `run ${runId}: ${first === undefined ? "the store refused the append" : `${first.code} at ${oneLine(first.pointer, 64) || "/"}: ${oneLine(first.message, 200)}`}`,
271
+ remediation: {
272
+ id: "correct-the-event",
273
+ summary: `Correct the kind or the payload against the ${version} contract and emit again.`,
274
+ },
275
+ }),
276
+ ],
277
+ };
278
+ }
279
+
280
+ if (kind === "run.ended") await store.clearCurrent(surface.worktreeKey, runId);
281
+ return { kind: "ok", summary: `emitted ${appended.event.kind} seq ${appended.event.seq} to run ${runId}` };
282
+ },
283
+ };
284
+
285
+ /**
286
+ * `run.started` is the one kind that allocates rather than resolves.
287
+ *
288
+ * The pointer is read BEFORE the journal is allocated, so the ordinary refusal
289
+ * — a run is already current and no `--force` was given — leaves no orphan
290
+ * journal behind. `--force` carries the displaced run's id into the new run's
291
+ * own `run.started` payload, which is what makes a restarted delivery legible
292
+ * afterwards.
293
+ *
294
+ * THE TWO REFUSALS THAT COME AFTER ALLOCATION are the append and the exclusive
295
+ * pointer write, and each takes the journal back through `discard`. Without
296
+ * that, a payload the store refuses leaves an empty journal per retry, and the
297
+ * loser of a pointer race leaves a journal carrying a real `run.started` —
298
+ * both of which `runs list` shows as open runs an operator cannot tell from an
299
+ * abandoned delivery. The removal is best-effort by construction: this command
300
+ * has already decided to refuse, and a store failure may not change that.
301
+ */
302
+ async function startRun(surface: RunSurface, force: boolean, supplied: unknown,
303
+ version: "run-event/1" | "run-event/2", eventId?: string): Promise<CommandResult> {
304
+ const store = surface.store;
305
+ const existing = await store.current(surface.worktreeKey);
306
+ const displaced = existing.ok ? existing.runId : undefined;
307
+ if (displaced !== undefined && !force) {
308
+ return {
309
+ kind: "blocked",
310
+ blockers: [
311
+ runSurfaceBlocker({
312
+ code: "run_already_current",
313
+ summary: "A run is already current for this worktree.",
314
+ details: `run ${displaced} is current; --force displaces it and records it as displacedRunId`,
315
+ remediation: {
316
+ id: "end-or-force",
317
+ summary: "End the current run with `emit run.ended`, or start this one with --force.",
318
+ },
319
+ }),
320
+ ],
321
+ };
322
+ }
323
+
324
+ const allocated = await store.allocate();
325
+ if (!allocated.ok) {
326
+ return {
327
+ kind: "blocked",
328
+ blockers: [
329
+ runSurfaceBlocker({
330
+ code: "run_not_allocated",
331
+ summary: "A run journal could not be allocated.",
332
+ details: oneLine(allocated.rejections[0]?.message ?? "the store refused to allocate", 200),
333
+ remediation: { id: "check-the-store", summary: "Check that the repository's git common directory is writable." },
334
+ }),
335
+ ],
336
+ };
337
+ }
338
+ const runId = allocated.runId;
339
+
340
+ const payload =
341
+ typeof supplied === "object" && supplied !== null && displaced !== undefined
342
+ ? { ...(supplied as Record<string, unknown>), displacedRunId: displaced }
343
+ : supplied;
344
+
345
+ const appended = await store.append(
346
+ runId,
347
+ buildRunEvent({ runId, commonDir: surface.commonDir, kind: "run.started", role: "executor", payload, version,
348
+ ...(eventId === undefined ? {} : { eventId }) }),
349
+ );
350
+ if (!appended.ok) {
351
+ const first = appended.rejections[0];
352
+ await store.discard(runId);
353
+ return {
354
+ kind: "blocked",
355
+ blockers: [
356
+ runSurfaceBlocker({
357
+ code: "run_event_refused",
358
+ summary: "The run event was refused: run.started",
359
+ details: `run ${runId}: ${first === undefined ? "the store refused the append" : `${first.code} at ${oneLine(first.pointer, 64) || "/"}: ${oneLine(first.message, 200)}`}`,
360
+ remediation: {
361
+ id: "correct-the-event",
362
+ summary: `Correct the payload against the ${version} contract and emit again.`,
363
+ },
364
+ }),
365
+ ],
366
+ };
367
+ }
368
+
369
+ let pointed = await store.setCurrent(surface.worktreeKey, runId, { force });
370
+ let displacedStale = false;
371
+ // A STALE POINTER IS NOT A CONFLICT. `current` answers `undefined` both when
372
+ // no pointer exists and when one exists naming a journal this store can no
373
+ // longer read — a pruned store, a restored `.git`. In the second case the
374
+ // exclusive create below refuses, and without this the caller's only recourse
375
+ // is to retry, allocating one more orphan journal per attempt. Re-reading the
376
+ // pointer separates the two: a run that is now current is a genuine race and
377
+ // is still refused; a pointer that still names nothing is stale and is
378
+ // displaced, which is what the operator meant.
379
+ if (!pointed.ok && !force) {
380
+ const recheck = await store.current(surface.worktreeKey);
381
+ if (recheck.ok && recheck.runId === undefined) {
382
+ pointed = await store.setCurrent(surface.worktreeKey, runId, { force: true });
383
+ displacedStale = pointed.ok;
384
+ }
385
+ }
386
+ if (!pointed.ok) {
387
+ await store.discard(runId);
388
+ return {
389
+ kind: "blocked",
390
+ blockers: [
391
+ runSurfaceBlocker({
392
+ code: "run_pointer_refused",
393
+ summary: "The worktree pointer could not be written.",
394
+ details: `run ${runId}: ${oneLine(pointed.rejections[0]?.message ?? "the pointer was refused", 200)}`,
395
+ // `--run` cannot help here: `run.started` allocates rather than
396
+ // resolves, so it never reads that flag. `--force` is the one thing
397
+ // that displaces a pointer this command refused to overwrite.
398
+ remediation: { id: "force-the-start", summary: "Start the run with --force to displace the pointer that is already current." },
399
+ }),
400
+ ],
401
+ };
402
+ }
403
+
404
+ // A stale displacement has no id to record — the pointer named nothing this
405
+ // store could read, which is why it was displaceable at all — so the summary
406
+ // is the only place the operator learns it happened. It is also the only
407
+ // signal that the store itself needs attention: a pointer reaches this state
408
+ // through a pruned journal, a truncated write, or a permissions fault.
409
+ return {
410
+ kind: "ok",
411
+ summary: `started run ${runId}${
412
+ displaced === undefined
413
+ ? displacedStale
414
+ ? " (displaced a stale pointer naming no readable run; the run store may need attention)"
415
+ : ""
416
+ : ` (displaced ${displaced})`
417
+ }`,
418
+ };
419
+ }
@@ -1,3 +1,4 @@
1
+ import { runDeclaredCheck } from "../declared-checks.ts";
1
2
  /**
2
3
  * `gate` — evaluate the delivery gate and, under a TTY, offer a scoped waiver.
3
4
  *
@@ -9,29 +10,128 @@
9
10
  * only ever offers a waiver to a `human` context, all-or-nothing over waivable
10
11
  * findings; the CLI adds no waiver logic of its own.
11
12
  */
12
- import { runAdmission } from "@agent-delivery-harness/kernel";
13
- import type { CommandContext, CommandDescriptor, CommandResult } from "../boundary.ts";
13
+ import { computeDeliverableIdentity, runAdmission, type AdmissionResult, type Blocker, type LiveProviderResult } from "@agent-delivery-harness/kernel";
14
+ import { CliInterruption, type CommandContext, type CommandDescriptor, type CommandResult } from "../boundary.ts";
15
+ import { oneLine } from "../run-surface.ts";
16
+
17
+ /**
18
+ * Runs the ordinary admission first, invokes only configured providers that can
19
+ * answer the resulting missing-evidence/live-result blocks, then re-evaluates
20
+ * through the same admission adapter. Configs without provider commands take
21
+ * the pre-existing path unchanged.
22
+ */
23
+ export async function runProviderBackedAdmission(
24
+ context: CommandContext,
25
+ options: { readonly allowPrompt: boolean; readonly includeInjectedLiveResults: boolean },
26
+ ): Promise<AdmissionResult & { readonly observedLiveResults?: readonly LiveProviderResult[] }> {
27
+ const wiring = await context.wire();
28
+ const admissionOptions = {
29
+ captureCandidate: wiring.captureCandidate,
30
+ projectActivation: wiring.projectActivation,
31
+ ...wiring.storageOptions,
32
+ };
33
+ const input = {
34
+ rootDir: context.rootDir,
35
+ config: context.config,
36
+ context: context.classifyContext(),
37
+ ...(options.includeInjectedLiveResults && context.liveResults !== undefined ? { liveResults: context.liveResults } : {}),
38
+ };
39
+ const finalAdmissionOptions = {
40
+ ...admissionOptions,
41
+ ...(options.allowPrompt && context.promptForWaiver !== undefined ? { promptForWaiver: context.promptForWaiver } : {}),
42
+ };
43
+
44
+ if (!context.config.providers.some((provider) => provider.command !== undefined || provider.check !== undefined)) {
45
+ return runAdmission(input, finalAdmissionOptions);
46
+ }
47
+
48
+ const liveResults: LiveProviderResult[] = options.includeInjectedLiveResults ? [...(context.liveResults ?? [])] : [];
49
+ const attempted = new Set<string>();
50
+ const attemptBlockers: Blocker[] = [];
51
+ let admission = await runAdmission(input, admissionOptions);
52
+
53
+ while (!admission.admitted && admission.decision !== undefined && admission.candidate !== undefined) {
54
+ const requested = new Map<string, { obligationIds: string[]; requiresEvidence: boolean; needsLiveResult: boolean }>();
55
+ for (const resolution of admission.decision.resolutions) {
56
+ if (resolution.kind !== "blocked") continue;
57
+ const obligation = context.config.obligations.find((entry) => entry.id === resolution.obligationId);
58
+ if (obligation === undefined) continue;
59
+ const missingCode = obligation.freshness === "live" ? "live_provider_missing" : "review_evidence_missing";
60
+ for (const finding of resolution.providerFindings ?? []) {
61
+ if (finding.code !== missingCode || finding.providerId === undefined || attempted.has(finding.providerId)) continue;
62
+ const registration = context.config.providers.find((provider) => provider.id === finding.providerId);
63
+ if (registration === undefined || (registration.command === undefined && registration.check === undefined) || !obligation.providers.includes(registration.id)) continue;
64
+ 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.includes("review.green/1")))) continue;
65
+ const entry = requested.get(registration.id) ?? { obligationIds: [], requiresEvidence: false, needsLiveResult: false };
66
+ entry.obligationIds.push(obligation.id);
67
+ entry.requiresEvidence ||= obligation.freshness === "exact_candidate";
68
+ entry.needsLiveResult ||= obligation.freshness === "live";
69
+ requested.set(registration.id, entry);
70
+ }
71
+ }
72
+
73
+ const next = requested.entries().next().value as
74
+ | [string, { obligationIds: string[]; requiresEvidence: boolean; needsLiveResult: boolean }]
75
+ | undefined;
76
+ if (next === undefined) break;
77
+ const [providerId, request] = next;
78
+ attempted.add(providerId);
79
+ const registration = context.config.providers.find(provider => provider.id === providerId)!;
80
+ if (registration.check !== undefined) {
81
+ attemptBlockers.push(...await runDeclaredCheck(context, registration, request.obligationIds, admission.candidate));
82
+ admission = await runAdmission({ ...input, ...(liveResults.length === 0 ? {} : { liveResults }) }, admissionOptions);
83
+ continue;
84
+ }
85
+ const result = await context.invokeProvider?.({
86
+ providerId,
87
+ requiresEvidence: request.requiresEvidence,
88
+ payload: {
89
+ gateId: context.config.gateId,
90
+ providerId,
91
+ obligationIds: [...new Set(request.obligationIds)].sort(),
92
+ candidate: admission.candidate,
93
+ },
94
+ });
95
+ if (result === undefined) continue;
96
+ if (result.kind === "interrupted") throw new CliInterruption("Provider invocation interrupted before a trustworthy terminal outcome.");
97
+ if (result.kind === "blocked") {
98
+ attemptBlockers.push(...result.blockers);
99
+ if (request.needsLiveResult) {
100
+ liveResults.push({ providerId, runId: result.runId, status: "failed", findings: [] });
101
+ }
102
+ } else if (request.needsLiveResult) {
103
+ liveResults.push(result.liveResult);
104
+ }
105
+
106
+ admission = await runAdmission(
107
+ { ...input, ...(liveResults.length === 0 ? {} : { liveResults }) },
108
+ admissionOptions,
109
+ );
110
+ }
111
+
112
+ if (admission.admitted) return { ...admission, observedLiveResults: liveResults };
113
+ const final = await runAdmission(
114
+ { ...input, ...(liveResults.length === 0 ? {} : { liveResults }) },
115
+ finalAdmissionOptions,
116
+ );
117
+ return attemptBlockers.length === 0 || final.admitted
118
+ ? { ...final, observedLiveResults: liveResults }
119
+ : { ...final, observedLiveResults: liveResults, blockers: [...attemptBlockers, ...final.blockers] };
120
+ }
14
121
 
15
122
  export const gateCommand: CommandDescriptor = {
16
123
  name: "gate",
17
124
  sourceId: "delivery-harness.cli.gate",
18
125
  summary: "Evaluate the delivery gate for the current candidate.",
126
+ usage: "Usage: delivery-harness gate\nTakes no arguments; a waiver is offered only under a real TTY.",
19
127
  async run(context: CommandContext): Promise<CommandResult> {
20
- const wiring = await context.wire();
21
- const result = await runAdmission(
22
- {
23
- rootDir: context.rootDir,
24
- config: context.config,
25
- context: context.classifyContext(),
26
- ...(context.liveResults === undefined ? {} : { liveResults: context.liveResults }),
27
- },
28
- {
29
- captureCandidate: wiring.captureCandidate,
30
- projectActivation: wiring.projectActivation,
31
- ...(context.promptForWaiver === undefined ? {} : { promptForWaiver: context.promptForWaiver }),
32
- ...wiring.storageOptions,
33
- },
34
- );
128
+ // Arguments before admission: `gate` accepts none, and a call it cannot
129
+ // honour is answered about the call rather than performed approximately.
130
+ const unexpected = context.args[0];
131
+ if (unexpected !== undefined) {
132
+ return { kind: "usage", message: `gate takes no arguments, and ${oneLine(unexpected, 64)} is one.\n${gateCommand.usage}` };
133
+ }
134
+ const result = await runProviderBackedAdmission(context, { allowPrompt: true, includeInjectedLiveResults: true });
35
135
 
36
136
  if (result.admitted) {
37
137
  const waiverNote =
@@ -39,7 +139,16 @@ export const gateCommand: CommandDescriptor = {
39
139
  ? ` (waived: ${result.waivedObligationIds.join(", ")})`
40
140
  : "";
41
141
  const kinds = (result.decision?.resolutions ?? []).map((resolution) => `${resolution.obligationId}=${resolution.kind}`);
42
- return { kind: "ok", summary: `admitted${waiverNote}: ${kinds.join(", ")}` };
142
+ // This observation describes the admitted immutable tree. Failure to
143
+ // observe it cannot change admission; the run journal is not evidence.
144
+ let digest: string | undefined;
145
+ if (result.candidate !== undefined) {
146
+ try {
147
+ digest = await computeDeliverableIdentity({ rootDir: context.rootDir, treeSha: result.candidate.treeSha,
148
+ config: { ...context.config, computingIdentityVersion: "validation-tree/v1", reviewNeutral: context.config.recordNeutral } });
149
+ } catch { /* Best-effort observability, like the completion append. */ }
150
+ }
151
+ return { kind: "ok", summary: `admitted${waiverNote}: ${kinds.join(", ")}`, ...(digest === undefined ? {} : { digest }) };
43
152
  }
44
153
  return { kind: "blocked", blockers: [...result.blockers] };
45
154
  },