@abelxiaoxing/cadence 1.5.3 → 1.7.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 (38) hide show
  1. package/README.md +61 -1
  2. package/config/plan-draft.example.json +3 -0
  3. package/config/plan-draft.multiple-tasks.example.json +30 -0
  4. package/package.json +1 -1
  5. package/prompts/abel-design.md +33 -3
  6. package/prompts/abel-implement.md +36 -4
  7. package/src/apply-transaction.ts +1 -9
  8. package/src/artifact-store.ts +1 -9
  9. package/src/change-verification.ts +758 -65
  10. package/src/contracts.ts +8 -0
  11. package/src/delivery-compiler.ts +452 -9
  12. package/src/design-control.ts +2 -2
  13. package/src/design-diagnostics.ts +6 -4
  14. package/src/directory-sync.ts +36 -0
  15. package/src/durable-contracts.ts +19 -3
  16. package/src/durable-workflow.ts +47 -38
  17. package/src/implement-continuation.ts +559 -0
  18. package/src/implement-graph.ts +114 -0
  19. package/src/implement-plan.ts +2 -0
  20. package/src/index.ts +179 -1
  21. package/src/operator-cli.mjs +12 -7
  22. package/src/package-state.ts +309 -0
  23. package/src/package-verification.ts +17 -1
  24. package/src/package-workflow.ts +219 -171
  25. package/src/phase-execution.ts +7 -1
  26. package/src/plan-draft-summary.ts +385 -7
  27. package/src/plan-draft.ts +9 -0
  28. package/src/run-store.ts +39 -15
  29. package/src/storage-schema.ts +14 -6
  30. package/src/subagent-activity.ts +3 -1
  31. package/src/verification-capability.ts +17 -5
  32. package/src/verification-environment.ts +20 -2
  33. package/src/workflow-policy.ts +296 -5
  34. package/src/workflow-recovery-policy.ts +2 -0
  35. package/src/workflow-state-machine.ts +400 -131
  36. package/src/workflow-status.ts +251 -59
  37. package/src/workspace-io-worker.mjs +48 -42
  38. package/src/workspace-store.ts +1 -9
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  projectDesignDiagnostic,
5
5
  type SafeDesignDiagnostic,
6
6
  } from "./design-diagnostics.ts";
7
+ import { PackageStateError } from "./package-state.ts";
7
8
  import { openPackageWorkflowService } from "./package-workflow.ts";
8
9
  import { packageContext } from "./pi-adapter.ts";
9
10
 
@@ -47,6 +48,11 @@ import {
47
48
  DesignFinalizationError,
48
49
  validateDesignControlRequest,
49
50
  } from "./design-control.ts";
51
+ import {
52
+ ImplementContinuationDriver,
53
+ implementChangeFromInvocation,
54
+ implementPromptBindsChange,
55
+ } from "./implement-continuation.ts";
50
56
  import { canonicalJson } from "./implement-graph.ts";
51
57
  import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
52
58
 
