@abelxiaoxing/cadence 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +25 -4
  2. package/config/.env.example +6 -0
  3. package/config/plan-draft.example.json +3 -0
  4. package/config/plan-draft.multiple-tasks.example.json +30 -0
  5. package/package.json +1 -1
  6. package/prompts/abel-design.md +33 -3
  7. package/prompts/abel-implement.md +36 -4
  8. package/src/apply-transaction.ts +28 -8
  9. package/src/build-windows-job.mjs +95 -0
  10. package/src/candidate-workspace.ts +6 -0
  11. package/src/change-verification.ts +764 -67
  12. package/src/contracts.ts +8 -0
  13. package/src/delivery-compiler.ts +452 -9
  14. package/src/design-control.ts +2 -2
  15. package/src/design-diagnostics.ts +6 -4
  16. package/src/durable-contracts.ts +21 -3
  17. package/src/durable-workflow.ts +55 -40
  18. package/src/execution-profile.ts +28 -2
  19. package/src/execution-retention.ts +77 -0
  20. package/src/implement-continuation.ts +559 -0
  21. package/src/implement-graph.ts +114 -0
  22. package/src/implement-plan.ts +2 -0
  23. package/src/index.ts +151 -1
  24. package/src/isolation-backend.ts +2 -1
  25. package/src/operator-cli.mjs +664 -104
  26. package/src/operator-tools.ts +7 -1
  27. package/src/package-state.ts +11 -0
  28. package/src/package-verification.ts +66 -18
  29. package/src/package-workflow.ts +6 -0
  30. package/src/phase-execution.ts +18 -7
  31. package/src/plan-draft-summary.ts +385 -7
  32. package/src/plan-draft.ts +9 -0
  33. package/src/remove-path.ts +32 -0
  34. package/src/subagent-activity.ts +3 -1
  35. package/src/verification-capability.ts +18 -6
  36. package/src/verification-environment-io.ts +123 -14
  37. package/src/verification-environment.ts +50 -3
  38. package/src/windows-job-backend.ts +419 -0
  39. package/src/windows-job.c +149 -0
  40. package/src/workflow-policy.ts +296 -5
  41. package/src/workflow-recovery-policy.ts +2 -0
  42. package/src/workflow-state-machine.ts +314 -80
  43. package/src/workflow-status.ts +251 -59
  44. package/src/workspace-io-entry.mjs +2 -2
  45. package/src/workspace-io-worker.mjs +207 -110
@@ -7,6 +7,7 @@ import { resolveOpenSpecInvocation } from "./openspec-cli.ts";
7
7
  import { resolveStateRoot } from "./state-root.ts";
8
8
  import { requestBounds } from "./transport-budget.ts";
9
9
  import { validateVerificationAdapterCapability } from "./verification-capability.ts";
10
+ import { probeWindowsJob } from "./windows-job-backend.ts";
10
11
 
