@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.
- package/README.md +115 -10
- package/package.json +5 -3
- package/prompts/abel-design.md +71 -17
- package/prompts/abel-diagnose.md +10 -1
- package/prompts/abel-implement.md +118 -20
- package/prompts/abel-init.md +9 -2
- package/src/apply-transaction.ts +16 -14
- package/src/artifact-store.ts +17 -0
- package/src/candidate-patch.ts +2 -1
- package/src/canonical.ts +16 -0
- package/src/change-contract.ts +268 -0
- package/src/child-session.ts +4 -3
- package/src/contracts.ts +141 -33
- package/src/control-contracts.ts +51 -1
- package/src/delivery-compiler.ts +148 -31
- package/src/design-control.ts +69 -11
- package/src/design-journal.ts +216 -72
- package/src/durable-workflow.ts +4385 -0
- package/src/implement-graph.ts +11 -16
- package/src/index.ts +380 -981
- package/src/isolation-backend.ts +84 -23
- package/src/package-delivery.ts +486 -0
- package/src/package-verification.ts +509 -0
- package/src/route-policy.ts +1 -11
- package/src/run-state.ts +4 -16
- package/src/run-store.ts +5 -144
- package/src/scoped-grep-worker.mjs +23 -0
- package/src/scoped-tools.ts +312 -116
- package/src/sqlite-schema.ts +171 -0
- package/src/storage-schema.ts +352 -0
- package/src/subagent-activity.ts +14 -1
- package/src/submit-tool.ts +20 -2
- package/src/task-ledger.ts +14 -55
- package/src/verification-capability.ts +298 -102
- package/src/verification-environment.ts +213 -0
- package/src/verification-identity.ts +77 -0
- package/src/worker-broker.ts +97 -34
- package/src/workflow-engine.ts +28 -8285
- package/src/workflow-policy.ts +1427 -0
- package/src/workflow-state-machine.ts +4478 -0
- package/src/workspace-io-entry.mjs +58 -0
- package/src/workspace-io-worker.mjs +984 -0
- package/src/workspace-io.ts +84 -0
- package/src/workspace-store.ts +125 -19
- 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
|
+
}
|
package/src/child-session.ts
CHANGED
|
@@ -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
|
|
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
|
-
!
|
|
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
|
-
|
|
595
|
-
contract.
|
|
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:
|
|
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(
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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:
|
|
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(
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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(
|
|
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
|
}
|
package/src/control-contracts.ts
CHANGED
|
@@ -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;
|