@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.
@@ -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
+ }
@@ -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
- .slice(0, 64 * 1024);
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 unavailable("verification-environment-unavailable", "environment");
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 isolation = new BubblewrapIsolationBackend();
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
- `/cadence/${relative}`,
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,
@@ -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: "report-file-v4",
117
- verificationEnvironment: (plan, signal) =>
118
- captureVerificationEnvironmentIdentity(
119
- consumerRoot,
120
- [
121
- ...plan.tasks.flatMap((task) => [
122
- ...Object.values(task.phases).map((phase) => phase.verification),
123
- task.affectedVerification,
124
- task.repairVerification,
125
- ]),
126
- plan.verification.baseline.fullSuite,
127
- plan.verification.change.fullSuite,
128
- plan.verification.change.postApply,
129
- ...(plan.verification.agentsCheckpoint.verification
130
- ? [plan.verification.agentsCheckpoint.verification]
131
- : []),
132
- ],
133
- signal,
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(input, contexts.getStore(), implementationAgent),
156
+ proposePackageCandidate(
157
+ input,
158
+ contexts.getStore(),
159
+ implementationAgent,
160
+ feedback.getStore()?.current(),
161
+ ),
146
162
  verifyPhase: async (input) =>
147
163
  phaseVerificationResult(
148
- await executePackageVerification({
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 executePackageVerification({
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) return observed;
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 contexts.run(context, async () => {
209
- const outcome = await engine.execute(
210
- validation.value,
211
- signal,
212
- onActivity,
213
- );
214
- const routePolicy = visibleRoutePolicyStatus(
215
- operationRouteResolution,
216
- engine.routePolicyStatus(),
217
- );
218
- return { ...outcome, routePolicy };
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 authored = record(authoredDraft);
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"))