@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
@@ -1,5 +1,8 @@
1
1
  import type { ArtifactStore } from "./artifact-store.ts";
2
- import type { StructuredVerificationContract } from "./contracts.ts";
2
+ import type {
3
+ StructuredVerificationContract,
4
+ VerificationInputObservation,
5
+ } from "./contracts.ts";
3
6
  import type { ImplementPlan, PlanTaskDraft } from "./delivery-compiler.ts";
4
7
  import type { RoutePolicy, WorkerRoutePolicy } from "./route-policy.ts";
5
8
 
@@ -15,6 +18,7 @@ import type {
15
18
  WorkflowContextRequest,
16
19
  WorkflowDeliverySource,
17
20
  WorkflowRecoveryFeedback,
21
+ WorkflowVerificationPrerequisite,
18
22
  } from "./workflow-policy.ts";
19
23
 
20
24
  import type { WorkspaceStore } from "./workspace-store.ts";
@@ -38,6 +42,7 @@ export type DurablePhaseVerificationResult =
38
42
  ok: false;
39
43
  kind: "paused" | "retryable" | "approval-needed";
40
44
  code: string;
45
+ inputObservation?: VerificationInputObservation;
41
46
  };
42
47
 
43
48
  export type DurableChangeVerificationResult =
@@ -60,6 +65,7 @@ export type DurableChangeVerificationResult =
60
65
  code: string;
61
66
  failureIdentities?: string[];
62
67
  attributionReliable?: boolean;
68
+ inputObservation?: VerificationInputObservation;
63
69
  };
64
70
 