@@ -457,6 +463,14 @@ function classifyDesignFailure(
457
463
  field?: string;
458
464
  } {
459
465
  const message = error instanceof Error ? error.message : "";
466
+ if (error instanceof PackageStateError) {
467
+ return {
468
+ code: error.code,
469
+ category: "storage",
470
+ retryable: false,
471
+ field: "packageVersion",
472
+ };
473
+ }
460
474
  if (error instanceof DesignControlValidationError) {
461
475
  return {
462
476
  code: "invalid-design-control-request",
@@ -698,7 +712,9 @@ export function registerWorkflowControl(
698
712
  const engines = new Map<string, Promise<WorkflowControlEngine>>();
699
713
  const designOperations = new Set<Promise<void>>();
700
714
  const designFailures = new Map<string, SafeDesignFailure>();
715
+ const implementContinuation = new ImplementContinuationDriver();
701
716
  let pendingPrompt: EligiblePrompt | undefined;
717
+ let pendingImplementChange: string | undefined;
702
718
  let pendingInit = false;
703
719
  let exitingStage = false;
704
720
  let activePrompt: EligiblePrompt | undefined;
@@ -788,6 +804,7 @@ export function registerWorkflowControl(
788
804
  };
789
805
  const deactivateStage = async () => {
790
806
  activePrompt = undefined;
807
+ implementContinuation.deactivate();
791
808
  await packetRuntime.drain();
792
809
  deactivate();
793
810
  };
@@ -1229,6 +1246,25 @@ export function registerWorkflowControl(
1229
1246
  if (exitingStage || !activation.isActive())
1230
1247
  throw new Error("stage-control-mismatch");
1231
1248
  } catch (error) {
1249
+ if (error instanceof PackageStateError) {
1250
+ const payload = {
1251
+ stage: "abel-implement",
1252
+ change: validation.value.change,
1253
+ state: "paused",
1254
+ durable: false,
1255
+ completed: false,
1256
+ pause: { code: error.code },
1257
+ legalCommands: ["status", "start"],
1258
+ tasks: [],
1259
+ queue: [],
1260
+ };
1261
+ return {
1262
+ content: [
1263
+ { type: "text" as const, text: JSON.stringify(payload) },
1264
+ ],
1265
+ details: payload,
1266
+ };
1267
+ }
1232
1268
  if (!(error instanceof RunStoreFormatError)) throw error;
1233
1269
  const payload = runStoreUnavailableStatus(error, validation.value);
1234
1270
  return {
@@ -1321,6 +1357,13 @@ export function registerWorkflowControl(
1321
1357
  registerDispatchTool("packet");
1322
1358
 
1323
1359
  pi.on("tool_result", (event) => {
1360
+ implementContinuation.noteToolResult({
1361
+ toolName: event.toolName,
1362
+ input: event.input,
1363
+ content: event.content,
1364
+ details: event.details,
1365
+ isError: event.isError,
1366
+ });
1324
1367
  if (
1325
1368
  event.toolName !== DISPATCH_TOOL ||
1326
1369
  !event.isError ||
@@ -1357,7 +1400,9 @@ export function registerWorkflowControl(
1357
1400
  };
1358
1401
  });
1359
1402
  pi.on("input", (event) => {
1403
+ implementContinuation.noteInput();
1360
1404
  pendingPrompt = undefined;
1405
+ pendingImplementChange = undefined;
1361
1406
  pendingInit = false;
1362
1407
  const prompt = invokedPrompt(event.text);
1363
1408
  const init = /^\/abel-init(?:\s|$)/u.test(event.text);
@@ -1367,6 +1412,10 @@ export function registerWorkflowControl(
1367
1412
  return { action: prompt || init ? "handled" : "continue" };
1368
1413
  }
1369
1414
  pendingPrompt = prompt;
1415
+ pendingImplementChange =
1416
+ prompt === "abel-implement"
1417
+ ? implementChangeFromInvocation(event.text)
1418
+ : undefined;
1370
1419
  pendingInit = init && hasPackageProvenance(pi, "abel-init");
1371
1420
  if (
1372
1421
  activePrompt &&
@@ -1380,11 +1429,13 @@ export function registerWorkflowControl(
1380
1429
  }
1381
1430
  return { action: "continue" };
1382
1431
  });
1383
- pi.on("before_agent_start", (event) => {
1432
+ pi.on("before_agent_start", (event, ctx) => {
1384
1433
  const prompt = pendingPrompt;
1434
+ const implementChange = pendingImplementChange;
1385
1435
  const init =
1386
1436
  pendingInit && hasExpandedPromptMarker(event.prompt, "abel-init");
1387
1437
  pendingPrompt = undefined;
1438
+ pendingImplementChange = undefined;
1388
1439
  pendingInit = false;
1389
1440
  const verified =
1390
1441
  prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
@@ -1394,10 +1445,30 @@ export function registerWorkflowControl(
1394
1445
  }
1395
1446
  registerDispatchTool(prompt === "abel-implement" ? "command" : "packet");
1396
1447
  activePrompt = prompt;
1448
+ if (prompt === "abel-implement") {
1449
+ if (
1450
+ implementChange &&
1451
+ implementPromptBindsChange(event.prompt, implementChange) &&
1452
+ typeof ctx.cwd === "string"
1453
+ )
1454
+ implementContinuation.activate({
1455
+ cwd: ctx.cwd,
1456
+ change: implementChange,
1457
+ });
1458
+ else implementContinuation.deactivate();
1459
+ } else {
1460
+ implementContinuation.deactivate();
1461
+ }
1397
1462
  }
1398
1463
  if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
1399
1464
  if (activation.isActive() && activePrompt === "abel-design")
1400
1465
  enforceDesignTools();
1466
+ if (
1467
+ activation.isActive() &&
1468
+ activePrompt === "abel-implement" &&
1469
+ typeof ctx.cwd === "string"
1470
+ )
1471
+ implementContinuation.beginParentTurn(ctx.cwd);
1401
1472
  const boundary = activePrompt
1402
1473
  ? `Abel stage ${activePrompt} is active only for the invoked task and its direct follow-ups. If the user ends it or requests an unrelated task, first call abel_dispatch with {"action":"finish"}, then handle that task normally with the restored tools. A successful finish ends stage authority immediately, including within this turn. Do not extend Gates or workflow rules to that task. A direct Gate answer or same-task continuation stays in this stage. Never invoke another Abel stage automatically.`
1403
1474
  : init
@@ -1417,10 +1488,113 @@ export function registerWorkflowControl(
1417
1488
  }
1418
1489
  return { ...event.payload, parallel_tool_calls: true };
1419
1490
  });
1491
+ pi.on("agent_end", async (event, ctx) => {
1492
+ // The host may replace its current signal after abort/settlement. Retain
1493
+ // this run's signal so a late status result cannot revive an aborted turn.
1494
+ const runSignal = ctx.signal;
1495
+ const aborted = () => runSignal?.aborted || ctx.signal?.aborted;
1496
+ if (
1497
+ aborted() ||
1498
+ exitingStage ||
1499
+ !activation.isActive() ||
1500
+ activePrompt !== "abel-implement"
1501
+ ) {
1502
+ return;
1503
+ }
1504
+ if (
1505
+ typeof ctx.hasPendingMessages === "function" &&
1506
+ ctx.hasPendingMessages()
1507
+ ) {
1508
+ return;
1509
+ }
1510
+ const probe = implementContinuation.prepareSettlement({
1511
+ cwd: ctx.cwd,
1512
+ messages: event.messages,
1513
+ });
1514
+ if (!probe) return;
1515
+
1516
+ let status: Record<string, unknown>;
1517
+ try {
1518
+ const engine = await engineFor(ctx);
1519
+ if (
1520
+ aborted() ||
1521
+ exitingStage ||
1522
+ !activation.isActive() ||
1523
+ activePrompt !== "abel-implement"
1524
+ ) {
1525
+ return;
1526
+ }
1527
+ status = await engine.execute(
1528
+ {
1529
+ command: "status",
1530
+ stage: "abel-implement",
1531
+ change: probe.change,
1532
+ },
1533
+ ctx,
1534
+ runSignal,
1535
+ );
1536
+ if (
1537
+ aborted() ||
1538
+ (typeof ctx.hasPendingMessages === "function" &&
1539
+ ctx.hasPendingMessages())
1540
+ ) {
1541
+ return;
1542
+ }
1543
+ } catch {
1544
+ // A host reminder cannot replace a trustworthy local status read.
1545
+ return;
1546
+ }
1547
+
1548
+ const decision = implementContinuation.finishSettlement(probe, status);
1549
+ if (decision?.kind === "terminal") {
1550
+ await deactivateStage();
1551
+ return;
1552
+ }
1553
+ if (!decision) return;
1554
+ if (
1555
+ aborted() ||
1556
+ (typeof ctx.hasPendingMessages === "function" && ctx.hasPendingMessages())
1557
+ ) {
1558
+ return;
1559
+ }
1560
+
1561
+ if (decision.kind === "stalled") {
1562
+ pi.sendMessage(
1563
+ {
1564
+ customType: "abel-implement-continuation-stalled",
1565
+ content: [{ type: "text", text: decision.message }],
1566
+ display: true,
1567
+ details: {
1568
+ code: decision.code,
1569
+ change: decision.change,
1570
+ state: decision.state,
1571
+ statusFingerprint: decision.statusFingerprint,
1572
+ },
1573
+ },
1574
+ { triggerTurn: false },
1575
+ );
1576
+ return;
1577
+ }
1578
+
1579
+ pi.sendMessage(
1580
+ {
1581
+ customType: "abel-implement-continuation",
1582
+ content: [{ type: "text", text: decision.message }],
1583
+ display: false,
1584
+ details: {
1585
+ change: decision.change,
1586
+ statusFingerprint: decision.statusFingerprint,
1587
+ },
1588
+ },
1589
+ { triggerTurn: true, deliverAs: "followUp" },
1590
+ );
1591
+ });
1420
1592
  pi.on("session_start", async (_event, ctx) => {
1421
1593
  pendingPrompt = undefined;
1594
+ pendingImplementChange = undefined;
1422
1595
  pendingInit = false;
1423
1596
  activePrompt = undefined;
1597
+ implementContinuation.deactivate();
1424
1598
  designFailures.clear();
1425
1599
  activity.detach();
1426
1600
  await packetRuntime.drain();
@@ -1429,7 +1603,11 @@ export function registerWorkflowControl(
1429
1603
  if (ctx.mode === "tui") activity.attach(ctx.ui);
1430
1604
  });
1431
1605
  pi.on("session_shutdown", async () => {
1606
+ pendingPrompt = undefined;
1607
+ pendingImplementChange = undefined;
1608
+ pendingInit = false;
1432
1609
  activePrompt = undefined;
1610
+ implementContinuation.deactivate();
1433
1611
  designFailures.clear();
1434
1612
  activity.detach();
1435
1613
  await packetRuntime.drain();
@@ -652,9 +652,6 @@ function observeSafePath(root, relative) {
652
652
  }
653
653
  return { kind: "unsafe", reason: "invalid-path" };
654
654
  }
655
- function isSafeRegularFile(root, relative) {
656
- return observeSafePath(root, relative).kind === "file";
657
- }
658
655
 
659
656
  // src/file-snapshot.ts
660
657
  function sha256(bytes) {
@@ -697,10 +694,16 @@ function capabilitySuccess(verificationId, runnerBindings = []) {
697
694
  }))
698
695
  };
699
696
  }
700
- function adapterFailure(verificationId, code, message) {
697
+ function adapterFailure(verificationId, code, message, inputObservation) {
701
698
  return {
702
699
  ok: false,
703
- diagnostic: { kind: "verification-adapter", code, verificationId, message }
700
+ diagnostic: {
701
+ kind: "verification-adapter",
702
+ code,
703
+ verificationId,
704
+ message,
705
+ ...inputObservation ? { inputObservation } : {}
706
+ }
704
707
  };
705
708
  }
706
709
  function fileStatus(root, relative) {
@@ -1223,8 +1226,10 @@ function validateCapability(root, value, options, requireInputAvailability) {
1223
1226
  function validateAcceptedVerificationCapability(root, verification, dependencyOwner, runnerEnvironment, requireInputAvailability, executionWritePaths) {
1224
1227
  if (requireInputAvailability) {
1225
1228
  for (const input of verificationInputPaths(verification)) {
1226
- if (!isSafeRegularFile(root, input)) {
1227
- return adapterFailure(verification.id, "input-missing", `verification input ${input} is missing or unsafe`);
1229
+ const status = fileStatus(root, input);
1230
+ if (status !== "regular") {
1231
+ const inputObservation = { path: input, kind: status };
1232
+ return adapterFailure(verification.id, status === "absent" ? "input-missing" : "input-unsafe", `verification input ${input} is ${status}`, inputObservation);
1228
1233
  }
1229
1234
  }
1230
1235
  }
@@ -0,0 +1,309 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ existsSync,
5
+ fsyncSync,
6
+ lstatSync,
7
+ mkdirSync,
8
+ openSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ renameSync,
12
+ unlinkSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import path from "node:path";
16
+ import { DatabaseSync } from "node:sqlite";
17
+ import { syncDirectory } from "./directory-sync.ts";
18
+ import { RunStore } from "./run-store.ts";
19
+ import { prepareStateRoot, type ResolvedStateRoot } from "./state-root.ts";
20
+
21
+ export class PackageStateError extends Error {
22
+ readonly code: string;
23
+ constructor(code: string) {
24
+ super(code);
25
+ this.code = code;
26
+ }
27
+ }
28
+ export interface PackageStateNotice {
29
+ reason:
30
+ | "package-upgrade"
31
+ | "unversioned-store"
32
+ | "unreadable-store"
33
+ | "missing-store";
34
+ previousVersion: string | null;
35
+ currentVersion: string;
36
+ backupPath: string;
37
+ oldRunsAbandoned: true;
38
+ workspaceRestored: false;
39
+ warning: string;
40
+ }
41
+ interface ResetIntent
42
+ extends Omit<
43
+ PackageStateNotice,
44
+ "oldRunsAbandoned" | "workspaceRestored" | "warning"
45
+ > {
46
+ backupName: string;
47
+ }
48
+ const fail = (code: string): never => {
49
+ throw new PackageStateError(code);
50
+ };
51
+ const versionParts = (version: string): number[] => {
52
+ if (!/^\d+\.\d+\.\d+$/u.test(version)) fail("package-state-version-invalid");
53
+ const parts = version.split(".").map(Number);
54
+ if (parts.some((part) => !Number.isSafeInteger(part)))
55
+ fail("package-state-version-invalid");
56
+ return parts;
57
+ };
58
+ function compareVersions(left: string, right: string): number {
59
+ const a = versionParts(left),
60
+ b = versionParts(right);
61
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i] ? 1 : -1;
62
+ return 0;
63
+ }
64
+ function regular(file: string): boolean {
65
+ const stat = lstatSync(file, { throwIfNoEntry: false });
66
+ if (!stat) return false;
67
+ if (!stat.isFile() || stat.isSymbolicLink())
68
+ fail("package-state-path-unsafe");
69
+ return true;
70
+ }
71
+ function table(db: DatabaseSync, name: string): boolean {
72
+ return !!db
73
+ .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?")
74
+ .get(name);
75
+ }
76
+ function storedVersion(db: DatabaseSync): string | null {
77
+ if (!table(db, "cadence_package_version")) return null;
78
+ const rows = db.prepare("SELECT version FROM cadence_package_version").all();
79
+ if (rows.length !== 1 || typeof rows[0].version !== "string")
80
+ fail("package-state-version-invalid");
81
+ const version = String(rows[0].version);
82
+ versionParts(version);
83
+ return version;
84
+ }
85
+ // Only inspect the version. Old task/lease/apply records are deliberately not
86
+ // interpreted: this policy abandons them, including malformed or unreadable facts.
87
+ function inspectVersion(file: string): {
88
+ version: string | null;
89
+ unreadable: boolean;
90
+ } {
91
+ let db: DatabaseSync | undefined;
92
+ try {
93
+ db = new DatabaseSync(file, { readOnly: true });
94
+ return { version: storedVersion(db), unreadable: false };
95
+ } catch (error) {
96
+ const failure = error as { errcode?: number; code?: string };
97
+ // An actual SQLite lock is not a stale durable lease record. Do not move
98
+ // storage while an uncoordinated old opener is observably using it.
99
+ if (failure.errcode !== undefined && [5, 6].includes(failure.errcode & 255))
100
+ fail("package-state-in-use");
101
+ if (
102
+ (error instanceof PackageStateError &&
103
+ error.code === "package-state-version-invalid") ||
104
+ (failure.errcode !== undefined &&
105
+ [1, 11, 26].includes(failure.errcode & 255))
106
+ )
107
+ return { version: null, unreadable: true };
108
+ // Schema errors / SQLITE_CORRUPT / SQLITE_NOTADB mean opaque old bytes.
109
+ // Permissions, disk I/O, resource exhaustion and unknown errors do not.
110
+ return fail("package-state-reset-unavailable");
111
+ } finally {
112
+ db?.close();
113
+ }
114
+ }
115
+
116
+ function hasOldResources(state: ResolvedStateRoot): boolean {
117
+ // Cover the production run-data layout and unknown historical resources.
118
+ // Never descend through resource links; the whole root is renamed as-is.
119
+ const emptyScaffolds = new Set(["artifacts", "workspaces", "transactions"]);
120
+ return readdirSync(state.rootDir).some((name) => {
121
+ const file = path.join(state.rootDir, name);
122
+ const stat = lstatSync(file);
123
+ return (
124
+ !emptyScaffolds.has(name) ||
125
+ !stat.isDirectory() ||
126
+ stat.isSymbolicLink() ||
127
+ readdirSync(file).length > 0
128
+ );
129
+ });
130
+ }
131
+
132
+ const RESET_WARNING =
133
+ "Old runs and approval evidence are abandoned, not resumed. Stop all old hosts and execution processes before resetting; the lock cannot fence older versions or detached descendants. The main workspace is not restored: interrupted apply may have left partial changes. Inspect Git diff before starting new work.";
134
+
135
+ function persistIntent(file: string, intent: ResetIntent): void {
136
+ const temp = `${file}.tmp`;
137
+ regular(temp);
138
+ const fd = openSync(temp, "w", 0o600);
139
+ try {
140
+ writeFileSync(fd, JSON.stringify(intent));
141
+ fsyncSync(fd);
142
+ } finally {
143
+ closeSync(fd);
144
+ }
145
+ renameSync(temp, file);
146
+ syncDirectory(path.dirname(file));
147
+ }
148
+
149
+ /** Package boundary only: low-level storage and read-only operator inspection never reset state.
150
+ * The external SQLite write lock is held until every service connection has closed.
151
+ * SQLite releases it on process death; no PID guessing or stale-lock deletion is used.
152
+ */
153
+ export function acquirePackageState(
154
+ unresolved: ResolvedStateRoot,
155
+ version = String(
156
+ JSON.parse(
157
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
158
+ ).version,
159
+ ),
160
+ ): { notice?: PackageStateNotice; close(): void } {
161
+ versionParts(version);
162
+ const state = prepareStateRoot(unresolved);
163
+ for (const suffix of ["-wal", "-shm", "-journal"])
164
+ regular(state.databasePath + suffix);
165
+ const coordinator = path.join(
166
+ state.rootsDir,
167
+ `${state.consumerRootHash}.package-state`,
168
+ );
169
+ const stat = lstatSync(coordinator, { throwIfNoEntry: false });
170
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink()))
171
+ fail("package-state-path-unsafe");
172
+ mkdirSync(coordinator, { recursive: true, mode: 0o700 });
173
+ const lockFile = path.join(coordinator, "lock.sqlite3");
174
+ for (const suffix of ["", "-journal", "-wal", "-shm"])
175
+ regular(lockFile + suffix);
176
+ if (!existsSync(lockFile)) {
177
+ try {
178
+ closeSync(openSync(lockFile, "wx", 0o600));
179
+ } catch (error) {
180
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
181
+ }
182
+ }
183
+ const lock = new DatabaseSync(lockFile);
184
+ let closed = false;
185
+ const close = () => {
186
+ if (!closed) {
187
+ closed = true;
188
+ lock.close();
189
+ }
190
+ };
191
+ try {
192
+ lock.exec("PRAGMA busy_timeout = 1; BEGIN IMMEDIATE");
193
+ } catch {
194
+ close();
195
+ fail("package-state-in-use");
196
+ }
197
+ try {
198
+ const intentFile = path.join(coordinator, "reset.json");
199
+ let intent: ResetIntent | undefined;
200
+ if (regular(intentFile)) {
201
+ intent = JSON.parse(readFileSync(intentFile, "utf8")) as ResetIntent;
202
+ if (
203
+ !intent ||
204
+ !/^backup-[a-f0-9-]{36}$/u.test(intent.backupName) ||
205
+ intent.backupPath !== path.join(coordinator, intent.backupName) ||
206
+ ![
207
+ "package-upgrade",
208
+ "unversioned-store",
209
+ "unreadable-store",
210
+ "missing-store",
211
+ ].includes(intent.reason) ||
212
+ (intent.previousVersion !== null &&
213
+ typeof intent.previousVersion !== "string")
214
+ )
215
+ fail("package-state-reset-intent-invalid");
216
+ if (intent.currentVersion !== version)
217
+ fail("package-state-reset-version-mismatch");
218
+ }
219
+ let fresh = !regular(state.databasePath);
220
+ if (!intent) {
221
+ const observed = fresh
222
+ ? { version: null, unreadable: false }
223
+ : inspectVersion(state.databasePath);
224
+ if (observed.version === version) return { close };
225
+ if (
226
+ observed.version !== null &&
227
+ compareVersions(observed.version, version) > 0
228
+ )
229
+ fail("package-state-downgrade-blocked");
230
+ if (!fresh || hasOldResources(state)) {
231
+ const backupName = `backup-${randomUUID()}`;
232
+ intent = {
233
+ backupName,
234
+ backupPath: path.join(coordinator, backupName),
235
+ previousVersion: observed.version,
236
+ currentVersion: version,
237
+ reason: fresh
238
+ ? "missing-store"
239
+ : observed.unreadable
240
+ ? "unreadable-store"
241
+ : observed.version === null
242
+ ? "unversioned-store"
243
+ : "package-upgrade",
244
+ };
245
+ persistIntent(intentFile, intent);
246
+ }
247
+ }
248
+ if (intent) {
249
+ const backup = lstatSync(intent.backupPath, { throwIfNoEntry: false });
250
+ if (backup && (!backup.isDirectory() || backup.isSymbolicLink()))
251
+ fail("package-state-path-unsafe");
252
+ if (!backup) {
253
+ // Abandon the old namespace as opaque bytes, including run-data and
254
+ // SQLite sidecars. No transaction replay or main-workspace rollback.
255
+ renameSync(state.rootDir, intent.backupPath);
256
+ syncDirectory(coordinator);
257
+ syncDirectory(state.rootsDir);
258
+ }
259
+ prepareStateRoot(state);
260
+ fresh = true;
261
+ }
262
+ if (fresh) {
263
+ RunStore.open(state).close();
264
+ const db = new DatabaseSync(state.databasePath);
265
+ try {
266
+ db.exec(
267
+ "PRAGMA synchronous = FULL; BEGIN IMMEDIATE; CREATE TABLE IF NOT EXISTS cadence_package_version (version TEXT NOT NULL) STRICT",
268
+ );
269
+ const existing = db
270
+ .prepare("SELECT version FROM cadence_package_version")
271
+ .all();
272
+ if (existing.length === 0)
273
+ db.prepare("INSERT INTO cadence_package_version VALUES (?)").run(
274
+ version,
275
+ );
276
+ else if (existing.length !== 1 || existing[0].version !== version)
277
+ fail("package-state-reset-version-mismatch");
278
+ db.exec("COMMIT");
279
+ } finally {
280
+ db.close();
281
+ }
282
+ syncDirectory(state.rootDir);
283
+ }
284
+ if (intent) {
285
+ unlinkSync(intentFile);
286
+ syncDirectory(coordinator);
287
+ }
288
+ return {
289
+ close,
290
+ ...(intent
291
+ ? {
292
+ notice: {
293
+ reason: intent.reason,
294
+ previousVersion: intent.previousVersion,
295
+ currentVersion: version,
296
+ backupPath: intent.backupPath,
297
+ oldRunsAbandoned: true,
298
+ workspaceRestored: false,
299
+ warning: RESET_WARNING,
300
+ },
301
+ }
302
+ : {}),
303
+ };
304
+ } catch (error) {
305
+ close();
306
+ if (error instanceof PackageStateError) throw error;
307
+ throw new PackageStateError("package-state-reset-unavailable");
308
+ }
309
+ }
@@ -4,6 +4,7 @@ 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";
@@ -321,11 +322,13 @@ export async function executePackageVerification(input: {
321
322
  const unavailable = (
322
323
  code: string,
323
324
  category: "environment" | "adapter" | "resource" = "adapter",
325
+ inputObservation?: VerificationInputObservation,
324
326
  ): VerificationObservation => ({
325
327
  kind: "unavailable",
326
328
  category,
327
329
  code,
328
330
  verificationId: input.verification.id,
331
+ ...(inputObservation ? { inputObservation } : {}),
329
332
  ...(executionDiagnostic
330
333
  ? {
331
334
  diagnostic: {
@@ -349,7 +352,14 @@ export async function executePackageVerification(input: {
349
352
  executionWritePaths: input.executionWritePaths,
350
353
  },
351
354
  );
352
- if (!capability.ok) return unavailable(capability.diagnostic.code);
355
+ if (!capability.ok)
356
+ return unavailable(
357
+ capability.diagnostic.code,
358
+ "adapter",
359
+ capability.diagnostic.kind === "verification-adapter"
360
+ ? capability.diagnostic.inputObservation
361
+ : undefined,
362
+ );
353
363
  let environment: Awaited<ReturnType<typeof prepareVerificationEnvironment>>;
354
364
  try {
355
365
  environment = await prepareVerificationEnvironment(
@@ -552,6 +562,9 @@ export function phaseVerificationResult(result: VerificationObservation) {
552
562
  kind: "paused" as const,
553
563
  code: result.code,
554
564
  category: result.category,
565
+ ...(result.inputObservation
566
+ ? { inputObservation: result.inputObservation }
567
+ : {}),
555
568
  };
556
569
  }
557
570
 
@@ -583,5 +596,8 @@ export function changeVerificationResult(result: VerificationObservation) {
583
596
  ? ("verification-adapter" as const)
584
597
  : ("environment" as const),
585
598
  code: result.code,
599
+ ...(result.inputObservation
600
+ ? { inputObservation: result.inputObservation }
601
+ : {}),
586
602
  };
587
603
  }