@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.
- package/LICENSE +104 -201
- package/NOTICE +1 -1
- package/package.json +10 -5
- package/src/boundary.ts +302 -9
- package/src/command-activity.ts +75 -0
- package/src/commands/admit.ts +106 -0
- package/src/commands/check.ts +13 -2
- package/src/commands/emit-review-evidence.ts +38 -0
- package/src/commands/emit.ts +419 -0
- package/src/commands/gate.ts +127 -18
- package/src/commands/maintain.ts +203 -0
- package/src/commands/managed.ts +478 -0
- package/src/commands/prepare.ts +123 -15
- package/src/commands/record.ts +100 -8
- package/src/commands/resume.ts +53 -0
- package/src/commands/review-context.ts +19 -0
- package/src/commands/runs.ts +640 -0
- package/src/commands/save-context.ts +51 -0
- package/src/commands/submit-evidence.ts +46 -7
- package/src/commands/verify.ts +173 -5
- package/src/declared-checks.ts +50 -0
- package/src/index.ts +40 -4
- package/src/main.ts +38 -42
- package/src/ordinary-context.ts +99 -0
- package/src/provider-rails.ts +24 -0
- package/src/record-retention.ts +325 -0
- package/src/review-evidence.ts +284 -0
- package/src/run-archive-commands.ts +164 -0
- package/src/run-archive.ts +127 -0
- package/src/run-artifact-commands.ts +163 -0
- package/src/run-attachments.ts +112 -0
- package/src/run-export.ts +164 -0
- package/src/run-live.ts +102 -0
- package/src/run-projection.ts +396 -0
- package/src/run-server.ts +905 -0
- package/src/run-surface.ts +334 -0
- package/src/run-view-command.ts +77 -0
- package/src/run-view-html.ts +138 -0
- package/src/run-view-record.ts +119 -0
- package/src/run-view.ts +343 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `maintain` — the installation-scoped maintenance lane: update, rollback, and
|
|
3
|
+
* trust-state maintenance.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS A SEPARATE COMMAND. `managed` addresses one delivery and its
|
|
6
|
+
* checkpoints; these operations address the installation itself and touch no
|
|
7
|
+
* delivery journal at all. Folding them into `managed` would have made a
|
|
8
|
+
* delivery resolve before an operator could repair the installation that
|
|
9
|
+
* delivery is blocked on — the wrong order for exactly the case the lane
|
|
10
|
+
* exists to serve.
|
|
11
|
+
*
|
|
12
|
+
* WHAT DEFENDS THIS SURFACE, PRECISELY. Every operation here consumes a
|
|
13
|
+
* maintenance-lane sensitive assertion bound to the target installation and
|
|
14
|
+
* generation identities, evaluated by the installation's configured assertion
|
|
15
|
+
* source in the model-external lane, and consumed BEFORE any byte moves. The
|
|
16
|
+
* caller cannot mint that assertion and cannot answer the evaluation: the
|
|
17
|
+
* operating system evaluates it and the product never sees a credential. So a
|
|
18
|
+
* model that invokes one of these cannot complete it.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IT DOES NOT DEFEND, STATED PLAINLY BECAUSE IT IS EASY TO ASSUME
|
|
21
|
+
* OTHERWISE. It does not stop the prompt from being raised. Probing an
|
|
22
|
+
* OS-native source checks that the platform's authentication surfaces exist,
|
|
23
|
+
* and existence is independent of whether the caller holds a terminal — so on a
|
|
24
|
+
* desktop platform a non-interactive session can still reach `evaluate`, which
|
|
25
|
+
* raises an interactive dialog in the operator's own session. Authorization
|
|
26
|
+
* holds, because nobody but the operator can answer it; what is not defended is
|
|
27
|
+
* the operator's attention. Prompt fatigue is the residual risk on this surface,
|
|
28
|
+
* and it is the reason to keep the lane narrow rather than to believe it closed.
|
|
29
|
+
*
|
|
30
|
+
* Operator confirmations are the class that must never be nameable on a shell
|
|
31
|
+
* surface, and none of them appear here.
|
|
32
|
+
*
|
|
33
|
+
* The installation is located the same way `managed` locates it: from the
|
|
34
|
+
* product namespace's pointer under the common git directory, never a
|
|
35
|
+
* candidate-writable path.
|
|
36
|
+
*/
|
|
37
|
+
import { execFile } from "node:child_process";
|
|
38
|
+
import { readFile } from "node:fs/promises";
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
import { compiledAdopterPolicyBindingDigest, createManagedDeliveryFacade, type CompiledAdopterPolicyBinding, type ManagedDeliveryFacade } from "@agent-delivery-harness/kernel";
|
|
41
|
+
import { commandBlocker } from "../boundary.ts";
|
|
42
|
+
import type { CommandContext, CommandDescriptor, CommandResult } from "../boundary.ts";
|
|
43
|
+
|
|
44
|
+
const SOURCE_ID = "delivery-harness.cli.maintain";
|
|
45
|
+
|
|
46
|
+
/** Every operation this command answers, in the order its usage lists them. */
|
|
47
|
+
const MAINTAIN_OPERATIONS: readonly string[] = Object.freeze([
|
|
48
|
+
"update",
|
|
49
|
+
"rollback",
|
|
50
|
+
"pin",
|
|
51
|
+
"revoke",
|
|
52
|
+
"unrevoke",
|
|
53
|
+
"advance-high-water-mark",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const blocked = (code: string, summary: string, remediation: string): CommandResult => ({
|
|
57
|
+
kind: "blocked",
|
|
58
|
+
blockers: [
|
|
59
|
+
commandBlocker({
|
|
60
|
+
code,
|
|
61
|
+
sourceId: SOURCE_ID,
|
|
62
|
+
summary,
|
|
63
|
+
remediations: [{ id: `${code.replaceAll("_", "-")}-remediation`, kind: "manual_action", summary: remediation }],
|
|
64
|
+
}),
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const gitCommonDir = (cwd: string): Promise<string | undefined> =>
|
|
69
|
+
new Promise((resolve) => {
|
|
70
|
+
execFile("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd, encoding: "utf8" }, (error, stdout) => {
|
|
71
|
+
resolve(error === null ? stdout.trim() : undefined);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const flag = (args: readonly string[], name: string): string | undefined => {
|
|
76
|
+
const index = args.indexOf(name);
|
|
77
|
+
return index === -1 ? undefined : args[index + 1];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function nowInstant(): string {
|
|
81
|
+
return `${new Date().toISOString().slice(0, 19)}Z`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function resolveFacade(context: CommandContext): Promise<ManagedDeliveryFacade | CommandResult> {
|
|
85
|
+
const common = await gitCommonDir(context.rootDir);
|
|
86
|
+
if (common === undefined) {
|
|
87
|
+
return blocked("not_a_repository", "The working directory is not a git repository.", "Run from the repository the product is installed for.");
|
|
88
|
+
}
|
|
89
|
+
let pointer: { installationPath: string; receiptDir: string; hostVersion: string; policyBindingDigest: string };
|
|
90
|
+
try {
|
|
91
|
+
pointer = JSON.parse(await readFile(path.join(common, "managed-delivery", "facade.json"), "utf8")) as typeof pointer;
|
|
92
|
+
} catch {
|
|
93
|
+
return blocked(
|
|
94
|
+
"no_managed_installation",
|
|
95
|
+
"No managed installation is registered for this repository.",
|
|
96
|
+
"Install the composition and register a delivery first.",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
let policyBinding: CompiledAdopterPolicyBinding | undefined = context.policyBinding;
|
|
100
|
+
if (policyBinding === undefined) {
|
|
101
|
+
try {
|
|
102
|
+
policyBinding = JSON.parse(await readFile(path.join(common, "managed-delivery", "policy-binding.json"), "utf8")) as CompiledAdopterPolicyBinding;
|
|
103
|
+
} catch {
|
|
104
|
+
return blocked(
|
|
105
|
+
"policy_binding_missing",
|
|
106
|
+
"No compiled adopter policy binding is retained for this installation.",
|
|
107
|
+
"Register a delivery through an adopter binding, or pass one through the embedding runtime.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (pointer.policyBindingDigest !== compiledAdopterPolicyBindingDigest(policyBinding)) {
|
|
112
|
+
return blocked(
|
|
113
|
+
"policy_binding_mismatch",
|
|
114
|
+
"The retained compiled adopter policy binding does not match the product namespace pointer.",
|
|
115
|
+
"Restore the exact binding captured at registration; drift requires a new owner-approved delivery.",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return createManagedDeliveryFacade({
|
|
119
|
+
repoDir: context.rootDir,
|
|
120
|
+
policyBinding,
|
|
121
|
+
installation: { installationPath: pointer.installationPath, receiptDir: pointer.receiptDir },
|
|
122
|
+
hostVersion: pointer.hostVersion,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isCommandResult = (value: ManagedDeliveryFacade | CommandResult): value is CommandResult => "kind" in value;
|
|
127
|
+
|
|
128
|
+
export const maintainCommand: CommandDescriptor = {
|
|
129
|
+
name: "maintain",
|
|
130
|
+
sourceId: SOURCE_ID,
|
|
131
|
+
summary: "Maintain the product installation (update, rollback, trust-state pin/revoke/unrevoke/high-water-mark).",
|
|
132
|
+
usage: `Usage: delivery-harness maintain <operation> [options]\nOperations: ${MAINTAIN_OPERATIONS.join(" | ")}`,
|
|
133
|
+
async run(context: CommandContext): Promise<CommandResult> {
|
|
134
|
+
const [operation, ...rest] = context.args;
|
|
135
|
+
if (operation === undefined) {
|
|
136
|
+
return { kind: "usage", message: `maintain requires an operation: ${MAINTAIN_OPERATIONS.join(" | ")}` };
|
|
137
|
+
}
|
|
138
|
+
// A typo is a usage error about the call, answered before the repository
|
|
139
|
+
// or the installation is consulted.
|
|
140
|
+
if (!MAINTAIN_OPERATIONS.includes(operation)) {
|
|
141
|
+
return { kind: "usage", message: `Unknown maintain operation: ${operation}.` };
|
|
142
|
+
}
|
|
143
|
+
const resolved = await resolveFacade(context);
|
|
144
|
+
if (isCommandResult(resolved)) return resolved;
|
|
145
|
+
const facade = resolved;
|
|
146
|
+
const now = nowInstant();
|
|
147
|
+
const emit = (value: unknown): void => context.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
148
|
+
|
|
149
|
+
switch (operation) {
|
|
150
|
+
case "update": {
|
|
151
|
+
const packedDir = flag(rest, "--packed");
|
|
152
|
+
if (packedDir === undefined) {
|
|
153
|
+
return { kind: "usage", message: "update requires --packed <dir> naming the verified packed generation to install." };
|
|
154
|
+
}
|
|
155
|
+
const outcome = await facade.updateComposition({ packedDir: path.resolve(context.rootDir, packedDir), now });
|
|
156
|
+
if (!outcome.ok) return { kind: "blocked", blockers: [...outcome.blockers] };
|
|
157
|
+
emit(outcome);
|
|
158
|
+
return {
|
|
159
|
+
kind: "ok",
|
|
160
|
+
summary: outcome.noOp
|
|
161
|
+
? `already at ${outcome.generationDigest}`
|
|
162
|
+
: `updated ${outcome.priorGenerationDigest} -> ${outcome.generationDigest}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
case "rollback": {
|
|
166
|
+
const target = flag(rest, "--generation");
|
|
167
|
+
if (target === undefined) {
|
|
168
|
+
return { kind: "usage", message: "rollback requires --generation <digest> naming a previously accepted generation." };
|
|
169
|
+
}
|
|
170
|
+
const outcome = await facade.rollbackComposition({ targetGenerationDigest: target, now });
|
|
171
|
+
if (!outcome.ok) return { kind: "blocked", blockers: [...outcome.blockers] };
|
|
172
|
+
return { kind: "ok", summary: `rolled back to ${outcome.generationDigest}` };
|
|
173
|
+
}
|
|
174
|
+
case "pin":
|
|
175
|
+
case "revoke":
|
|
176
|
+
case "unrevoke": {
|
|
177
|
+
const generationDigest = flag(rest, "--generation");
|
|
178
|
+
if (generationDigest === undefined) {
|
|
179
|
+
return { kind: "usage", message: `${operation} requires --generation <digest>.` };
|
|
180
|
+
}
|
|
181
|
+
const outcome = await facade.maintainTrustState({ operation, generationDigest, now });
|
|
182
|
+
if (!outcome.ok) return { kind: "blocked", blockers: [...outcome.blockers] };
|
|
183
|
+
emit(outcome.state);
|
|
184
|
+
return { kind: "ok", summary: `${operation} recorded at revocation epoch ${outcome.state.revocationEpoch}` };
|
|
185
|
+
}
|
|
186
|
+
case "advance-high-water-mark": {
|
|
187
|
+
const raw = flag(rest, "--to");
|
|
188
|
+
const highWaterMark = raw === undefined ? Number.NaN : Number.parseInt(raw, 10);
|
|
189
|
+
if (!Number.isInteger(highWaterMark)) {
|
|
190
|
+
return { kind: "usage", message: "advance-high-water-mark requires --to <integer>." };
|
|
191
|
+
}
|
|
192
|
+
const outcome = await facade.maintainTrustState({ operation: "advance-high-water-mark", highWaterMark, now });
|
|
193
|
+
if (!outcome.ok) return { kind: "blocked", blockers: [...outcome.blockers] };
|
|
194
|
+
emit(outcome.state);
|
|
195
|
+
return { kind: "ok", summary: `high-water mark is ${outcome.state.highWaterMark}` };
|
|
196
|
+
}
|
|
197
|
+
default:
|
|
198
|
+
// Unreachable: checked against MAINTAIN_OPERATIONS above. Kept so a
|
|
199
|
+
// name added to that list without a case here fails loudly.
|
|
200
|
+
return { kind: "usage", message: `Unknown maintain operation: ${operation}.` };
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
@@ -0,0 +1,478 @@
|
|
|
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
|
+
usage: `Usage: delivery-harness managed <operation> [options]\nOperations: ${MANAGED_OPERATIONS.join(" | ")}`,
|
|
232
|
+
async run(context: CommandContext): Promise<CommandResult> {
|
|
233
|
+
const [operation, ...rest] = context.args;
|
|
234
|
+
if (operation === undefined) {
|
|
235
|
+
return {
|
|
236
|
+
kind: "usage",
|
|
237
|
+
message:
|
|
238
|
+
`managed requires an operation: ${MANAGED_OPERATIONS.join(" | ")}`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// An unknown operation is a usage error about the CALL, and it is answered
|
|
242
|
+
// before anything about the repository is consulted: a typo answered with
|
|
243
|
+
// "this is not a repository" sends a reader to fix the wrong thing.
|
|
244
|
+
if (!MANAGED_OPERATIONS.includes(operation)) {
|
|
245
|
+
return { kind: "usage", message: `Unknown managed operation: ${operation}.` };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The inspectable contract: every operation, what it costs, whether it
|
|
249
|
+
// binds the fence, and which surfaces reach it. It describes the product,
|
|
250
|
+
// not a delivery, so it answers before any delivery has to resolve —
|
|
251
|
+
// otherwise the one operation that explains the surface would be
|
|
252
|
+
// unavailable exactly when a reader most needs it.
|
|
253
|
+
if (operation === "operations") {
|
|
254
|
+
context.write(`${JSON.stringify(FACADE_OPERATIONS, null, 2)}\n`);
|
|
255
|
+
return { kind: "ok", summary: `${FACADE_OPERATIONS.length} facade operations` };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Only the retention operations may name a delivery; everything else binds
|
|
259
|
+
// the fence of the worktree it runs in.
|
|
260
|
+
const RETENTION_OPERATIONS = ["export", "delete"];
|
|
261
|
+
// Every flag on this surface is space-separated, and this one is refused in
|
|
262
|
+
// the GNU `--flag=value` form rather than parsed: accepting it here alone
|
|
263
|
+
// would make the convention inconsistent, and letting it through unread is
|
|
264
|
+
// the silent-retarget hazard below by another spelling — `indexOf` never
|
|
265
|
+
// matches `--delivery=id`, so it would fall back to the implicitly resolved
|
|
266
|
+
// delivery and point a destructive operation somewhere the operator did not
|
|
267
|
+
// name.
|
|
268
|
+
if (rest.some((argument) => argument.startsWith("--delivery="))) {
|
|
269
|
+
return { kind: "usage", message: "--delivery takes its value as a separate argument: --delivery <id>." };
|
|
270
|
+
}
|
|
271
|
+
const namesDelivery = rest.includes("--delivery");
|
|
272
|
+
if (namesDelivery && !RETENTION_OPERATIONS.includes(operation)) {
|
|
273
|
+
return { kind: "usage", message: `--delivery is accepted only by: ${RETENTION_OPERATIONS.join(", ")}.` };
|
|
274
|
+
}
|
|
275
|
+
const requestedDelivery = RETENTION_OPERATIONS.includes(operation) ? flag(rest, "--delivery") : undefined;
|
|
276
|
+
// A trailing `--delivery` with no value reads as absent, and absent falls
|
|
277
|
+
// back to the implicitly resolved delivery. On `delete` that is a typo
|
|
278
|
+
// silently retargeting a destructive operation at a different delivery, so
|
|
279
|
+
// the flag's presence without a value is a refusal rather than a default.
|
|
280
|
+
if (namesDelivery && requestedDelivery === undefined) {
|
|
281
|
+
return { kind: "usage", message: "--delivery requires a delivery id." };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const resolved = await resolveManaged(context, requestedDelivery);
|
|
285
|
+
if (isCommandResult(resolved)) return resolved;
|
|
286
|
+
const { facade, deliveryId, fence } = resolved;
|
|
287
|
+
const requireFence = (): number | CommandResult =>
|
|
288
|
+
fence ??
|
|
289
|
+
blocked(
|
|
290
|
+
"workspace_superseded",
|
|
291
|
+
"This worktree is not the delivery's currently bound workspace, so it carries no invocation fence.",
|
|
292
|
+
"Drive checkpoints from the bound worktree; a superseded task's outputs are permanently rejected.",
|
|
293
|
+
);
|
|
294
|
+
const emit = (value: unknown): void => context.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The typed stage-result document a checkpoint submits. Prose cannot
|
|
298
|
+
* advance the reducer, so these operations read a
|
|
299
|
+
* `workflow-stage-result/1` file rather than accepting free text.
|
|
300
|
+
*/
|
|
301
|
+
const resultFileArg = async (operation: string): Promise<string | CommandResult> => {
|
|
302
|
+
const file = flag(rest, "--result-file");
|
|
303
|
+
if (file === undefined) {
|
|
304
|
+
return {
|
|
305
|
+
kind: "usage",
|
|
306
|
+
message: `${operation} requires --result-file <path> containing a typed workflow-stage-result/1 document; prose cannot advance a checkpoint.`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
return await readFile(path.resolve(context.rootDir, file), "utf8");
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return { kind: "usage", message: `${operation} could not read ${file}: ${error instanceof Error ? error.message : String(error)}` };
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
switch (operation) {
|
|
317
|
+
case "status": {
|
|
318
|
+
const status = await facade.status({ deliveryId, observedAt: nowInstant() });
|
|
319
|
+
if (!status.ok) return { kind: "blocked", blockers: [...status.blockers] };
|
|
320
|
+
emit(status.status);
|
|
321
|
+
return {
|
|
322
|
+
kind: "ok",
|
|
323
|
+
summary: `state ${status.status.delivery.state}; host ${status.status.hostActivity}; next ${status.status.nextCheckpoint.kind}`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
case "blockers": {
|
|
327
|
+
const inventory = await facade.blockerInventory({ deliveryId });
|
|
328
|
+
if (!inventory.ok) return { kind: "blocked", blockers: [...inventory.blockers] };
|
|
329
|
+
emit(inventory.entries);
|
|
330
|
+
return { kind: "ok", summary: `${inventory.entries.length} blocker(s) journaled` };
|
|
331
|
+
}
|
|
332
|
+
case "propose-approval": {
|
|
333
|
+
const requestKind = flag(rest, "--kind");
|
|
334
|
+
const criterionId = flag(rest, "--criterion");
|
|
335
|
+
const actorId = flag(rest, "--actor");
|
|
336
|
+
const reason = flag(rest, "--reason");
|
|
337
|
+
if ((requestKind !== "waiver" && requestKind !== "amendment") || criterionId === undefined || actorId === undefined || reason === undefined) {
|
|
338
|
+
return {
|
|
339
|
+
kind: "usage",
|
|
340
|
+
message: "propose-approval requires --kind <waiver|amendment> --criterion <id> --actor <id> --reason <text>.",
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const invokingFence = requireFence();
|
|
344
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
345
|
+
// Proposing is all this does. The approval half is the sensitive lane's
|
|
346
|
+
// and needs a fresh assertion the proposer cannot mint.
|
|
347
|
+
const proposed = await facade.recordApprovalRequest({
|
|
348
|
+
deliveryId,
|
|
349
|
+
requestKind,
|
|
350
|
+
criterionId,
|
|
351
|
+
actorId,
|
|
352
|
+
reason,
|
|
353
|
+
fence: invokingFence,
|
|
354
|
+
});
|
|
355
|
+
if (!proposed.ok) return { kind: "blocked", blockers: [...proposed.blockers] };
|
|
356
|
+
return { kind: "ok", summary: `${requestKind} proposed for ${criterionId}; delivery is ${proposed.state}` };
|
|
357
|
+
}
|
|
358
|
+
case "request-cancellation": {
|
|
359
|
+
const requested = await facade.requestCancellation({ deliveryId });
|
|
360
|
+
if (!requested.ok) return { kind: "blocked", blockers: [...requested.blockers] };
|
|
361
|
+
return { kind: "ok", summary: `cancellation requested; delivery is ${requested.state}` };
|
|
362
|
+
}
|
|
363
|
+
case "finalize-cancellation": {
|
|
364
|
+
const finalized = await facade.finalizeCancellation({ deliveryId });
|
|
365
|
+
if (!finalized.ok) return { kind: "blocked", blockers: [...finalized.blockers] };
|
|
366
|
+
return { kind: "ok", summary: `prior workspace quarantined; delivery is ${finalized.state}` };
|
|
367
|
+
}
|
|
368
|
+
case "export": {
|
|
369
|
+
const exported = await facade.exportDelivery({ deliveryId });
|
|
370
|
+
if (!exported.ok) return { kind: "blocked", blockers: [...exported.blockers] };
|
|
371
|
+
emit({ exportPath: exported.exportPath, artifactDigest: exported.artifactDigest });
|
|
372
|
+
return { kind: "ok", summary: `exported to ${exported.exportPath}` };
|
|
373
|
+
}
|
|
374
|
+
case "delete": {
|
|
375
|
+
const deleted = await facade.deleteDelivery({ deliveryId });
|
|
376
|
+
if (!deleted.ok) return { kind: "blocked", blockers: [...deleted.blockers] };
|
|
377
|
+
emit({ preservedAuditRecords: deleted.preservedAuditRecords });
|
|
378
|
+
return { kind: "ok", summary: `deleted; ${deleted.preservedAuditRecords.length} audit record(s) preserved` };
|
|
379
|
+
}
|
|
380
|
+
case "recover": {
|
|
381
|
+
const target = flag(rest, "--generation");
|
|
382
|
+
const recovered = await facade.recoverSecurityBlocked({
|
|
383
|
+
deliveryId,
|
|
384
|
+
now: nowInstant(),
|
|
385
|
+
...(target === undefined ? {} : { targetGenerationDigest: target }),
|
|
386
|
+
});
|
|
387
|
+
if (!recovered.ok) return { kind: "blocked", blockers: [...recovered.blockers] };
|
|
388
|
+
return { kind: "ok", summary: `${recovered.mode}; delivery is ${recovered.state}` };
|
|
389
|
+
}
|
|
390
|
+
case "next": {
|
|
391
|
+
const next = await facade.nextCheckpoint({ deliveryId });
|
|
392
|
+
if (!next.ok) return { kind: "blocked", blockers: [...next.blockers] };
|
|
393
|
+
emit(next.checkpoint);
|
|
394
|
+
return { kind: "ok", summary: `next checkpoint: ${next.checkpoint.kind}` };
|
|
395
|
+
}
|
|
396
|
+
case "submit-plan": {
|
|
397
|
+
const invokingFence = requireFence();
|
|
398
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
399
|
+
const resultBytes = await resultFileArg("submit-plan");
|
|
400
|
+
if (typeof resultBytes !== "string") return resultBytes;
|
|
401
|
+
const submitted = await facade.submitStageResult({ deliveryId, stageId: "plan", resultBytes, fence: invokingFence });
|
|
402
|
+
if (!submitted.ok) return { kind: "blocked", blockers: [...submitted.blockers] };
|
|
403
|
+
return { kind: "ok", summary: `plan accepted; delivery is ${submitted.state}` };
|
|
404
|
+
}
|
|
405
|
+
case "checkpoint": {
|
|
406
|
+
const invokingFence = requireFence();
|
|
407
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
408
|
+
const resultBytes = await resultFileArg("checkpoint");
|
|
409
|
+
if (typeof resultBytes !== "string") return resultBytes;
|
|
410
|
+
const checkpointed = await facade.checkpointCandidate({ deliveryId, resultBytes, fence: invokingFence });
|
|
411
|
+
if (!checkpointed.ok) return { kind: "blocked", blockers: [...checkpointed.blockers] };
|
|
412
|
+
return { kind: "ok", summary: `candidate ${checkpointed.treeSha} checkpointed; delivery is ${checkpointed.state}` };
|
|
413
|
+
}
|
|
414
|
+
case "run-sensor": {
|
|
415
|
+
const invokingFence = requireFence();
|
|
416
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
417
|
+
const sensed = await facade.runSensor({ deliveryId, fence: invokingFence });
|
|
418
|
+
if (!sensed.ok) return { kind: "blocked", blockers: [...sensed.blockers] };
|
|
419
|
+
return { kind: "ok", summary: `sensor ${sensed.outcome}; delivery is ${sensed.state}` };
|
|
420
|
+
}
|
|
421
|
+
case "reduce-review": {
|
|
422
|
+
const invokingFence = requireFence();
|
|
423
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
424
|
+
const reduced = await facade.reduceReview({ deliveryId, fence: invokingFence });
|
|
425
|
+
if (!reduced.ok) return { kind: "blocked", blockers: [...reduced.blockers] };
|
|
426
|
+
return { kind: "ok", summary: `review reduced; delivery is ${reduced.state}` };
|
|
427
|
+
}
|
|
428
|
+
case "compound": {
|
|
429
|
+
const invokingFence = requireFence();
|
|
430
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
431
|
+
const resultBytes = await resultFileArg("compound");
|
|
432
|
+
if (typeof resultBytes !== "string") return resultBytes;
|
|
433
|
+
const compounded = await facade.submitStageResult({ deliveryId, stageId: "compound", resultBytes, fence: invokingFence });
|
|
434
|
+
if (!compounded.ok) return { kind: "blocked", blockers: [...compounded.blockers] };
|
|
435
|
+
return { kind: "ok", summary: `compound recorded; delivery is ${compounded.state}` };
|
|
436
|
+
}
|
|
437
|
+
case "admit": {
|
|
438
|
+
const invokingFence = requireFence();
|
|
439
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
440
|
+
const admitted = await facade.admit({ deliveryId, recordedAtInstant: nowInstant(), env: context.env, fence: invokingFence });
|
|
441
|
+
if (!admitted.ok) return { kind: "blocked", blockers: [...admitted.blockers] };
|
|
442
|
+
return { kind: "ok", summary: `admitted; delivery is ${admitted.state}` };
|
|
443
|
+
}
|
|
444
|
+
case "prepare-record": {
|
|
445
|
+
const invokingFence = requireFence();
|
|
446
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
447
|
+
const prepared = await facade.prepareTrackedRecord({ deliveryId, env: context.env, fence: invokingFence });
|
|
448
|
+
if (!prepared.ok) return { kind: "blocked", blockers: [...prepared.blockers] };
|
|
449
|
+
return { kind: "ok", summary: `tracked record written at ${prepared.relativePath}; commit it through native git tooling` };
|
|
450
|
+
}
|
|
451
|
+
case "confirm-record": {
|
|
452
|
+
const invokingFence = requireFence();
|
|
453
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
454
|
+
const confirmed = await facade.confirmTrackedRecord({ deliveryId, fence: invokingFence });
|
|
455
|
+
if (!confirmed.ok) return { kind: "blocked", blockers: [...confirmed.blockers] };
|
|
456
|
+
return { kind: "ok", summary: `tracked record verified; delivery is ${confirmed.state}` };
|
|
457
|
+
}
|
|
458
|
+
case "finish": {
|
|
459
|
+
const invokingFence = requireFence();
|
|
460
|
+
if (typeof invokingFence !== "number") return invokingFence;
|
|
461
|
+
const finished = await facade.completeFinishLine({ deliveryId, fence: invokingFence });
|
|
462
|
+
if (!finished.ok) return { kind: "blocked", blockers: [...finished.blockers] };
|
|
463
|
+
return { kind: "ok", summary: `merge-ready; delivery is ${finished.state} (result ${finished.resultDigest})` };
|
|
464
|
+
}
|
|
465
|
+
case "explain-blocker": {
|
|
466
|
+
const explained = await facade.explainBlocker({ deliveryId });
|
|
467
|
+
if (!explained.ok) return { kind: "blocked", blockers: [...explained.blockers] };
|
|
468
|
+
emit(explained.blocker ?? { blocker: null });
|
|
469
|
+
return { kind: "ok", summary: explained.blocker === undefined ? "no blocker recorded" : `blocker ${explained.blocker.code}` };
|
|
470
|
+
}
|
|
471
|
+
default:
|
|
472
|
+
// Unreachable: the operation was checked against MANAGED_OPERATIONS
|
|
473
|
+
// before the delivery resolved. Kept so adding a name to that list
|
|
474
|
+
// without a case here fails loudly rather than silently doing nothing.
|
|
475
|
+
return { kind: "usage", message: `Unknown managed operation: ${operation}.` };
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
};
|