@abelxiaoxing/cadence 1.3.0 → 1.4.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 +115 -10
  2. package/package.json +5 -3
  3. package/prompts/abel-design.md +71 -17
  4. package/prompts/abel-diagnose.md +10 -1
  5. package/prompts/abel-implement.md +118 -20
  6. package/prompts/abel-init.md +9 -2
  7. package/src/apply-transaction.ts +16 -14
  8. package/src/artifact-store.ts +17 -0
  9. package/src/candidate-patch.ts +2 -1
  10. package/src/canonical.ts +16 -0
  11. package/src/change-contract.ts +268 -0
  12. package/src/child-session.ts +4 -3
  13. package/src/contracts.ts +141 -33
  14. package/src/control-contracts.ts +51 -1
  15. package/src/delivery-compiler.ts +148 -31
  16. package/src/design-control.ts +69 -11
  17. package/src/design-journal.ts +216 -72
  18. package/src/durable-workflow.ts +4385 -0
  19. package/src/implement-graph.ts +11 -16
  20. package/src/index.ts +380 -981
  21. package/src/isolation-backend.ts +84 -23
  22. package/src/package-delivery.ts +486 -0
  23. package/src/package-verification.ts +509 -0
  24. package/src/route-policy.ts +1 -11
  25. package/src/run-state.ts +4 -16
  26. package/src/run-store.ts +5 -144
  27. package/src/scoped-grep-worker.mjs +23 -0
  28. package/src/scoped-tools.ts +312 -116
  29. package/src/sqlite-schema.ts +171 -0
  30. package/src/storage-schema.ts +352 -0
  31. package/src/subagent-activity.ts +14 -1
  32. package/src/submit-tool.ts +20 -2
  33. package/src/task-ledger.ts +14 -55
  34. package/src/verification-capability.ts +298 -102
  35. package/src/verification-environment.ts +213 -0
  36. package/src/verification-identity.ts +77 -0
  37. package/src/worker-broker.ts +97 -34
  38. package/src/workflow-engine.ts +28 -8285
  39. package/src/workflow-policy.ts +1427 -0
  40. package/src/workflow-state-machine.ts +4478 -0
  41. package/src/workspace-io-entry.mjs +58 -0
  42. package/src/workspace-io-worker.mjs +984 -0
  43. package/src/workspace-io.ts +84 -0
  44. package/src/workspace-store.ts +125 -19
  45. package/skills/abel-workflow/SKILL.md +0 -205
