@abelxiaoxing/cadence 1.4.1 → 1.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/README.md +74 -6
- package/config/plan-draft.quick.example.json +78 -0
- package/package.json +6 -2
- package/prompts/abel-design.md +8 -0
- package/prompts/abel-implement.md +9 -0
- package/src/change-verification.ts +25 -1
- package/src/child-budget.ts +58 -0
- package/src/child-model.ts +17 -4
- package/src/child-session.ts +20 -24
- package/src/contracts.ts +16 -0
- package/src/delivery-compiler.ts +2 -0
- package/src/durable-contracts.ts +3 -0
- package/src/execution-profile.ts +38 -0
- package/src/isolation-backend.ts +62 -40
- package/src/operator-cli-entry.mjs +24 -0
- package/src/operator-cli.mjs +1421 -0
- package/src/operator-tools.ts +249 -0
- package/src/package-candidate.ts +3 -0
- package/src/package-verification.ts +86 -8
- package/src/package-workflow.ts +73 -35
- package/src/plan-draft-summary.ts +4 -1
- package/src/single-task-draft.ts +142 -0
- package/src/transport-budget.ts +21 -1
- package/src/verification-capability.ts +5 -3
- package/src/verification-diagnostics.ts +76 -0
- package/src/verification-environment-io.ts +280 -0
- package/src/verification-environment.ts +56 -154
- package/src/verification-scan.ts +64 -0
- package/src/worker-broker.ts +2 -0
- package/src/workspace-io-entry.mjs +19 -1
- package/src/workspace-io-worker.mjs +268 -50
- package/src/workspace-io.ts +6 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { executionProfile } from "./execution-profile.ts";
|
|
6
|
+
import { resolveOpenSpecInvocation } from "./openspec-cli.ts";
|
|
7
|
+
import { resolveStateRoot } from "./state-root.ts";
|
|
8
|
+
import { requestBounds } from "./transport-budget.ts";
|
|
9
|
+
import { validateVerificationAdapterCapability } from "./verification-capability.ts";
|
|
10
|
+
|
|
11
|
+
export interface DoctorCheck {
|
|
12
|
+
name: string;
|
|
13
|
+
ok: boolean;
|
|
14
|
+
detail: string;
|
|
15
|
+
nextStep?: string;
|
|
16
|
+
}
|
|
17
|
+
export function inspectConsumer(root: string) {
|
|
18
|
+
root = realpathSync(root);
|
|
19
|
+
const checks: DoctorCheck[] = [];
|
|
20
|
+
const check = (name: string, action: () => string, nextStep: string) => {
|
|
21
|
+
try {
|
|
22
|
+
checks.push({ name, ok: true, detail: action() });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
checks.push({
|
|
25
|
+
name,
|
|
26
|
+
ok: false,
|
|
27
|
+
detail: error instanceof Error ? error.message : "unavailable",
|
|
28
|
+
nextStep,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
check(
|
|
33
|
+
"node",
|
|
34
|
+
() => {
|
|
35
|
+
if (
|
|
36
|
+
Number(process.versions.node.split(".")[0]) < 22 ||
|
|
37
|
+
(Number(process.versions.node.split(".")[0]) === 22 &&
|
|
38
|
+
Number(process.versions.node.split(".")[1]) < 13)
|
|
39
|
+
)
|
|
40
|
+
throw new Error("Node 22.13+ required");
|
|
41
|
+
return process.versions.node;
|
|
42
|
+
},
|
|
43
|
+
"Install Node >=22.13.",
|
|
44
|
+
);
|
|
45
|
+
check(
|
|
46
|
+
"git",
|
|
47
|
+
() => {
|
|
48
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
49
|
+
cwd: root,
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
timeout: 5000,
|
|
52
|
+
maxBuffer: 65536,
|
|
53
|
+
});
|
|
54
|
+
if (result.status !== 0) throw new Error("git-repository-unavailable");
|
|
55
|
+
return result.stdout.trim();
|
|
56
|
+
},
|
|
57
|
+
"Install Git and initialize the consumer repository.",
|
|
58
|
+
);
|
|
59
|
+
check(
|
|
60
|
+
"openspec",
|
|
61
|
+
() => {
|
|
62
|
+
resolveOpenSpecInvocation(root);
|
|
63
|
+
return "resolved (protocol not executed)";
|
|
64
|
+
},
|
|
65
|
+
"Install @fission-ai/openspec@1.5.0, or configure ABEL_OPENSPEC_PACKAGE_ROOT / ABEL_OPENSPEC_NODE before launching the host.",
|
|
66
|
+
);
|
|
67
|
+
check(
|
|
68
|
+
"execution",
|
|
69
|
+
() => {
|
|
70
|
+
const profile = executionProfile();
|
|
71
|
+
if (process.platform !== "linux")
|
|
72
|
+
throw new Error("linux-pid-isolation-required");
|
|
73
|
+
|
|
74
|
+
const result = spawnSync(
|
|
75
|
+
profile.bwrapPath,
|
|
76
|
+
[
|
|
77
|
+
"--ro-bind",
|
|
78
|
+
"/",
|
|
79
|
+
"/",
|
|
80
|
+
profile.mode === "local-trusted" ? "--unshare-pid" : "--unshare-all",
|
|
81
|
+
"--",
|
|
82
|
+
"/bin/true",
|
|
83
|
+
],
|
|
84
|
+
{ timeout: 5000, encoding: "utf8", maxBuffer: 65536 },
|
|
85
|
+
);
|
|
86
|
+
if (result.status !== 0) throw new Error("bubblewrap-probe-failed");
|
|
87
|
+
return `${profile.mode}: Bubblewrap PID namespace probe passed`;
|
|
88
|
+
},
|
|
89
|
+
"On Linux install/enable Bubblewrap; set ABEL_BWRAP_PATH for a custom install. Trusted Linux projects may explicitly set ABEL_EXECUTION_MODE=local-trusted; Windows should use WSL.",
|
|
90
|
+
);
|
|
91
|
+
check(
|
|
92
|
+
"request-budgets",
|
|
93
|
+
() => JSON.stringify(requestBounds()),
|
|
94
|
+
"Set ABEL_FIRST_PROGRESS_MS and ABEL_STREAM_IDLE_MS to positive milliseconds <=1200000.",
|
|
95
|
+
);
|
|
96
|
+
check(
|
|
97
|
+
"state-root",
|
|
98
|
+
() =>
|
|
99
|
+
resolveStateRoot({
|
|
100
|
+
consumerRoot: root,
|
|
101
|
+
xdgStateHome: process.env.XDG_STATE_HOME,
|
|
102
|
+
}).rootDir,
|
|
103
|
+
"Use an absolute repository-external XDG_STATE_HOME.",
|
|
104
|
+
);
|
|
105
|
+
check(
|
|
106
|
+
"workspace-dependencies",
|
|
107
|
+
() => {
|
|
108
|
+
const dependencies = path.join(root, "node_modules");
|
|
109
|
+
if (!lstatSync(dependencies, { throwIfNoEntry: false }))
|
|
110
|
+
return "no node_modules; prepare dependencies before verification";
|
|
111
|
+
const names = readdirSync(dependencies).flatMap((name) =>
|
|
112
|
+
name.startsWith("@") &&
|
|
113
|
+
lstatSync(path.join(dependencies, name)).isDirectory()
|
|
114
|
+
? readdirSync(path.join(dependencies, name)).map(
|
|
115
|
+
(child) => `${name}/${child}`,
|
|
116
|
+
)
|
|
117
|
+
: [name],
|
|
118
|
+
);
|
|
119
|
+
if (names.length > 20_000) throw new Error("dependency-view-too-large");
|
|
120
|
+
let links = 0;
|
|
121
|
+
for (const name of names) {
|
|
122
|
+
const file = path.join(dependencies, name);
|
|
123
|
+
if (!lstatSync(file).isSymbolicLink()) continue;
|
|
124
|
+
const resolved = realpathSync(file);
|
|
125
|
+
const relative = path.relative(root, resolved);
|
|
126
|
+
if (
|
|
127
|
+
relative === ".." ||
|
|
128
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
129
|
+
path.isAbsolute(relative)
|
|
130
|
+
)
|
|
131
|
+
throw new Error("dependency-link-outside-consumer");
|
|
132
|
+
if (!resolved.startsWith(`${dependencies}${path.sep}`)) links++;
|
|
133
|
+
}
|
|
134
|
+
return `${links} repository workspace link(s); their targets must be included in the candidate baseline`;
|
|
135
|
+
},
|
|
136
|
+
"Repair dangling/external workspace links and ensure local package sources are tracked or admitted in the candidate.",
|
|
137
|
+
);
|
|
138
|
+
const manifest = path.join(root, "package.json");
|
|
139
|
+
if (lstatSync(manifest, { throwIfNoEntry: false })?.isFile()) {
|
|
140
|
+
check(
|
|
141
|
+
"manifest",
|
|
142
|
+
() => {
|
|
143
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
144
|
+
if (!parsed || typeof parsed !== "object")
|
|
145
|
+
throw new Error("manifest-invalid");
|
|
146
|
+
return (
|
|
147
|
+
Object.keys(parsed.scripts ?? {}).join(", ") || "no package scripts"
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
"Repair package.json.",
|
|
151
|
+
);
|
|
152
|
+
const parsed = (() => {
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(readFileSync(manifest, "utf8"));
|
|
155
|
+
} catch {
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
})();
|
|
159
|
+
for (const script of ["test", "check", "build"].filter(
|
|
160
|
+
(name) => typeof parsed.scripts?.[name] === "string",
|
|
161
|
+
)) {
|
|
162
|
+
check(
|
|
163
|
+
`script:${script}`,
|
|
164
|
+
() => {
|
|
165
|
+
const manager =
|
|
166
|
+
["bun", "pnpm", "yarn", "npm"].find((name) =>
|
|
167
|
+
lstatSync(
|
|
168
|
+
path.join(
|
|
169
|
+
root,
|
|
170
|
+
(
|
|
171
|
+
{
|
|
172
|
+
bun: "bun.lock",
|
|
173
|
+
pnpm: "pnpm-lock.yaml",
|
|
174
|
+
yarn: "yarn.lock",
|
|
175
|
+
npm: "package-lock.json",
|
|
176
|
+
} as Record<string, string>
|
|
177
|
+
)[name],
|
|
178
|
+
),
|
|
179
|
+
{ throwIfNoEntry: false },
|
|
180
|
+
),
|
|
181
|
+
) ?? "npm";
|
|
182
|
+
const result = validateVerificationAdapterCapability(root, {
|
|
183
|
+
kind: "package-script",
|
|
184
|
+
id: `doctor-${script}`,
|
|
185
|
+
packageManager: manager as "bun" | "pnpm" | "yarn" | "npm",
|
|
186
|
+
script,
|
|
187
|
+
command: parsed.scripts[script],
|
|
188
|
+
args: [],
|
|
189
|
+
classification: "expected-green",
|
|
190
|
+
});
|
|
191
|
+
if (!result.ok) throw new Error(result.diagnostic.code);
|
|
192
|
+
return "statically admitted; tests not executed";
|
|
193
|
+
},
|
|
194
|
+
"Check the runner, installed dependencies and execution configuration; this probe does not execute your tests.",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { root, ok: checks.every((entry) => entry.ok), checks };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Bounded metadata traversal, no symlink following and no automatic deletion. */
|
|
202
|
+
export function storageUsage(root: string, maximum = 100_000) {
|
|
203
|
+
let entries = 0;
|
|
204
|
+
let bytes = 0;
|
|
205
|
+
let truncated = false;
|
|
206
|
+
const visit = (file: string) => {
|
|
207
|
+
if (entries >= maximum) {
|
|
208
|
+
truncated = true;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
212
|
+
if (!stat) return;
|
|
213
|
+
entries++;
|
|
214
|
+
if (stat.isFile()) bytes += stat.size;
|
|
215
|
+
else if (stat.isDirectory())
|
|
216
|
+
for (const name of readdirSync(file)) {
|
|
217
|
+
visit(path.join(file, name));
|
|
218
|
+
if (truncated) break;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
visit(root);
|
|
222
|
+
return { entries, bytes, truncated };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function inspectRuns(root: string) {
|
|
226
|
+
const state = resolveStateRoot({
|
|
227
|
+
consumerRoot: root,
|
|
228
|
+
xdgStateHome: process.env.XDG_STATE_HOME,
|
|
229
|
+
});
|
|
230
|
+
const usage = storageUsage(state.rootDir);
|
|
231
|
+
if (!lstatSync(state.databasePath, { throwIfNoEntry: false }))
|
|
232
|
+
return { stateRoot: state.rootDir, usage, runs: [], truncated: false };
|
|
233
|
+
const db = new DatabaseSync(state.databasePath, { readOnly: true });
|
|
234
|
+
try {
|
|
235
|
+
const rows = db
|
|
236
|
+
.prepare(
|
|
237
|
+
"SELECT run_id, stage, change_name, state, sequence FROM runs ORDER BY run_id LIMIT 1001",
|
|
238
|
+
)
|
|
239
|
+
.all();
|
|
240
|
+
return {
|
|
241
|
+
stateRoot: state.rootDir,
|
|
242
|
+
usage,
|
|
243
|
+
runs: rows.slice(0, 1000),
|
|
244
|
+
truncated: rows.length > 1000,
|
|
245
|
+
};
|
|
246
|
+
} finally {
|
|
247
|
+
db.close();
|
|
248
|
+
}
|
|
249
|
+
}
|
package/src/package-candidate.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { runChildSession } from "./child-session.ts";
|
|
4
|
+
import type { VerificationFailureSummary } from "./contracts.ts";
|
|
4
5
|
import { LIMITS } from "./contracts.ts";
|
|
5
6
|
import type { DurableWorkflowEngineOptions } from "./durable-contracts.ts";
|
|
6
7
|
import type { ParentModelSource } from "./model-source.ts";
|
|
@@ -25,6 +26,7 @@ export async function proposePackageCandidate(
|
|
|
25
26
|
input: Parameters<DurableWorkflowEngineOptions["proposeCandidate"]>[0],
|
|
26
27
|
context: ParentModelSource | undefined,
|
|
27
28
|
implementationAgent: { content: string },
|
|
29
|
+
verificationDiagnostics: VerificationFailureSummary[] = [],
|
|
28
30
|
): Promise<DurableCandidateProposal> {
|
|
29
31
|
if (!context) {
|
|
30
32
|
return { kind: "paused", code: "parent-context-unavailable" };
|
|
@@ -88,6 +90,7 @@ export async function proposePackageCandidate(
|
|
|
88
90
|
]),
|
|
89
91
|
].sort();
|
|
90
92
|
const phaseContract = {
|
|
93
|
+
verificationDiagnostics,
|
|
91
94
|
candidateId: input.candidateArtifact.identity.candidateId,
|
|
92
95
|
taskId: input.taskId,
|
|
93
96
|
phase: input.phase,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type VerificationObservation,
|
|
8
8
|
verificationSteps,
|
|
9
9
|
} from "./contracts.ts";
|
|
10
|
+
import { executionProfile } from "./execution-profile.ts";
|
|
10
11
|
import { canonicalJson } from "./implement-graph.ts";
|
|
11
12
|
import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
|
|
12
13
|
import { observeSafePath } from "./safe-path.ts";
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
isVerificationCapabilityCurrent,
|
|
16
17
|
type VerificationRunnerBinding,
|
|
17
18
|
} from "./verification-capability.ts";
|
|
19
|
+
import { diagnosticText } from "./verification-diagnostics.ts";
|
|
18
20
|
import { prepareVerificationEnvironment } from "./verification-environment.ts";
|
|
19
21
|
|
|
20
22
|
function sha256(value: Uint8Array | string): string {
|
|
@@ -247,7 +249,7 @@ function normalizedFailureIdentities(input: {
|
|
|
247
249
|
.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu"), "")
|
|
248
250
|
.replace(/\r\n?/gu, "\n")
|
|
249
251
|
.replace(/\b\d+(?:\.\d+)?(?:ms|s)\b/gu, "<duration>")
|
|
250
|
-
.
|
|
252
|
+
.replace(/\b\d{4}-\d{2}-\d{2}T[\d:.]+Z\b/gu, "<timestamp>");
|
|
251
253
|
return [
|
|
252
254
|
sha256(
|
|
253
255
|
canonicalJson({
|
|
@@ -313,6 +315,9 @@ export async function executePackageVerification(input: {
|
|
|
313
315
|
maxReportBytes?: number;
|
|
314
316
|
executionWritePaths?: readonly string[];
|
|
315
317
|
}): Promise<VerificationObservation> {
|
|
318
|
+
let executionDiagnostic:
|
|
319
|
+
| import("./contracts.ts").VerificationFailureSummary
|
|
320
|
+
| undefined;
|
|
316
321
|
const unavailable = (
|
|
317
322
|
code: string,
|
|
318
323
|
category: "environment" | "adapter" | "resource" = "adapter",
|
|
@@ -321,6 +326,16 @@ export async function executePackageVerification(input: {
|
|
|
321
326
|
category,
|
|
322
327
|
code,
|
|
323
328
|
verificationId: input.verification.id,
|
|
329
|
+
...(executionDiagnostic
|
|
330
|
+
? {
|
|
331
|
+
diagnostic: {
|
|
332
|
+
...executionDiagnostic,
|
|
333
|
+
code,
|
|
334
|
+
nextStep:
|
|
335
|
+
"Repair the runner, environment or report adapter; do not treat missing evidence as a product failure.",
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
: {}),
|
|
324
339
|
});
|
|
325
340
|
if (input.signal.aborted) return { kind: "cancelled" };
|
|
326
341
|
const maxReportBytes = input.maxReportBytes ?? VERIFICATION_REPORT_LIMIT;
|
|
@@ -335,18 +350,37 @@ export async function executePackageVerification(input: {
|
|
|
335
350
|
},
|
|
336
351
|
);
|
|
337
352
|
if (!capability.ok) return unavailable(capability.diagnostic.code);
|
|
338
|
-
let environment: ReturnType<typeof prepareVerificationEnvironment
|
|
353
|
+
let environment: Awaited<ReturnType<typeof prepareVerificationEnvironment>>;
|
|
339
354
|
try {
|
|
340
|
-
environment = prepareVerificationEnvironment(
|
|
355
|
+
environment = await prepareVerificationEnvironment(
|
|
341
356
|
input.root,
|
|
342
357
|
input.dependencyOwner,
|
|
343
358
|
capability.value.runnerBindings,
|
|
359
|
+
input.signal,
|
|
344
360
|
);
|
|
345
|
-
} catch {
|
|
346
|
-
return
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (input.signal.aborted) return { kind: "cancelled" };
|
|
363
|
+
const code =
|
|
364
|
+
error instanceof Error &&
|
|
365
|
+
[
|
|
366
|
+
"workspace-dependency-missing",
|
|
367
|
+
"dependency-path-unsafe",
|
|
368
|
+
"execution-mode-invalid",
|
|
369
|
+
"verification-timeout-invalid",
|
|
370
|
+
"verification-environment-names-invalid",
|
|
371
|
+
"verification-environment-requires-trusted-mode",
|
|
372
|
+
].includes(error.message)
|
|
373
|
+
? error.message
|
|
374
|
+
: "verification-environment-unavailable";
|
|
375
|
+
return unavailable(code, "environment");
|
|
347
376
|
}
|
|
348
377
|
try {
|
|
349
|
-
const
|
|
378
|
+
const profile = executionProfile();
|
|
379
|
+
const isolation = new BubblewrapIsolationBackend({
|
|
380
|
+
bwrapPath: profile.bwrapPath,
|
|
381
|
+
timeoutMs: profile.timeoutMs,
|
|
382
|
+
localTrusted: profile.mode === "local-trusted",
|
|
383
|
+
});
|
|
350
384
|
let final: VerificationObservation | undefined;
|
|
351
385
|
let sequence = 0;
|
|
352
386
|
for (const step of verificationSteps(input.verification)) {
|
|
@@ -354,9 +388,17 @@ export async function executePackageVerification(input: {
|
|
|
354
388
|
const invocation = verificationInvocation(
|
|
355
389
|
step,
|
|
356
390
|
environment.bindings,
|
|
357
|
-
|
|
391
|
+
`${environment.reportRoot}/${relative}`,
|
|
358
392
|
);
|
|
359
393
|
if (!invocation) return unavailable("runner-missing");
|
|
394
|
+
if (profile.mode === "local-trusted") {
|
|
395
|
+
const translate = (value: string) =>
|
|
396
|
+
value.startsWith("/workspace/")
|
|
397
|
+
? path.join(input.root, value.slice(11))
|
|
398
|
+
: value;
|
|
399
|
+
invocation.executable = translate(invocation.executable);
|
|
400
|
+
invocation.args = invocation.args.map(translate);
|
|
401
|
+
}
|
|
360
402
|
const executed = await isolation.run({
|
|
361
403
|
root: input.root,
|
|
362
404
|
...invocation,
|
|
@@ -376,6 +418,20 @@ export async function executePackageVerification(input: {
|
|
|
376
418
|
: "environment",
|
|
377
419
|
);
|
|
378
420
|
}
|
|
421
|
+
executionDiagnostic = {
|
|
422
|
+
verificationId: step.id,
|
|
423
|
+
code: "verification-rejected",
|
|
424
|
+
exitCode: executed.exitCode,
|
|
425
|
+
failures: [],
|
|
426
|
+
stdout: diagnosticText(executed.stdout),
|
|
427
|
+
stderr: diagnosticText(executed.stderr),
|
|
428
|
+
truncated:
|
|
429
|
+
executed.logs.stdout.truncated ||
|
|
430
|
+
executed.logs.stderr.truncated ||
|
|
431
|
+
executed.stdout.length > 4096 ||
|
|
432
|
+
executed.stderr.length > 4096,
|
|
433
|
+
nextStep: "Inspect the reported verification failure.",
|
|
434
|
+
};
|
|
379
435
|
let report: ReturnType<typeof vitestReport>;
|
|
380
436
|
if (step.kind === "vitest") {
|
|
381
437
|
try {
|
|
@@ -426,6 +482,7 @@ export async function executePackageVerification(input: {
|
|
|
426
482
|
classification: step.classification,
|
|
427
483
|
policy: "report-file-v3" as const,
|
|
428
484
|
...(report ? { tests: report.total } : {}),
|
|
485
|
+
attributionReliable: step.kind === "vitest",
|
|
429
486
|
failureIdentities: accepted
|
|
430
487
|
? []
|
|
431
488
|
: normalizedFailureIdentities({
|
|
@@ -443,12 +500,31 @@ export async function executePackageVerification(input: {
|
|
|
443
500
|
? "red-not-witnessed"
|
|
444
501
|
: "verification-rejected",
|
|
445
502
|
evidence,
|
|
503
|
+
diagnostic: {
|
|
504
|
+
verificationId: step.id,
|
|
505
|
+
code: "verification-rejected",
|
|
506
|
+
exitCode: executed.exitCode,
|
|
507
|
+
failures: (report?.failedText ?? [])
|
|
508
|
+
.slice(0, 16)
|
|
509
|
+
.map((text) => diagnosticText(text, 1024)),
|
|
510
|
+
stdout: diagnosticText(executed.stdout),
|
|
511
|
+
stderr: diagnosticText(executed.stderr),
|
|
512
|
+
truncated:
|
|
513
|
+
executed.logs.stdout.truncated ||
|
|
514
|
+
executed.logs.stderr.truncated ||
|
|
515
|
+
executed.stdout.length > 4096 ||
|
|
516
|
+
executed.stderr.length > 4096,
|
|
517
|
+
nextStep:
|
|
518
|
+
step.classification === "expected-red"
|
|
519
|
+
? "Make the approved regression fail for the expected assertion, not an environment error."
|
|
520
|
+
: "Fix the reported failure in the candidate; preserve the accepted verification contract.",
|
|
521
|
+
},
|
|
446
522
|
};
|
|
447
523
|
final = { kind: "accepted", evidence };
|
|
448
524
|
}
|
|
449
525
|
return final ?? unavailable("verification-contract-unsupported");
|
|
450
526
|
} finally {
|
|
451
|
-
environment.cleanup();
|
|
527
|
+
await environment.cleanup();
|
|
452
528
|
}
|
|
453
529
|
}
|
|
454
530
|
|
|
@@ -469,6 +545,7 @@ export function phaseVerificationResult(result: VerificationObservation) {
|
|
|
469
545
|
kind: "retryable" as const,
|
|
470
546
|
code: result.code,
|
|
471
547
|
failureIdentities: result.evidence.failureIdentities,
|
|
548
|
+
attributionReliable: result.evidence.attributionReliable,
|
|
472
549
|
};
|
|
473
550
|
return {
|
|
474
551
|
ok: false as const,
|
|
@@ -497,6 +574,7 @@ export function changeVerificationResult(result: VerificationObservation) {
|
|
|
497
574
|
kind: "verification" as const,
|
|
498
575
|
code: result.code,
|
|
499
576
|
failureIdentities: result.evidence.failureIdentities,
|
|
577
|
+
attributionReliable: result.evidence.attributionReliable,
|
|
500
578
|
};
|
|
501
579
|
return {
|
|
502
580
|
ok: false as const,
|
package/src/package-workflow.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from "./contracts.ts";
|
|
10
10
|
import { validateControlCommand } from "./control-contracts.ts";
|
|
11
11
|
import { DesignController } from "./design-control.ts";
|
|
12
|
+
import { executionProfile } from "./execution-profile.ts";
|
|
12
13
|
import type { PackageContext } from "./model-source.ts";
|
|
13
14
|
import { inspectOpenSpecDelivery } from "./openspec-cli.ts";
|
|
14
15
|
import { proposePackageCandidate } from "./package-candidate.ts";
|
|
@@ -25,7 +26,9 @@ import {
|
|
|
25
26
|
unavailableRoutePolicy,
|
|
26
27
|
} from "./route-policy.ts";
|
|
27
28
|
import { resolveStateRoot } from "./state-root.ts";
|
|
29
|
+
import { VerificationFeedback } from "./verification-diagnostics.ts";
|
|
28
30
|
import { captureVerificationEnvironmentIdentity } from "./verification-environment.ts";
|
|
31
|
+
import { coalesceVerificationScans } from "./verification-scan.ts";
|
|
29
32
|
import { openDurableWorkflowEngine } from "./workflow-engine.ts";
|
|
30
33
|
export interface PackageWorkflowService {
|
|
31
34
|
execute(
|
|
@@ -88,6 +91,12 @@ export function openPackageWorkflowService(
|
|
|
88
91
|
xdgStateHome: process.env.XDG_STATE_HOME,
|
|
89
92
|
});
|
|
90
93
|
const contexts = new AsyncLocalStorage<PackageContext>();
|
|
94
|
+
const feedback = new AsyncLocalStorage<VerificationFeedback>();
|
|
95
|
+
const verify: typeof executePackageVerification = async (input) => {
|
|
96
|
+
const result = await executePackageVerification(input);
|
|
97
|
+
feedback.getStore()?.observe(result);
|
|
98
|
+
return result;
|
|
99
|
+
};
|
|
91
100
|
const design = DesignController.open({
|
|
92
101
|
consumerRoot,
|
|
93
102
|
stateRoot,
|
|
@@ -113,25 +122,27 @@ export function openPackageWorkflowService(
|
|
|
113
122
|
const workHardLimit = hostLimit("ABEL_WORK_MAX_UNITS", 512);
|
|
114
123
|
const engine = openDurableWorkflowEngine({
|
|
115
124
|
workHardLimit,
|
|
116
|
-
verificationPolicy:
|
|
117
|
-
verificationEnvironment: (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
...
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
verificationPolicy: `report-file-v5:${JSON.stringify(executionProfile())}`,
|
|
126
|
+
verificationEnvironment: coalesceVerificationScans(
|
|
127
|
+
(plan: import("./implement-plan.ts").ImplementPlan, signal) =>
|
|
128
|
+
captureVerificationEnvironmentIdentity(
|
|
129
|
+
consumerRoot,
|
|
130
|
+
[
|
|
131
|
+
...plan.tasks.flatMap((task) => [
|
|
132
|
+
...Object.values(task.phases).map((phase) => phase.verification),
|
|
133
|
+
task.affectedVerification,
|
|
134
|
+
task.repairVerification,
|
|
135
|
+
]),
|
|
136
|
+
plan.verification.baseline.fullSuite,
|
|
137
|
+
plan.verification.change.fullSuite,
|
|
138
|
+
plan.verification.change.postApply,
|
|
139
|
+
...(plan.verification.agentsCheckpoint.verification
|
|
140
|
+
? [plan.verification.agentsCheckpoint.verification]
|
|
141
|
+
: []),
|
|
142
|
+
],
|
|
143
|
+
signal,
|
|
144
|
+
),
|
|
145
|
+
),
|
|
135
146
|
consumerRoot,
|
|
136
147
|
stateRoot,
|
|
137
148
|
deliverySource: packageDeliverySource(consumerRoot, {
|
|
@@ -142,10 +153,15 @@ export function openPackageWorkflowService(
|
|
|
142
153
|
? routeResolution.policy
|
|
143
154
|
: unavailableRoutePolicy(),
|
|
144
155
|
proposeCandidate: (input) =>
|
|
145
|
-
proposePackageCandidate(
|
|
156
|
+
proposePackageCandidate(
|
|
157
|
+
input,
|
|
158
|
+
contexts.getStore(),
|
|
159
|
+
implementationAgent,
|
|
160
|
+
feedback.getStore()?.current(),
|
|
161
|
+
),
|
|
146
162
|
verifyPhase: async (input) =>
|
|
147
163
|
phaseVerificationResult(
|
|
148
|
-
await
|
|
164
|
+
await verify({
|
|
149
165
|
maxReportBytes,
|
|
150
166
|
executionWritePaths: input.executionWritePaths,
|
|
151
167
|
root: input.root,
|
|
@@ -158,8 +174,9 @@ export function openPackageWorkflowService(
|
|
|
158
174
|
const verifications = input.verification
|
|
159
175
|
? [input.verification]
|
|
160
176
|
: input.plan.tasks.map((task) => affectedVerification(task));
|
|
177
|
+
let failure: ReturnType<typeof changeVerificationResult> | undefined;
|
|
161
178
|
for (const verification of verifications) {
|
|
162
|
-
const result = await
|
|
179
|
+
const result = await verify({
|
|
163
180
|
maxReportBytes,
|
|
164
181
|
executionWritePaths: input.plan.tasks.flatMap((task) =>
|
|
165
182
|
Object.values(task.phases).flatMap((phase) => [
|
|
@@ -173,8 +190,22 @@ export function openPackageWorkflowService(
|
|
|
173
190
|
signal: input.signal,
|
|
174
191
|
});
|
|
175
192
|
const observed = changeVerificationResult(result);
|
|
176
|
-
if (!observed.ok)
|
|
193
|
+
if (!observed.ok) {
|
|
194
|
+
if (observed.kind !== "verification") return observed;
|
|
195
|
+
if (failure && !failure.ok && failure.kind === "verification") {
|
|
196
|
+
failure.failureIdentities = [
|
|
197
|
+
...new Set([
|
|
198
|
+
...(failure.failureIdentities ?? []),
|
|
199
|
+
...(observed.failureIdentities ?? []),
|
|
200
|
+
]),
|
|
201
|
+
];
|
|
202
|
+
failure.attributionReliable =
|
|
203
|
+
failure.attributionReliable !== false &&
|
|
204
|
+
observed.attributionReliable !== false;
|
|
205
|
+
} else failure = observed;
|
|
206
|
+
}
|
|
177
207
|
}
|
|
208
|
+
if (failure) return failure;
|
|
178
209
|
return {
|
|
179
210
|
ok: true,
|
|
180
211
|
exitCode: 0,
|
|
@@ -205,18 +236,24 @@ export function openPackageWorkflowService(
|
|
|
205
236
|
error.name = "ControlCommandError";
|
|
206
237
|
throw error;
|
|
207
238
|
}
|
|
208
|
-
return
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
239
|
+
return feedback.run(new VerificationFeedback(), () =>
|
|
240
|
+
contexts.run(context, async () => {
|
|
241
|
+
const outcome = await engine.execute(
|
|
242
|
+
validation.value,
|
|
243
|
+
signal,
|
|
244
|
+
onActivity,
|
|
245
|
+
);
|
|
246
|
+
const routePolicy = visibleRoutePolicyStatus(
|
|
247
|
+
operationRouteResolution,
|
|
248
|
+
engine.routePolicyStatus(),
|
|
249
|
+
);
|
|
250
|
+
return {
|
|
251
|
+
...outcome,
|
|
252
|
+
routePolicy,
|
|
253
|
+
verificationDiagnostics: feedback.getStore()?.current() ?? [],
|
|
254
|
+
};
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
220
257
|
},
|
|
221
258
|
executeDesign(request: unknown) {
|
|
222
259
|
return design.execute(request);
|
|
@@ -235,6 +272,7 @@ export function openPackageWorkflowService(
|
|
|
235
272
|
async close() {
|
|
236
273
|
contexts.disable();
|
|
237
274
|
await engine.close();
|
|
275
|
+
feedback.disable();
|
|
238
276
|
design.close();
|
|
239
277
|
},
|
|
240
278
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isValidRelativePath } from "./contracts.ts";
|
|
2
2
|
import type { ImplementPlan } from "./implement-plan.ts";
|
|
3
|
+
import { expandSingleTaskDraft } from "./single-task-draft.ts";
|
|
3
4
|
|
|
4
5
|
/** Review data only: no executable callbacks, evidence cache or approval authority. */
|
|
5
6
|
function record(value: unknown): Record<string, unknown> {
|
|
@@ -11,7 +12,8 @@ export function planAuthoringSummary(
|
|
|
11
12
|
plan: ImplementPlan,
|
|
12
13
|
authoredDraft?: unknown,
|
|
13
14
|
) {
|
|
14
|
-
const
|
|
15
|
+
const quick = record(authoredDraft).singleTask !== undefined;
|
|
16
|
+
const authored = record(expandSingleTaskDraft(authoredDraft));
|
|
15
17
|
const derivations: Array<{ field: string; source: string; taskId?: string }> =
|
|
16
18
|
[];
|
|
17
19
|
let derivationsTruncated = false;
|
|
@@ -22,6 +24,7 @@ export function planAuthoringSummary(
|
|
|
22
24
|
}
|
|
23
25
|
derivations.push({ field, source, ...(taskId ? { taskId } : {}) });
|
|
24
26
|
};
|
|
27
|
+
if (quick) derived("singleTask", "single-task-author-input");
|
|
25
28
|
if (authoredDraft !== undefined) {
|
|
26
29
|
if (!Object.hasOwn(authored, "tracking")) derived("tracking", "task-ids");
|
|
27
30
|
if (plan.changeContract && !Object.hasOwn(authored, "changeContract"))
|