11
12
  export interface DoctorCheck {
12
13
  name: string;
@@ -121,6 +122,11 @@ export function inspectConsumer(root: string) {
121
122
  "execution",
122
123
  () => {
123
124
  const profile = executionProfile();
125
+ if (profile.mode === "host-trusted") {
126
+ if (!probeWindowsJob(profile.windowsHelperPath ?? ""))
127
+ throw new Error("windows-job-backend-unavailable");
128
+ return "host-trusted: Windows x64 Job probe passed; host files/network accessible";
129
+ }
124
130
  if (process.platform !== "linux")
125
131
  throw new Error("linux-pid-isolation-required");
126
132
 
@@ -139,7 +145,7 @@ export function inspectConsumer(root: string) {
139
145
  if (result.status !== 0) throw new Error("bubblewrap-probe-failed");
140
146
  return `${profile.mode}: Bubblewrap PID namespace probe passed`;
141
147
  },
142
- "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.",
148
+ "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 x64 trusted projects may explicitly select ABEL_EXECUTION_MODE=host-trusted and install the matching native Job helper (ABEL_WINDOWS_JOB_HELPER).",
143
149
  );
144
150
  check(
145
151
  "request-budgets",
@@ -15,6 +15,7 @@ import {
15
15
  import path from "node:path";
16
16
  import { DatabaseSync } from "node:sqlite";
17
17
  import { syncDirectory } from "./directory-sync.ts";
18
+ import { assertExecutionsSettled } from "./execution-retention.ts";
18
19
  import { RunStore } from "./run-store.ts";
19
20
  import { prepareStateRoot, type ResolvedStateRoot } from "./state-root.ts";
20
21
 
@@ -160,6 +161,16 @@ export function acquirePackageState(
160
161
  ): { notice?: PackageStateNotice; close(): void } {
161
162
  versionParts(version);
162
163
  const state = prepareStateRoot(unresolved);
164
+ try {
165
+ assertExecutionsSettled(state.rootDir);
166
+ } catch (error) {
167
+ if (
168
+ error instanceof Error &&
169
+ error.message === "isolation-termination-unconfirmed"
170
+ )
171
+ fail(error.message);
172
+ throw error;
173
+ }
163
174
  for (const suffix of ["-wal", "-shm", "-journal"])
164
175
  regular(state.databasePath + suffix);
165
176
  const coordinator = path.join(
@@ -4,10 +4,12 @@ import path from "node:path";
4
4
  import {
5
5
  type AtomicVerificationContract,
6
6
  type StructuredVerificationContract,
7
+ type VerificationInputObservation,
7
8
  type VerificationObservation,
8
9
  verificationSteps,
9
10
  } from "./contracts.ts";
10
11
  import { executionProfile } from "./execution-profile.ts";
12
+ import { retainExecution } from "./execution-retention.ts";
11
13
  import { canonicalJson } from "./implement-graph.ts";
12
14
  import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
13
15
  import { observeSafePath } from "./safe-path.ts";
@@ -18,6 +20,7 @@ import {
18
20
  } from "./verification-capability.ts";
19
21
  import { diagnosticText } from "./verification-diagnostics.ts";
20
22
  import { prepareVerificationEnvironment } from "./verification-environment.ts";
23
+ import { WindowsJobBackend } from "./windows-job-backend.ts";
21
24
 
22
25
  function sha256(value: Uint8Array | string): string {
23
26
  return createHash("sha256").update(value).digest("hex");
@@ -310,6 +313,7 @@ function readReport(root: string, relative: string, maximum: number): string {
310
313
  export async function executePackageVerification(input: {
311
314
  root: string;
312
315
  dependencyOwner: string;
316
+ executionOwnerRoot?: string;
313
317
  verification: StructuredVerificationContract;
314
318
  signal: AbortSignal;
315
319
  maxReportBytes?: number;
@@ -321,11 +325,13 @@ export async function executePackageVerification(input: {
321
325
  const unavailable = (
322
326
  code: string,
323
327
  category: "environment" | "adapter" | "resource" = "adapter",
328
+ inputObservation?: VerificationInputObservation,
324
329
  ): VerificationObservation => ({
325
330
  kind: "unavailable",
326
331
  category,
327
332
  code,
328
333
  verificationId: input.verification.id,
334
+ ...(inputObservation ? { inputObservation } : {}),
329
335
  ...(executionDiagnostic
330
336
  ? {
331
337
  diagnostic: {
@@ -349,7 +355,14 @@ export async function executePackageVerification(input: {
349
355
  executionWritePaths: input.executionWritePaths,
350
356
  },
351
357
  );
352
- if (!capability.ok) return unavailable(capability.diagnostic.code);
358
+ if (!capability.ok)
359
+ return unavailable(
360
+ capability.diagnostic.code,
361
+ "adapter",
362
+ capability.diagnostic.kind === "verification-adapter"
363
+ ? capability.diagnostic.inputObservation
364
+ : undefined,
365
+ );
353
366
  let environment: Awaited<ReturnType<typeof prepareVerificationEnvironment>>;
354
367
  try {
355
368
  environment = await prepareVerificationEnvironment(
@@ -366,6 +379,7 @@ export async function executePackageVerification(input: {
366
379
  "workspace-dependency-missing",
367
380
  "dependency-path-unsafe",
368
381
  "execution-mode-invalid",
382
+ "windows-job-helper-path-invalid",
369
383
  "verification-timeout-invalid",
370
384
  "verification-environment-names-invalid",
371
385
  "verification-environment-requires-trusted-mode",
@@ -374,13 +388,20 @@ export async function executePackageVerification(input: {
374
388
  : "verification-environment-unavailable";
375
389
  return unavailable(code, "environment");
376
390
  }
391
+ let retained = false;
377
392
  try {
378
393
  const profile = executionProfile();
379
- const isolation = new BubblewrapIsolationBackend({
380
- bwrapPath: profile.bwrapPath,
381
- timeoutMs: profile.timeoutMs,
382
- localTrusted: profile.mode === "local-trusted",
383
- });
394
+ const isolation =
395
+ profile.mode === "host-trusted"
396
+ ? new WindowsJobBackend({
397
+ helperPath: profile.windowsHelperPath ?? "",
398
+ timeoutMs: profile.timeoutMs,
399
+ })
400
+ : new BubblewrapIsolationBackend({
401
+ bwrapPath: profile.bwrapPath,
402
+ timeoutMs: profile.timeoutMs,
403
+ localTrusted: profile.mode === "local-trusted",
404
+ });
384
405
  let final: VerificationObservation | undefined;
385
406
  let sequence = 0;
386
407
  for (const step of verificationSteps(input.verification)) {
@@ -391,7 +412,7 @@ export async function executePackageVerification(input: {
391
412
  `${environment.reportRoot}/${relative}`,
392
413
  );
393
414
  if (!invocation) return unavailable("runner-missing");
394
- if (profile.mode === "local-trusted") {
415
+ if (profile.mode !== "isolated") {
395
416
  const translate = (value: string) =>
396
417
  value.startsWith("/workspace/")
397
418
  ? path.join(input.root, value.slice(11))
@@ -399,16 +420,37 @@ export async function executePackageVerification(input: {
399
420
  invocation.executable = translate(invocation.executable);
400
421
  invocation.args = invocation.args.map(translate);
401
422
  }
402
- const executed = await isolation.run({
403
- root: input.root,
404
- ...invocation,
405
- mounts: environment.mounts,
406
- environment: environment.environment,
407
- ...(step.kind !== "vitest" && step.classification === "expected-red"
408
- ? { outputWitness: step.expectedFailure }
409
- : {}),
410
- signal: input.signal,
411
- });
423
+ const lease =
424
+ profile.mode === "host-trusted"
425
+ ? retainExecution(
426
+ input.executionOwnerRoot ?? path.dirname(input.root),
427
+ [input.root, environment.privateRoot],
428
+ )
429
+ : undefined;
430
+ let executed: Awaited<ReturnType<typeof isolation.run>>;
431
+ try {
432
+ executed = await isolation.run({
433
+ root: input.root,
434
+ ...invocation,
435
+ mounts: profile.mode === "host-trusted" ? [] : environment.mounts,
436
+ environment: environment.environment,
437
+ ...(step.kind !== "vitest" && step.classification === "expected-red"
438
+ ? { outputWitness: step.expectedFailure }
439
+ : {}),
440
+ signal: input.signal,
441
+ });
442
+ if (
443
+ !executed.ok &&
444
+ executed.code === "isolation-termination-unconfirmed"
445
+ ) {
446
+ retained = true;
447
+ lease?.uncertain();
448
+ } else lease?.settled();
449
+ } catch (error) {
450
+ retained = !!lease;
451
+ lease?.uncertain();
452
+ throw error;
453
+ }
412
454
  if (!executed.ok) {
413
455
  if (executed.state === "cancelled") return { kind: "cancelled" };
414
456
  return unavailable(
@@ -524,7 +566,7 @@ export async function executePackageVerification(input: {
524
566
  }
525
567
  return final ?? unavailable("verification-contract-unsupported");
526
568
  } finally {
527
- await environment.cleanup();
569
+ if (!retained) await environment.cleanup();
528
570
  }
529
571
  }
530
572
 
@@ -552,6 +594,9 @@ export function phaseVerificationResult(result: VerificationObservation) {
552
594
  kind: "paused" as const,
553
595
  code: result.code,
554
596
  category: result.category,
597
+ ...(result.inputObservation
598
+ ? { inputObservation: result.inputObservation }
599
+ : {}),
555
600
  };
556
601
  }
557
602
 
@@ -583,5 +628,8 @@ export function changeVerificationResult(result: VerificationObservation) {
583
628
  ? ("verification-adapter" as const)
584
629
  : ("environment" as const),
585
630
  code: result.code,
631
+ ...(result.inputObservation
632
+ ? { inputObservation: result.inputObservation }
633
+ : {}),
586
634
  };
587
635
  }
@@ -160,6 +160,7 @@ function openPreparedPackageWorkflowService(
160
160
  const workHardLimit = hostLimit("ABEL_WORK_MAX_UNITS", 512);
161
161
  const engine = openDurableWorkflowEngine({
162
162
  workHardLimit,
163
+ awaitPostApplySettlement: true,
163
164
  verificationPolicy: `report-file-v5:${JSON.stringify(executionProfile())}`,
164
165
  verificationEnvironment: coalesceVerificationScans(
165
166
  (plan: import("./implement-plan.ts").ImplementPlan, signal) =>
@@ -171,6 +172,9 @@ function openPreparedPackageWorkflowService(
171
172
  (phase) => phase.verification,
172
173
  ),
173
174
  task.affectedVerification,
175
+ ...(task.baselineVerification
176
+ ? [task.baselineVerification]
177
+ : []),
174
178
  task.repairVerification,
175
179
  ]),
176
180
  plan.verification.baseline.fullSuite,
@@ -207,6 +211,7 @@ function openPreparedPackageWorkflowService(
207
211
  executionWritePaths: input.executionWritePaths,
208
212
  root: input.root,
209
213
  dependencyOwner: consumerRoot,
214
+ executionOwnerRoot: stateRoot.rootDir,
210
215
  verification: input.verification,
211
216
  signal: input.signal,
212
217
  }),
@@ -227,6 +232,7 @@ function openPreparedPackageWorkflowService(
227
232
  ),
228
233
  root: input.root,
229
234
  dependencyOwner: consumerRoot,
235
+ executionOwnerRoot: stateRoot.rootDir,
230
236
  verification,
231
237
  signal: input.signal,
232
238
  });
@@ -1,12 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdtempSync, rmSync } from "node:fs";
2
+ import { mkdtempSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import {
5
5
  inspectCandidateArtifact,
6
6
  sealCandidateArtifact,
7
7
  } from "./candidate-artifact.ts";
8
8
  import { revisionChanges, runGitApply } from "./candidate-workspace.ts";
9
-
10
9
  import type { StructuredVerificationContract } from "./contracts.ts";
11
10
  import type { PlanTaskDraft } from "./delivery-compiler.ts";
12
11
  import type {
@@ -20,6 +19,8 @@ import type {
20
19
  DurableWorkflowEngineOptions,
21
20
  PhaseResources,
22
21
  } from "./durable-contracts.ts";
22
+ import { isExecutionRetained } from "./execution-retention.ts";
23
+ import { removePathSync } from "./remove-path.ts";
23
24
  import type {
24
25
  BeginCandidateInput,
25
26
  TaskLedger,
@@ -43,6 +44,7 @@ import {
43
44
  LOCKFILES,
44
45
  SHA256,
45
46
  semanticRouteFailure,
47
+ taskExecutionContract,
46
48
  type WorkflowAttemptOutcome,
47
49
  type WorkflowRouteFacts,
48
50
  type WorkflowWorker,
@@ -60,6 +62,7 @@ export interface PhaseExecutionServices {
60
62
  resources: PhaseResources;
61
63
  ledger: TaskLedger;
62
64
  signal: AbortSignal;
65
+ taskId?: string;
63
66
  }): Promise<DurableBaselineResult>;
64
67
  verifyTaskAffected(input: {
65
68
  resources: PhaseResources;
@@ -197,7 +200,10 @@ export class PhaseExecution {
197
200
  runId: input.runId,
198
201
  deliveryRevision: input.deliveryRevision,
199
202
  taskId: input.taskId,
200
- boundaryHash: hash("durable-task-boundary", JSON.stringify(input.task)),
203
+ boundaryHash: hash(
204
+ "durable-task-boundary",
205
+ JSON.stringify(taskExecutionContract(input.task)),
206
+ ),
201
207
  objective: input.task.objective,
202
208
  contextRefs,
203
209
  initialPhase:
@@ -303,7 +309,8 @@ export class PhaseExecution {
303
309
  policy: this.#options.verificationPolicy ?? "legacy",
304
310
  });
305
311
  } finally {
306
- rmSync(root, { recursive: true, force: true });
312
+ if (!isExecutionRetained(root))
313
+ removePathSync(root, { recursive: true, force: true });
307
314
  }
308
315
  }
309
316
  return undefined;
@@ -975,7 +982,8 @@ export class PhaseExecution {
975
982
  },
976
983
  };
977
984
  } finally {
978
- rmSync(proposalRoot, { recursive: true, force: true });
985
+ if (!isExecutionRetained(proposalRoot))
986
+ removePathSync(proposalRoot, { recursive: true, force: true });
979
987
  }
980
988
  }
981
989
 
@@ -1160,7 +1168,8 @@ export class PhaseExecution {
1160
1168
  signal: request.signal,
1161
1169
  });
1162
1170
  } finally {
1163
- rmSync(root, { recursive: true, force: true });
1171
+ if (!isExecutionRetained(root))
1172
+ removePathSync(root, { recursive: true, force: true });
1164
1173
  }
1165
1174
  };
1166
1175
  emitWorkflowActivity(request.onActivity, { state: "verifying" });
@@ -1562,6 +1571,7 @@ export class PhaseExecution {
1562
1571
  resources,
1563
1572
  ledger,
1564
1573
  signal: input.signal,
1574
+ taskId: input.taskId,
1565
1575
  });
1566
1576
  if (!captured.ok) {
1567
1577
  const outcome = captured.outcome;
@@ -2242,7 +2252,8 @@ export class PhaseExecution {
2242
2252
  baselineRevisionId: resources.baselineRevisionId,
2243
2253
  });
2244
2254
  } finally {
2245
- rmSync(proposalRoot, { recursive: true, force: true });
2255
+ if (!isExecutionRetained(proposalRoot))
2256
+ removePathSync(proposalRoot, { recursive: true, force: true });
2246
2257
  }
2247
2258
  }
2248
2259
  }