@@ -0,0 +1,268 @@
1
+ import {
2
+ type ImplementTaskBoundary,
3
+ isValidRelativePath,
4
+ type StructuredVerificationContract,
5
+ validateVerificationContract,
6
+ verificationInputPaths,
7
+ } from "./contracts.ts";
8
+ import { canonicalJson } from "./run-state.ts";
9
+
10
+ export interface ChangeContract {
11
+ goal: string;
12
+ acceptance: Array<{
13
+ id: string;
14
+ statement: string;
15
+ verification: StructuredVerificationContract;
16
+ }>;
17
+ constraints: Array<{ id: string; statement: string }>;
18
+ policy: {
19
+ writeRoots: string[];
20
+ dependencies: string[];
21
+ verificationModes: Array<"behavior" | "mechanical" | "refactor">;
22
+ };
23
+ }
24
+
25
+ function record(value: unknown): value is Record<string, unknown> {
26
+ return !!value && typeof value === "object" && !Array.isArray(value);
27
+ }
28
+ function keys(value: Record<string, unknown>, expected: string[]) {
29
+ return (
30
+ canonicalJson(Object.keys(value).sort()) === canonicalJson(expected.sort())
31
+ );
32
+ }
33
+ function text(value: unknown): value is string {
34
+ return (
35
+ typeof value === "string" && value.trim().length > 0 && value.length <= 8192
36
+ );
37
+ }
38
+ const identifier = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/u;
39
+
40
+ export function normalizeChangeContract(value: unknown): ChangeContract {
41
+ if (
42
+ !record(value) ||
43
+ !keys(value, ["goal", "acceptance", "constraints", "policy"]) ||
44
+ !text(value.goal) ||
45
+ !Array.isArray(value.acceptance) ||
46
+ value.acceptance.length < 1 ||
47
+ value.acceptance.length > 256 ||
48
+ !Array.isArray(value.constraints) ||
49
+ value.constraints.length > 256 ||
50
+ !record(value.policy)
51
+ )
52
+ throw new Error("change-contract-invalid");
53
+ const entries = (values: unknown[], acceptance: boolean) =>
54
+ values
55
+ .map((entry) => {
56
+ if (
57
+ !record(entry) ||
58
+ !keys(
59
+ entry,
60
+ acceptance
61
+ ? ["id", "statement", "verification"]
62
+ : ["id", "statement"],
63
+ ) ||
64
+ typeof entry.id !== "string" ||
65
+ !identifier.test(entry.id) ||
66
+ !text(entry.statement)
67
+ )
68
+ throw new Error("change-contract-invalid");
69
+ const verification = acceptance
70
+ ? validateVerificationContract(entry.verification)
71
+ : undefined;
72
+ if (verification && !verification.ok)
73
+ throw new Error("change-contract-invalid");
74
+ return {
75
+ id: entry.id,
76
+ statement: entry.statement.replace(/\r\n?/gu, "\n").trim(),
77
+ ...(verification?.ok ? { verification: verification.value } : {}),
78
+ };
79
+ })
80
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
81
+ const acceptance = entries(value.acceptance, true);
82
+ const constraints = entries(value.constraints, false);
83
+ for (const group of [acceptance, constraints])
84
+ if (new Set(group.map((item) => item.id)).size !== group.length)
85
+ throw new Error("change-contract-invalid");
86
+ const policy = value.policy;
87
+ if (!keys(policy, ["writeRoots", "dependencies", "verificationModes"]))
88
+ throw new Error("change-contract-invalid");
89
+ for (const key of ["writeRoots", "dependencies", "verificationModes"])
90
+ if (
91
+ !Array.isArray(policy[key]) ||
92
+ policy[key].length > 256 ||
93
+ !policy[key].every(text) ||
94
+ new Set(policy[key]).size !== policy[key].length
95
+ )
96
+ throw new Error("change-contract-invalid");
97
+ const roots = policy.writeRoots as string[];
98
+ const modes = policy.verificationModes as string[];
99
+ if (
100
+ !roots.length ||
101
+ !roots.every((root) => root === "." || isValidRelativePath(root)) ||
102
+ !modes.length ||
103
+ !modes.every((mode) =>
104
+ ["behavior", "mechanical", "refactor"].includes(mode),
105
+ )
106
+ )
107
+ throw new Error("change-contract-invalid");
108
+ const result = {
109
+ goal: value.goal.replace(/\r\n?/gu, "\n").trim(),
110
+ acceptance,
111
+ constraints,
112
+ policy: {
113
+ writeRoots: [...roots].sort(),
114
+ dependencies: [...(policy.dependencies as string[])].sort(),
115
+ verificationModes: [...modes].sort(),
116
+ },
117
+ } as ChangeContract;
118
+ if (Buffer.byteLength(canonicalJson(result)) > 64 * 1024)
119
+ throw new Error("change-contract-invalid");
120
+ return result;
121
+ }
122
+
123
+ /** Display identities and fresh input bindings do not change a verification obligation. */
124
+ export function verificationObligation(
125
+ value: StructuredVerificationContract,
126
+ ): string {
127
+ const strip = (item: unknown): unknown =>
128
+ Array.isArray(item)
129
+ ? item.map(strip)
130
+ : record(item)
131
+ ? Object.fromEntries(
132
+ Object.entries(item)
133
+ .filter(([key]) => !["id", "executionBindings"].includes(key))
134
+ .map(([key, child]) => [key, strip(child)]),
135
+ )
136
+ : item;
137
+ return canonicalJson(strip(value));
138
+ }
139
+
140
+ export function assertPlanWithinChangeContract(
141
+ contract: ChangeContract,
142
+ tasks: readonly (ImplementTaskBoundary & {
143
+ verificationMode?: string;
144
+ affectedVerification: StructuredVerificationContract;
145
+ repairVerification: StructuredVerificationContract;
146
+ })[],
147
+ verifications: readonly StructuredVerificationContract[],
148
+ ): void {
149
+ const available = new Set(
150
+ [
151
+ ...verifications,
152
+ ...tasks.flatMap((task) => [
153
+ task.affectedVerification,
154
+ ...Object.entries(task.phases)
155
+ .filter(
156
+ ([phase]) =>
157
+ phase !== "red" ||
158
+ !task.verificationMode ||
159
+ task.verificationMode === "behavior",
160
+ )
161
+ .map(([, phase]) => phase.verification),
162
+ ]),
163
+ ].map(verificationObligation),
164
+ );
165
+ for (const accepted of contract.acceptance)
166
+ if (!available.has(verificationObligation(accepted.verification)))
167
+ throw new Error("change-contract-acceptance-missing");
168
+ for (const task of tasks) {
169
+ if (
170
+ !contract.policy.verificationModes.includes(
171
+ (task.verificationMode ?? "behavior") as "behavior",
172
+ )
173
+ )
174
+ throw new Error("change-contract-verification-mode");
175
+ if (
176
+ task.approvedDependencies.some(
177
+ (name) => !contract.policy.dependencies.includes(name),
178
+ )
179
+ )
180
+ throw new Error("change-contract-dependency-outside-policy");
181
+ if (
182
+ task.agents &&
183
+ task.agents.impact !== "none" &&
184
+ !contract.policy.writeRoots.includes(task.agents.target ?? "")
185
+ )
186
+ throw new Error("change-contract-agents-outside-policy");
187
+ const mode = task.verificationMode ?? "behavior";
188
+ const writes = Object.entries(task.phases)
189
+ .filter(([phase]) => phase !== "red" || mode === "behavior")
190
+ .flatMap(([, phase]) => [...phase.write, ...phase.delete]);
191
+ if (
192
+ mode !== "behavior" &&
193
+ task.impactClosure?.changedSurfaces.some((surface) => surface !== "none")
194
+ )
195
+ throw new Error("change-contract-behavior-requires-red");
196
+ if (
197
+ mode === "mechanical" &&
198
+ writes.some((file) => !/\.(?:md|txt|json|ya?ml|toml|lock)$/iu.test(file))
199
+ )
200
+ throw new Error("change-contract-mechanical-path");
201
+ if (
202
+ mode === "refactor" &&
203
+ contract.acceptance.some((accepted) =>
204
+ verificationInputPaths(accepted.verification).some((file) =>
205
+ writes.includes(file),
206
+ ),
207
+ )
208
+ )
209
+ throw new Error("change-contract-refactor-verification-mutation");
210
+ for (const phase of Object.values(task.phases))
211
+ for (const file of [...phase.write, ...phase.delete])
212
+ if (
213
+ !contract.policy.writeRoots.some(
214
+ (root) =>
215
+ root === "." || file === root || file.startsWith(`${root}/`),
216
+ )
217
+ )
218
+ throw new Error("change-contract-write-outside-policy");
219
+ }
220
+ }
221
+
222
+ export function retainedChangeContract(plan: {
223
+ changeContract?: ChangeContract;
224
+ changeId: string;
225
+ tasks: readonly (ImplementTaskBoundary & {
226
+ affectedVerification: StructuredVerificationContract;
227
+ })[];
228
+ verification: {
229
+ change: {
230
+ fullSuite: StructuredVerificationContract;
231
+ postApply: StructuredVerificationContract;
232
+ };
233
+ };
234
+ }): ChangeContract {
235
+ if (plan.changeContract) return normalizeChangeContract(plan.changeContract);
236
+ return normalizeChangeContract({
237
+ goal: plan.changeId,
238
+ acceptance: [
239
+ plan.verification.change.fullSuite,
240
+ plan.verification.change.postApply,
241
+ ...plan.tasks.map((task) => task.affectedVerification),
242
+ ].map((verification, index) => ({
243
+ id: `legacy-${index}`,
244
+ statement: "Retained verification obligation",
245
+ verification,
246
+ })),
247
+ constraints: [],
248
+ policy: {
249
+ writeRoots: [
250
+ ...new Set(
251
+ plan.tasks.flatMap((task) => [
252
+ ...(task.agents.impact !== "none" && task.agents.target
253
+ ? [task.agents.target]
254
+ : []),
255
+ ...Object.values(task.phases).flatMap((phase) => [
256
+ ...phase.write,
257
+ ...phase.delete,
258
+ ]),
259
+ ]),
260
+ ),
261
+ ],
262
+ dependencies: [
263
+ ...new Set(plan.tasks.flatMap((task) => task.approvedDependencies)),
264
+ ],
265
+ verificationModes: ["behavior"],
266
+ },
267
+ });
268
+ }
@@ -120,6 +120,7 @@ function wrapScopedTools(
120
120
  ) {
121
121
  const order = ["read", "grep", "find", "ls"];
122
122
  return createScopedTools({
123
+ cwd,
123
124
  roots,
124
125
  ...(allowedPaths
125
126
  ? {
@@ -162,9 +163,10 @@ function wrapScopedTools(
162
163
  : Type.Object({
163
164
  path: Type.Optional(Type.String()),
164
165
  }),
165
- async execute(_id, params) {
166
+ async execute(_id, params, signal) {
166
167
  const result = await scoped.execute(
167
168
  params as Record<string, unknown>,
169
+ signal,
168
170
  );
169
171
  return {
170
172
  content: [{ type: "text" as const, text: JSON.stringify(result) }],
@@ -625,8 +627,7 @@ export async function runChildSession(input: {
625
627
  }
626
628
  const failureKind: ChildFailureKind = timedOut
627
629
  ? "timed-out"
628
- : input.signal !== undefined &&
629
- (input.signal.aborted || abort.signal.reason === input.signal.reason)
630
+ : input.signal?.aborted === true
630
631
  ? "cancelled"
631
632
  : "failed";
632
633
  const classification = classifySession();
package/src/contracts.ts CHANGED
@@ -150,6 +150,9 @@ export const VERIFICATION_ADAPTER_CODES = [
150
150
  "script-command-mismatch",
151
151
  "script-missing",
152
152
  "script-unsafe",
153
+ "report-arguments-conflict",
154
+ "verification-config-mismatch",
155
+ "verification-config-unsafe",
153
156
  ] as const;
154
157
  export const CHILD_TRANSPORT_CODES = [
155
158
  "child-no-final-assistant",
@@ -249,10 +252,35 @@ export type VerificationClassification =
249
252
  | "expected-green"
250
253
  | "expected-refactor";
251
254
 
255
+ /** Runtime-owned evidence; unavailable checks never become product baselines. */
256
+ export interface VerificationEvidence {
257
+ id: string;
258
+ exitCode: number;
259
+ classification: VerificationClassification;
260
+ tests?: number;
261
+ failureIdentities: string[];
262
+ policy: "report-file-v3";
263
+ }
264
+ export type VerificationObservation =
265
+ | { kind: "accepted"; evidence: VerificationEvidence }
266
+ | {
267
+ kind: "rejected";
268
+ code: "red-not-witnessed" | "verification-rejected";
269
+ evidence: VerificationEvidence;
270
+ }
271
+ | {
272
+ kind: "unavailable";
273
+ category: "environment" | "adapter" | "resource";
274
+ code: string;
275
+ verificationId: string;
276
+ }
277
+ | { kind: "cancelled" };
278
+
252
279
  interface VerificationBase {
253
280
  id: string;
254
281
  classification: VerificationClassification;
255
282
  expectedFailure?: string;
283
+ executionBindings?: Record<string, string | null>;
256
284
  }
257
285
 
258
286
  export type PackageManager = "bun" | "npm" | "pnpm" | "yarn";
@@ -338,6 +366,7 @@ export interface PhaseBoundary {
338
366
  }
339
367
 
340
368
  export interface TaskBoundary {
369
+ verificationMode?: "behavior" | "mechanical" | "refactor";
341
370
  changeId: string;
342
371
  taskId: string;
343
372
  dependsOn: string[];
@@ -512,7 +541,7 @@ function validScriptCommand(value: unknown): value is string {
512
541
  value.length > 0 &&
513
542
  value.length <= 1024 &&
514
543
  value.trim() === value &&
515
- !UNSAFE_VERIFICATION_TOKEN.test(value)
544
+ !value.includes("\0")
516
545
  );
517
546
  }
518
547
 
@@ -575,6 +604,7 @@ function validateExecutableRunner(value: unknown): value is ExecutableRunner {
575
604
 
576
605
  function validateVitestRunner(value: ExecutableRunner): boolean {
577
606
  if (value.kind === "package-script") {
607
+ if (UNSAFE_VERIFICATION_TOKEN.test(value.command)) return false;
578
608
  const tokens = value.command.split(/\s+/u);
579
609
  return (
580
610
  tokens[0] === "vitest" ||
@@ -591,8 +621,23 @@ function validateAtomicVerification(
591
621
  ): value is AtomicVerificationContract {
592
622
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
593
623
  const contract = value as Record<string, unknown>;
594
- const expectedFailure =
595
- contract.classification === "expected-red" ? ["expectedFailure"] : [];
624
+ if (
625
+ contract.executionBindings !== undefined &&
626
+ (!contract.executionBindings ||
627
+ typeof contract.executionBindings !== "object" ||
628
+ Array.isArray(contract.executionBindings) ||
629
+ Object.entries(contract.executionBindings).some(
630
+ ([key, hash]) =>
631
+ !VERIFICATION_CONFIGURATION_PATHS.includes(key) ||
632
+ (hash !== null &&
633
+ (typeof hash !== "string" || !/^[a-f0-9]{64}$/u.test(hash))),
634
+ ))
635
+ )
636
+ return false;
637
+ const expectedFailure = [
638
+ ...(contract.classification === "expected-red" ? ["expectedFailure"] : []),
639
+ ...(contract.executionBindings === undefined ? [] : ["executionBindings"]),
640
+ ];
596
641
  if (contract.kind === "vitest") {
597
642
  const executableRunner = contract.runner as ExecutableRunner;
598
643
  return (
@@ -719,6 +764,38 @@ export function verificationSteps(
719
764
  : [structuredClone(normalized.value)];
720
765
  }
721
766
 
767
+ export const VERIFICATION_CONFIGURATION_PATHS: readonly string[] = [
768
+ "package.json",
769
+ "package-lock.json",
770
+ "npm-shrinkwrap.json",
771
+ "bun.lock",
772
+ "bun.lockb",
773
+ "pnpm-lock.yaml",
774
+ "yarn.lock",
775
+ ".npmrc",
776
+ ".yarnrc",
777
+ ".yarnrc.yml",
778
+ "bunfig.toml",
779
+ "pnpm-workspace.yaml",
780
+ ];
781
+
782
+ export function verificationBoundInputPaths(
783
+ verification: VerificationContract,
784
+ ): string[] {
785
+ return [
786
+ ...new Set([
787
+ ...verificationInputPaths(verification),
788
+ ...verificationSteps(verification).flatMap((step) =>
789
+ step.kind === "vitest" ||
790
+ step.kind === "package-script" ||
791
+ ("runner" in step && step.runner.kind === "package-script")
792
+ ? VERIFICATION_CONFIGURATION_PATHS
793
+ : [],
794
+ ),
795
+ ]),
796
+ ];
797
+ }
798
+
722
799
  export function verificationInputPaths(
723
800
  verification: VerificationContract,
724
801
  ): string[] {
@@ -976,6 +1053,7 @@ function isWithinRoots(path: string, roots: string[]): boolean {
976
1053
  function validatePhaseBoundary(
977
1054
  value: unknown,
978
1055
  phase: ImplementationPhase,
1056
+ mode: unknown = "behavior",
979
1057
  ): value is PhaseBoundary {
980
1058
  if (
981
1059
  !hasExactKeys(
@@ -1002,7 +1080,10 @@ function validatePhaseBoundary(
1002
1080
  const verification = validateVerificationContract(value.verification);
1003
1081
  if (!verification.ok) return false;
1004
1082
  const classification = {
1005
- red: "expected-red",
1083
+ red:
1084
+ mode !== undefined && mode !== "behavior"
1085
+ ? "expected-green"
1086
+ : "expected-red",
1006
1087
  green: "expected-green",
1007
1088
  refactor: "expected-refactor",
1008
1089
  } as const;
@@ -1014,19 +1095,27 @@ function validateTaskBoundary(
1014
1095
  snapshot: unknown,
1015
1096
  ): value is TaskBoundary {
1016
1097
  if (
1017
- !hasExactKeys(value, [
1018
- "changeId",
1019
- "taskId",
1020
- "dependsOn",
1021
- "objective",
1022
- "context",
1023
- "roots",
1024
- "phases",
1025
- "scheduling",
1026
- "agents",
1027
- "approvedDependencies",
1028
- "impactClosure",
1029
- ]) ||
1098
+ !hasExactKeys(
1099
+ value,
1100
+ [
1101
+ "changeId",
1102
+ "taskId",
1103
+ "dependsOn",
1104
+ "objective",
1105
+ "context",
1106
+ "roots",
1107
+ "phases",
1108
+ "scheduling",
1109
+ "agents",
1110
+ "approvedDependencies",
1111
+ "impactClosure",
1112
+ ],
1113
+ ["verificationMode"],
1114
+ ) ||
1115
+ (value.verificationMode !== undefined &&
1116
+ !["behavior", "mechanical", "refactor"].includes(
1117
+ String(value.verificationMode),
1118
+ )) ||
1030
1119
  !validIdentifier(value.changeId) ||
1031
1120
  !validIdentifier(value.taskId) ||
1032
1121
  !validateIdentifierSet(value.dependsOn) ||
@@ -1038,7 +1127,7 @@ function validateTaskBoundary(
1038
1127
  typeof value.context.contract !== "string" ||
1039
1128
  !validateRoots(value.roots) ||
1040
1129
  !hasExactKeys(value.phases, ["red", "green"], ["refactor"]) ||
1041
- !validatePhaseBoundary(value.phases.red, "red") ||
1130
+ !validatePhaseBoundary(value.phases.red, "red", value.verificationMode) ||
1042
1131
  !validatePhaseBoundary(value.phases.green, "green") ||
1043
1132
  (value.phases.refactor !== undefined &&
1044
1133
  !validatePhaseBoundary(value.phases.refactor, "refactor")) ||
@@ -1120,6 +1209,7 @@ export type ImplementGraphContractDiagnostic =
1120
1209
  function phaseBoundaryContractDiagnostic(
1121
1210
  value: unknown,
1122
1211
  phase: ImplementationPhase,
1212
+ mode: unknown = "behavior",
1123
1213
  ): Omit<
1124
1214
  Extract<
1125
1215
  ImplementGraphContractDiagnostic,
@@ -1176,7 +1266,10 @@ function phaseBoundaryContractDiagnostic(
1176
1266
  };
1177
1267
  }
1178
1268
  const classification = {
1179
- red: "expected-red",
1269
+ red:
1270
+ mode !== undefined && mode !== "behavior"
1271
+ ? "expected-green"
1272
+ : "expected-red",
1180
1273
  green: "expected-green",
1181
1274
  refactor: "expected-refactor",
1182
1275
  } as const;
@@ -1227,22 +1320,33 @@ function taskBoundaryContractDiagnostic(
1227
1320
  category,
1228
1321
  });
1229
1322
  if (
1230
- !hasExactKeys(value, [
1231
- "changeId",
1232
- "taskId",
1233
- "dependsOn",
1234
- "objective",
1235
- "context",
1236
- "roots",
1237
- "phases",
1238
- "scheduling",
1239
- "agents",
1240
- "approvedDependencies",
1241
- "impactClosure",
1242
- ])
1323
+ !hasExactKeys(
1324
+ value,
1325
+ [
1326
+ "changeId",
1327
+ "taskId",
1328
+ "dependsOn",
1329
+ "objective",
1330
+ "context",
1331
+ "roots",
1332
+ "phases",
1333
+ "scheduling",
1334
+ "agents",
1335
+ "approvedDependencies",
1336
+ "impactClosure",
1337
+ ],
1338
+ ["verificationMode"],
1339
+ )
1243
1340
  ) {
1244
1341
  return diagnostic("task", "shape");
1245
1342
  }
1343
+ if (
1344
+ value.verificationMode !== undefined &&
1345
+ !["behavior", "mechanical", "refactor"].includes(
1346
+ String(value.verificationMode),
1347
+ )
1348
+ )
1349
+ return diagnostic("verificationMode", "shape");
1246
1350
  if (!validIdentifier(value.changeId)) {
1247
1351
  return diagnostic("changeId", "identifier");
1248
1352
  }
@@ -1275,7 +1379,11 @@ function taskBoundaryContractDiagnostic(
1275
1379
  for (const phase of IMPLEMENTATION_PHASES) {
1276
1380
  const boundary = value.phases[phase];
1277
1381
  if (phase === "refactor" && boundary === undefined) continue;
1278
- const phaseDiagnostic = phaseBoundaryContractDiagnostic(boundary, phase);
1382
+ const phaseDiagnostic = phaseBoundaryContractDiagnostic(
1383
+ boundary,
1384
+ phase,
1385
+ value.verificationMode,
1386
+ );
1279
1387
  if (phaseDiagnostic)
1280
1388
  return { ...diagnostic("phases", "contract"), ...phaseDiagnostic };
1281
1389
  }
@@ -13,6 +13,12 @@ export const CONTROL_COMMANDS = [
13
13
 
14
14
  export const CONTROL_STAGES = ["abel-implement"] as const;
15
15
 
16
+ export interface RecoveryRequest {
17
+ incidentKey: string;
18
+ failureSequence: number;
19
+ reason: "parent-directed-retry" | "route-changed" | "context-extended";
20
+ }
21
+
16
22
  const CONTROL_SCHEMA_PROPERTIES = {
17
23
  command: {
18
24
  type: "string",
@@ -24,6 +30,19 @@ const CONTROL_SCHEMA_PROPERTIES = {
24
30
  deliveryRevision: { type: "integer", minimum: 1 },
25
31
  receiptHash: { type: "string" },
26
32
  routeId: { type: "string" },
33
+ recovery: {
34
+ type: "object",
35
+ additionalProperties: false,
36
+ properties: {
37
+ incidentKey: { type: "string", pattern: "^[a-f0-9]{64}$" },
38
+ failureSequence: { type: "integer", minimum: 1 },
39
+ reason: {
40
+ type: "string",
41
+ enum: ["parent-directed-retry", "route-changed", "context-extended"],
42
+ },
43
+ },
44
+ required: ["incidentKey", "failureSequence", "reason"],
45
+ },
27
46
  } as const;
28
47
 
29
48
  type ControlSchemaField = Exclude<
@@ -61,6 +80,15 @@ export const CONTROL_COMMAND_PARAMETERS = {
61
80
  commandSchema("start", ["stage", "change", "operationId"]),
62
81
  commandSchema("status", ["stage", "change"]),
63
82
  commandSchema("resume", ["stage", "change", "operationId"]),
83
+ commandSchema("resume", ["stage", "change", "operationId", "recovery"]),
84
+ commandSchema("resume", [
85
+ "stage",
86
+ "change",
87
+ "operationId",
88
+ "deliveryRevision",
89
+ "receiptHash",
90
+ "recovery",
91
+ ]),
64
92
  commandSchema("resume", [
65
93
  "stage",
66
94
  "change",
@@ -102,6 +130,7 @@ export interface ResumeControlCommand extends ControlBase {
102
130
  operationId: string;
103
131
  deliveryRevision?: number;
104
132
  receiptHash?: string;
133
+ recovery?: RecoveryRequest;
105
134
  }
106
135
 
107
136
  export interface RebindControlCommand extends ControlBase {
@@ -144,6 +173,7 @@ const CONTROL_TOOL_FIELDS = new Set([
144
173
  "deliveryRevision",
145
174
  "receiptHash",
146
175
  "routeId",
176
+ "recovery",
147
177
  ]);
148
178
 
149
179
  const COMMAND_FIELDS: Readonly<
@@ -158,6 +188,7 @@ const COMMAND_FIELDS: Readonly<
158
188
  "operationId",
159
189
  "deliveryRevision",
160
190
  "receiptHash",
191
+ "recovery",
161
192
  ]),
162
193
  rebind: new Set(["command", "stage", "change", "operationId", "routeId"]),
163
194
  cancel: new Set(["command", "stage", "change", "operationId"]),
@@ -282,13 +313,32 @@ export function validateControlCommand(
282
313
  !hasExactKeys(
283
314
  value,
284
315
  [...common, "operationId"],
285
- ["deliveryRevision", "receiptHash"],
316
+ ["deliveryRevision", "receiptHash", "recovery"],
286
317
  ) ||
287
318
  !validChange(value.change) ||
288
319
  !validOperationId(value.operationId)
289
320
  ) {
290
321
  break;
291
322
  }
323
+ if (
324
+ value.recovery !== undefined &&
325
+ (!isRecord(value.recovery) ||
326
+ !hasExactKeys(value.recovery, [
327
+ "incidentKey",
328
+ "failureSequence",
329
+ "reason",
330
+ ]) ||
331
+ typeof value.recovery.incidentKey !== "string" ||
332
+ !SHA256.test(value.recovery.incidentKey) ||
333
+ !Number.isSafeInteger(value.recovery.failureSequence) ||
334
+ (value.recovery.failureSequence as number) < 1 ||
335
+ ![
336
+ "parent-directed-retry",
337
+ "route-changed",
338
+ "context-extended",
339
+ ].includes(String(value.recovery.reason)))
340
+ )
341
+ break;
292
342
  const hasRevision = value.deliveryRevision !== undefined;
293
343
  const hasReceipt = value.receiptHash !== undefined;
294
344
  if (hasRevision !== hasReceipt) break;