65
71
  export interface DurableVerificationObservation {
@@ -68,6 +74,8 @@ export interface DurableVerificationObservation {
68
74
  failureIdentities: string[];
69
75
  code?: string;
70
76
  attributionReliable?: boolean;
77
+ /** Code-owned comparable verification obligation, excluding display identity. */
78
+ contractIdentity?: string;
71
79
  }
72
80
 
73
81
  export interface DurableVerificationBaseline {
@@ -89,7 +97,11 @@ export type DurableObservedVerification =
89
97
  | {
90
98
  ok: false;
91
99
  outcome:
92
- | { kind: "paused"; code: string }
100
+ | {
101
+ kind: "paused";
102
+ code: string;
103
+ prerequisite?: WorkflowVerificationPrerequisite;
104
+ }
93
105
  | { kind: "approval-needed"; code: string }
94
106
  | { kind: "operation-cancelled"; code: "cancelled" };
95
107
  };
@@ -161,6 +173,8 @@ export interface DurableWorkflowEngineOptions {
161
173
  leaseTtlMs?: number;
162
174
  workHardLimit?: number;
163
175
  verificationPolicy?: string;
176
+ /** Real process executors must settle cancellation before post-apply cleanup. */
177
+ awaitPostApplySettlement?: boolean;
164
178
  verificationEnvironment?(
165
179
  plan: ImplementPlan,
166
180
  signal: AbortSignal,
@@ -175,11 +189,15 @@ export interface DurableExecutionResources {
175
189
  artifacts: ArtifactStore;
176
190
  workspaces: WorkspaceStore;
177
191
  ledgers: Map<number, TaskLedger>;
192
+ /** Run-shared original-baseline observations, independent of delivery revision. */
193
+ baselineLedger?: TaskLedger;
178
194
  baselineRevisionId: string;
195
+ /** Root capture before any authorized boundary expansion; immutable evidence source. */
196
+ originalBaselineRevisionId?: string;
179
197
  currentRevisionId: string;
180
198
  mergeTail: Promise<void>;
181
199
  verificationFact?: object;
182
- baselinePromises: Map<number, Promise<DurableBaselineResult>>;
200
+ baselinePromises: Map<string | number, Promise<DurableBaselineResult>>;
183
201
  environmentIdentity?: string;
184
202
  }
185
203
 
@@ -1,16 +1,13 @@
1
- import {
2
- ChangeVerification,
3
- parseVerificationBaseline,
4
- sha256Bytes,
5
- verificationBaselineFact,
6
- } from "./change-verification.ts";
1
+ import { ChangeVerification, sha256Bytes } from "./change-verification.ts";
7
2
  import type {
8
3
  DurableExecutionResources,
9
4
  DurablePhaseFact,
10
5
  DurableResourceInput,
11
6
  DurableWorkflowEngineOptions,
12
7
  } from "./durable-contracts.ts";
8
+ import { assertExecutionsSettled } from "./execution-retention.ts";
13
9
  import { PhaseExecution } from "./phase-execution.ts";
10
+ import { removePathSync } from "./remove-path.ts";
14
11
 
15
12
  export type {
16
13
  DurableChangeVerificationResult,
@@ -23,7 +20,7 @@ interface DurableRunResources extends DurableExecutionResources {
23
20
  closed: boolean;
24
21
  }
25
22
 
26
- import { lstatSync, mkdirSync, readFileSync, rmSync } from "node:fs";
23
+ import { lstatSync, mkdirSync, readFileSync } from "node:fs";
27
24
  import path from "node:path";
28
25
  import { DatabaseSync } from "node:sqlite";
29
26
  import { ApplyTransaction } from "./apply-transaction.ts";
@@ -281,6 +278,7 @@ class DurableWorkflowComposition
281
278
  signal: AbortSignal,
282
279
  ): Promise<DurableRunResources> {
283
280
  signal.throwIfAborted();
281
+ assertExecutionsSettled(this.#options.stateRoot.rootDir);
284
282
  const pending = this.#initializing.get(input.runId);
285
283
  if (pending) return pending;
286
284
  if (
@@ -330,6 +328,7 @@ class DurableWorkflowComposition
330
328
  baselineRevisionId?: string;
331
329
  currentWorkspaceRevisionId?: string;
332
330
  }): DurableRunResources {
331
+ assertExecutionsSettled(this.#options.stateRoot.rootDir);
333
332
  const existing = this.#runs.get(input.runId);
334
333
  if (existing) {
335
334
  if (
@@ -358,7 +357,16 @@ class DurableWorkflowComposition
358
357
  const currentRevisionId = input.currentWorkspaceRevisionId;
359
358
  if (!baselineRevisionId || !currentRevisionId)
360
359
  throw new Error("workflow-workspace-revision-unavailable");
361
- workspaces.getRevision(baselineRevisionId);
360
+ let originalBaseline = workspaces.getRevision(baselineRevisionId);
361
+ const ancestors = new Set<string>();
362
+ while (originalBaseline.parentRevisionId !== null) {
363
+ if (ancestors.has(originalBaseline.revisionId))
364
+ throw new Error("workflow-baseline-revision-conflict");
365
+ ancestors.add(originalBaseline.revisionId);
366
+ originalBaseline = workspaces.getRevision(
367
+ originalBaseline.parentRevisionId,
368
+ );
369
+ }
362
370
  workspaces.getRevision(currentRevisionId);
363
371
  let resources!: DurableRunResources;
364
372
  const transactions = new ApplyTransaction({
@@ -366,6 +374,7 @@ class DurableWorkflowComposition
366
374
  artifacts,
367
375
  workspaces,
368
376
  hooks: {
377
+ awaitPostApplySettlement: this.#options.awaitPostApplySettlement,
369
378
  postApply: ({ transactionId, root: verificationRoot, signal }) =>
370
379
  this.#verification.verifyPostApply(
371
380
  resources,
@@ -385,11 +394,13 @@ class DurableWorkflowComposition
385
394
  ledgers: new Map(),
386
395
  transactions,
387
396
  baselineRevisionId,
397
+ originalBaselineRevisionId: originalBaseline.revisionId,
388
398
  currentRevisionId,
389
399
  mergeTail: Promise.resolve(),
390
400
  baselinePromises: new Map(),
391
401
  closed: false,
392
402
  };
403
+ resources.baselineLedger = this.#ledger(resources, 1);
393
404
  this.#runs.set(input.runId, resources);
394
405
  return resources;
395
406
  }
@@ -729,37 +740,10 @@ class DurableWorkflowComposition
729
740
  }
730
741
  if (!grew) break;
731
742
  }
732
- if (hasVerificationLifecycle(input.plan)) {
733
- const retainedRevisions = new Set(
734
- input.tasks
735
- .filter((task) => !invalidated.has(task.taskId))
736
- .map((task) => task.deliveryRevision),
737
- );
738
- for (const deliveryRevision of retainedRevisions) {
739
- const ledger = this.#ledger(resources, deliveryRevision);
740
- const stored = ledger.durableFact(
741
- this.#verification.baselineFactKey(resources),
742
- );
743
- resources.baselinePromises.delete(deliveryRevision);
744
- if (stored === undefined) continue;
745
- const baseline = parseVerificationBaseline(stored, resources.artifacts);
746
- if (baseline.revisionId === expandedBaseline.revisionId) continue;
747
- if (baseline.revisionId !== input.baselineRevisionId) {
748
- throw new Error("workflow-verification-baseline-conflict");
749
- }
750
- ledger.replaceDurableFact(
751
- this.#verification.baselineFactKey(resources),
752
- stored,
753
- verificationBaselineFact(
754
- {
755
- ...baseline,
756
- revisionId: expandedBaseline.revisionId,
757
- },
758
- resources.artifacts,
759
- ),
760
- );
761
- }
762
- }
743
+ // Boundary expansion changes candidate/apply inputs, never the root capture
744
+ // or its verification observations. Their contract-bound caches revalidate
745
+ // only the obligations changed by this delivery.
746
+ resources.baselinePromises.clear();
763
747
  resources.baselineRevisionId = expandedBaseline.revisionId;
764
748
  resources.currentRevisionId = rebuilt.revisionId;
765
749
  if (hasVerificationLifecycle(input.plan)) {
@@ -914,12 +898,13 @@ class DurableWorkflowComposition
914
898
  }
915
899
 
916
900
  cleanup(runId: string): void {
901
+ assertExecutionsSettled(this.#options.stateRoot.rootDir, true);
917
902
  const resources = this.#runs.get(runId);
918
903
  if (resources) {
919
904
  this.#closeResources(resources);
920
905
  this.#runs.delete(runId);
921
906
  }
922
- rmSync(this.#runRoot(runId), { recursive: true, force: true });
907
+ removePathSync(this.#runRoot(runId), { recursive: true, force: true });
923
908
  }
924
909
 
925
910
  close(): void {
@@ -932,6 +917,36 @@ class DurableWorkflowComposition
932
917
  runAttempt(input: Parameters<WorkflowWorker["runAttempt"]>[0]) {
933
918
  return this.#phase.runAttempt(input);
934
919
  }
920
+
921
+ async prepareTask(input: Parameters<WorkflowWorker["runAttempt"]>[0]) {
922
+ const resources = await this.#prepareResources(input, input.signal);
923
+ const workspace = {
924
+ baselineRevisionId: resources.baselineRevisionId,
925
+ currentWorkspaceRevisionId: resources.currentRevisionId,
926
+ };
927
+ if (hasVerificationLifecycle(input.plan)) {
928
+ const captured = await this.#verification.ensureVerificationBaseline({
929
+ resources,
930
+ ledger: this.#ledger(resources, input.deliveryRevision),
931
+ taskId: input.taskId,
932
+ signal: input.signal,
933
+ });
934
+ if (!captured.ok)
935
+ return {
936
+ ...captured.outcome,
937
+ ...workspace,
938
+ ...(captured.outcome.kind === "paused"
939
+ ? {
940
+ verification: {
941
+ scope: "baseline" as const,
942
+ attribution: "environment" as const,
943
+ },
944
+ }
945
+ : {}),
946
+ };
947
+ }
948
+ return { kind: "prepared" as const, ...workspace };
949
+ }
935
950
  hasPendingVerification(input: Parameters<WorkflowWorker["runAttempt"]>[0]) {
936
951
  return this.#phase.hasPendingVerification(input);
937
952
  }
@@ -1,8 +1,12 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
1
4
  export interface ExecutionProfile {
2
- mode: "isolated" | "local-trusted";
5
+ mode: "isolated" | "local-trusted" | "host-trusted";
3
6
  timeoutMs: number;
4
7
  bwrapPath: string;
5
8
  inheritEnvironment: string[];
9
+ windowsHelperPath?: string;
6
10
  }
7
11
 
8
12
  /** Operator configuration only. Unknown modes never silently disable isolation. */
@@ -10,7 +14,11 @@ export function executionProfile(
10
14
  env: NodeJS.ProcessEnv = process.env,
11
15
  ): ExecutionProfile {
12
16
  const mode = env.ABEL_EXECUTION_MODE ?? "isolated";
13
- if (mode !== "isolated" && mode !== "local-trusted")
17
+ if (
18
+ mode !== "isolated" &&
19
+ mode !== "local-trusted" &&
20
+ mode !== "host-trusted"
21
+ )
14
22
  throw new Error("execution-mode-invalid");
15
23
  const timeoutMs = Number(env.ABEL_VERIFICATION_TIMEOUT_MS ?? 600_000);
16
24
  if (
@@ -29,10 +37,28 @@ export function executionProfile(
29
37
  throw new Error("verification-environment-names-invalid");
30
38
  if (mode === "isolated" && inheritEnvironment.length)
31
39
  throw new Error("verification-environment-requires-trusted-mode");
40
+ if (
41
+ mode === "host-trusted" &&
42
+ env.ABEL_WINDOWS_JOB_HELPER !== undefined &&
43
+ (!env.ABEL_WINDOWS_JOB_HELPER ||
44
+ env.ABEL_WINDOWS_JOB_HELPER.includes("\0") ||
45
+ !(
46
+ path.isAbsolute(env.ABEL_WINDOWS_JOB_HELPER) ||
47
+ path.win32.isAbsolute(env.ABEL_WINDOWS_JOB_HELPER)
48
+ ))
49
+ )
50
+ throw new Error("windows-job-helper-path-invalid");
32
51
  return {
33
52
  mode,
34
53
  timeoutMs,
35
54
  bwrapPath: env.ABEL_BWRAP_PATH ?? "/usr/bin/bwrap",
36
55
  inheritEnvironment,
56
+ ...(mode === "host-trusted"
57
+ ? {
58
+ windowsHelperPath:
59
+ env.ABEL_WINDOWS_JOB_HELPER ??
60
+ fileURLToPath(new URL("./windows-job.exe", import.meta.url)),
61
+ }
62
+ : {}),
37
63
  };
38
64
  }
@@ -0,0 +1,77 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ fsyncSync,
5
+ lstatSync,
6
+ openSync,
7
+ readdirSync,
8
+ unlinkSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import path from "node:path";
12
+ import { syncDirectory } from "./directory-sync.ts";
13
+
14
+ const prefix = ".cadence-execution-";
15
+ const active = new Set<string>();
16
+ const retained = new Map<string, readonly string[]>();
17
+ /** Written before launching a host process. Absence of a settlement receipt
18
+ * survives host loss; reopening must not infer ownership from a recycled PID. */
19
+ export function retainExecution(ownerRoot: string, roots: readonly string[]) {
20
+ assertExecutionsSettled(ownerRoot);
21
+ const file = path.join(ownerRoot, `${prefix}${randomUUID()}.json`);
22
+ const fd = openSync(file, "wx", 0o600);
23
+ try {
24
+ writeFileSync(
25
+ fd,
26
+ JSON.stringify({
27
+ version: 1,
28
+ roots: roots.map((root) => path.resolve(root)),
29
+ }),
30
+ );
31
+ fsyncSync(fd);
32
+ } finally {
33
+ closeSync(fd);
34
+ }
35
+ syncDirectory(ownerRoot);
36
+ active.add(file);
37
+ retained.set(file, roots);
38
+ return {
39
+ settled() {
40
+ unlinkSync(file);
41
+ syncDirectory(ownerRoot);
42
+ active.delete(file);
43
+ retained.delete(file);
44
+ },
45
+ uncertain() {
46
+ active.delete(file);
47
+ },
48
+ };
49
+ }
50
+ export function assertExecutionsSettled(
51
+ ownerRoot: string,
52
+ includeActive = false,
53
+ ): void {
54
+ if (!lstatSync(ownerRoot, { throwIfNoEntry: false })) return;
55
+ if (
56
+ readdirSync(ownerRoot).some(
57
+ (name) =>
58
+ name.startsWith(prefix) &&
59
+ (includeActive || !active.has(path.join(ownerRoot, name))),
60
+ )
61
+ )
62
+ throw new Error("isolation-termination-unconfirmed");
63
+ }
64
+ export function isExecutionRetained(root: string): boolean {
65
+ const resolved = path.resolve(root);
66
+ return [...retained.values()].some((roots) =>
67
+ roots.some((candidate) => {
68
+ const relative = path.relative(resolved, path.resolve(candidate));
69
+ return (
70
+ relative === "" ||
71
+ (relative !== ".." &&
72
+ !relative.startsWith(`..${path.sep}`) &&
73
+ !path.isAbsolute(relative))
74
+ );
75
+ }),
76
+ );
77
+ }