@abelxiaoxing/cadence 1.0.2 → 1.0.3
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/agents/contract-reviewer.md +2 -2
- package/agents/diagnosis-worker.md +3 -2
- package/agents/implementation-worker.md +3 -2
- package/package.json +1 -1
- package/prompts/abel-design.md +13 -3
- package/prompts/abel-implement.md +50 -21
- package/skills/abel-workflow/SKILL.md +57 -25
- package/src/candidate-preflight.ts +361 -56
- package/src/child-session.ts +92 -27
- package/src/contracts.ts +987 -38
- package/src/index.ts +50 -24
- package/src/parent-provider.ts +89 -23
- package/src/patch.ts +269 -41
- package/src/result-store.ts +254 -16
- package/src/runtime.ts +1281 -560
- package/src/scheduler.ts +59 -16
- package/src/subagent-activity.ts +56 -25
- package/src/submit-tool.ts +12 -2
- package/src/worker.ts +114 -195
package/src/contracts.ts
CHANGED
|
@@ -21,19 +21,22 @@ export const PHASES = [
|
|
|
21
21
|
"review",
|
|
22
22
|
] as const;
|
|
23
23
|
|
|
24
|
-
export const
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
24
|
+
export const AGENTS_IMPACTS = [
|
|
25
|
+
"none",
|
|
26
|
+
"update-existing",
|
|
27
|
+
"create-index",
|
|
28
|
+
"remove-index",
|
|
29
29
|
] as const;
|
|
30
|
+
export type AgentsImpact = (typeof AGENTS_IMPACTS)[number];
|
|
30
31
|
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
export const IMPACT_SURFACES = [
|
|
33
|
+
"none",
|
|
34
|
+
"route-authorization",
|
|
35
|
+
"page-state",
|
|
36
|
+
"api-response",
|
|
37
|
+
"public-html",
|
|
38
|
+
] as const;
|
|
39
|
+
export type ImpactSurface = (typeof IMPACT_SURFACES)[number];
|
|
37
40
|
|
|
38
41
|
export const LIMITS = {
|
|
39
42
|
maxActiveChildSessions: 4,
|
|
@@ -43,6 +46,136 @@ export const LIMITS = {
|
|
|
43
46
|
maxCompleteResultBytes: 64 * 1024,
|
|
44
47
|
} as const;
|
|
45
48
|
|
|
49
|
+
export const ARTIFACT_FAILURE_CODES = [
|
|
50
|
+
"agents-impact-mismatch",
|
|
51
|
+
"agents-target-mismatch",
|
|
52
|
+
"baseline-bound-mismatch",
|
|
53
|
+
"baseline-deletion-kind",
|
|
54
|
+
"baseline-directory-kind",
|
|
55
|
+
"baseline-directory-mismatch",
|
|
56
|
+
"baseline-directory-special-file",
|
|
57
|
+
"baseline-directory-symlink",
|
|
58
|
+
"baseline-file-kind",
|
|
59
|
+
"baseline-shape",
|
|
60
|
+
"candidate-check-failed",
|
|
61
|
+
"checkout-special-file",
|
|
62
|
+
"checkout-symlink",
|
|
63
|
+
"dependency-bound-mismatch",
|
|
64
|
+
"escaping-path",
|
|
65
|
+
"git-apply-check-failed",
|
|
66
|
+
"git-apply-failed",
|
|
67
|
+
"git-numstat-rejected",
|
|
68
|
+
"git-summary-rejected",
|
|
69
|
+
"ignored-baseline-input",
|
|
70
|
+
"incomplete-baseline",
|
|
71
|
+
"invalid-approved-dependencies",
|
|
72
|
+
"invalid-checkpoint-contract",
|
|
73
|
+
"invalid-diff",
|
|
74
|
+
"invalid-diff-bytes",
|
|
75
|
+
"invalid-snapshot",
|
|
76
|
+
"invalid-structural-result",
|
|
77
|
+
"invalid-verification-contract",
|
|
78
|
+
"lockfile-bound-mismatch",
|
|
79
|
+
"noncanonical-path",
|
|
80
|
+
"nonregular-mode",
|
|
81
|
+
"outside-managed-region",
|
|
82
|
+
"package-bound-mismatch",
|
|
83
|
+
"package-manifest-invalid",
|
|
84
|
+
"parent-review-rejected",
|
|
85
|
+
"red-not-witnessed",
|
|
86
|
+
"submodule",
|
|
87
|
+
"target-baseline-missing",
|
|
88
|
+
"verification-baseline-missing",
|
|
89
|
+
"verification-input-unavailable",
|
|
90
|
+
"verification-rejected",
|
|
91
|
+
"verification-report-invalid",
|
|
92
|
+
"verification-report-missing",
|
|
93
|
+
"write-set-mismatch",
|
|
94
|
+
] as const;
|
|
95
|
+
export const STALE_FAILURE_CODES = [
|
|
96
|
+
"baseline-deletion-drift",
|
|
97
|
+
"baseline-directory-drift",
|
|
98
|
+
"baseline-file-drift",
|
|
99
|
+
"git-apply-check-failed",
|
|
100
|
+
"stale-snapshot",
|
|
101
|
+
] as const;
|
|
102
|
+
export const ENVIRONMENT_FAILURE_CODES = [
|
|
103
|
+
"bubblewrap-launch-failed",
|
|
104
|
+
"bubblewrap-or-dependency-unavailable",
|
|
105
|
+
"bun-executable-unavailable",
|
|
106
|
+
"checkpoint-unavailable",
|
|
107
|
+
"checkout-cleanup-failed",
|
|
108
|
+
"checkout-failed",
|
|
109
|
+
"clone-alternates",
|
|
110
|
+
"clone-failed",
|
|
111
|
+
"dependency-path-unsafe",
|
|
112
|
+
"git-apply-check-unavailable",
|
|
113
|
+
"git-apply-failed",
|
|
114
|
+
"git-apply-unavailable",
|
|
115
|
+
"git-ignore-check-failed",
|
|
116
|
+
"git-index-unavailable",
|
|
117
|
+
"root-unavailable",
|
|
118
|
+
"sandbox-runtime-unavailable",
|
|
119
|
+
] as const;
|
|
120
|
+
export const APPROVAL_BOUNDARY_CODES = [
|
|
121
|
+
"unapproved-dependency-change",
|
|
122
|
+
"behavior-contract-insufficient",
|
|
123
|
+
"architecture-contract-insufficient",
|
|
124
|
+
"task-scope-insufficient",
|
|
125
|
+
"verification-contract-insufficient",
|
|
126
|
+
"agents-contract-insufficient",
|
|
127
|
+
] as const;
|
|
128
|
+
export const CHILD_TRANSPORT_CODES = ["timeout", "transport-failure"] as const;
|
|
129
|
+
|
|
130
|
+
export type ArtifactFailureCode = (typeof ARTIFACT_FAILURE_CODES)[number];
|
|
131
|
+
export type StaleFailureCode = (typeof STALE_FAILURE_CODES)[number];
|
|
132
|
+
export type EnvironmentFailureCode = (typeof ENVIRONMENT_FAILURE_CODES)[number];
|
|
133
|
+
export type ApprovalBoundaryCode = (typeof APPROVAL_BOUNDARY_CODES)[number];
|
|
134
|
+
export type ChildTransportCode = (typeof CHILD_TRANSPORT_CODES)[number];
|
|
135
|
+
|
|
136
|
+
export type CandidateFailure =
|
|
137
|
+
| { kind: "artifact"; code: ArtifactFailureCode; evidence?: string[] }
|
|
138
|
+
| { kind: "stale"; code: StaleFailureCode }
|
|
139
|
+
| { kind: "environment"; code: EnvironmentFailureCode }
|
|
140
|
+
| { kind: "approval-boundary"; code: ApprovalBoundaryCode }
|
|
141
|
+
| { kind: "cancelled"; code: "cancelled" }
|
|
142
|
+
| { kind: "result-limit"; limitBytes: number };
|
|
143
|
+
|
|
144
|
+
export type ChildFailure =
|
|
145
|
+
| CandidateFailure
|
|
146
|
+
| { kind: "transport"; code: ChildTransportCode };
|
|
147
|
+
|
|
148
|
+
export type CandidateRejection =
|
|
149
|
+
| {
|
|
150
|
+
kind: "artifact";
|
|
151
|
+
code: "parent-review-rejected";
|
|
152
|
+
evidence?: string[];
|
|
153
|
+
}
|
|
154
|
+
| { kind: "approval-boundary"; code: ApprovalBoundaryCode };
|
|
155
|
+
|
|
156
|
+
export type TaskFailure =
|
|
157
|
+
| { kind: "approval-boundary"; code: ApprovalBoundaryCode }
|
|
158
|
+
| {
|
|
159
|
+
kind: "attempts-exhausted";
|
|
160
|
+
cause: "artifact" | "stale" | "transport";
|
|
161
|
+
}
|
|
162
|
+
| {
|
|
163
|
+
kind: "checkpoint-attempts-exhausted";
|
|
164
|
+
cause: "artifact" | "stale";
|
|
165
|
+
}
|
|
166
|
+
| { kind: "environment"; code: EnvironmentFailureCode }
|
|
167
|
+
| { kind: "result-limit"; limitBytes: number };
|
|
168
|
+
|
|
169
|
+
export interface CandidateApplyEvidence {
|
|
170
|
+
targets: string[];
|
|
171
|
+
checkExitCode: 0;
|
|
172
|
+
applyExitCode: 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export type ApplyCandidateResult =
|
|
176
|
+
| { ok: true; result: CandidateApplyEvidence }
|
|
177
|
+
| { ok: false; failure: CandidateFailure };
|
|
178
|
+
|
|
46
179
|
const NONCANONICAL = /(^|\/)\.\.(\/|$)|(^|\/)\/|^\//;
|
|
47
180
|
const SNAPSHOT_SHA256 = /^[a-f0-9]{64}$/;
|
|
48
181
|
|
|
@@ -54,6 +187,69 @@ export interface VerificationContract {
|
|
|
54
187
|
minTests: number;
|
|
55
188
|
}
|
|
56
189
|
|
|
190
|
+
export interface ImpactClosureContract {
|
|
191
|
+
changedSurfaces: ImpactSurface[];
|
|
192
|
+
searchEvidence: string[];
|
|
193
|
+
relatedTests: Array<{
|
|
194
|
+
path: string;
|
|
195
|
+
disposition: "current-task" | "regression-task" | "unaffected";
|
|
196
|
+
evidence: string;
|
|
197
|
+
regressionTaskId?: string;
|
|
198
|
+
}>;
|
|
199
|
+
affectedSuite: string[];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type ImplementationPhase = "red" | "green" | "refactor";
|
|
203
|
+
|
|
204
|
+
export interface PhaseBoundary {
|
|
205
|
+
read: string[];
|
|
206
|
+
write: string[];
|
|
207
|
+
verification: VerificationContract;
|
|
208
|
+
verificationLock?: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface TaskBoundary {
|
|
212
|
+
changeId: string;
|
|
213
|
+
taskId: string;
|
|
214
|
+
objective: string;
|
|
215
|
+
context: { agents: string; contract: string };
|
|
216
|
+
roots: string[];
|
|
217
|
+
phases: {
|
|
218
|
+
red: PhaseBoundary;
|
|
219
|
+
green: PhaseBoundary;
|
|
220
|
+
refactor?: PhaseBoundary;
|
|
221
|
+
};
|
|
222
|
+
scheduling: { conflicts: string[]; resources: string[] };
|
|
223
|
+
agents: {
|
|
224
|
+
impact: AgentsImpact;
|
|
225
|
+
target?: string;
|
|
226
|
+
managedOnly: true;
|
|
227
|
+
};
|
|
228
|
+
approvedDependencies: string[];
|
|
229
|
+
impactClosure: ImpactClosureContract;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface PhaseAttempt {
|
|
233
|
+
changeId: string;
|
|
234
|
+
taskId: string;
|
|
235
|
+
requestId: string;
|
|
236
|
+
phase: ImplementationPhase;
|
|
237
|
+
snapshot: unknown;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type ImplementRunRequest =
|
|
241
|
+
| {
|
|
242
|
+
stage: "abel-implement";
|
|
243
|
+
kind: "open-task";
|
|
244
|
+
boundary: TaskBoundary;
|
|
245
|
+
attempt: PhaseAttempt;
|
|
246
|
+
}
|
|
247
|
+
| {
|
|
248
|
+
stage: "abel-implement";
|
|
249
|
+
kind: "phase-attempt";
|
|
250
|
+
attempt: PhaseAttempt;
|
|
251
|
+
};
|
|
252
|
+
|
|
57
253
|
export interface RequestEnvelope {
|
|
58
254
|
stage: string;
|
|
59
255
|
role: string;
|
|
@@ -71,21 +267,43 @@ export interface RequestEnvelope {
|
|
|
71
267
|
verificationLock?: string;
|
|
72
268
|
};
|
|
73
269
|
output: string;
|
|
270
|
+
agentsImpact?: AgentsImpact;
|
|
271
|
+
agentsTarget?: string;
|
|
272
|
+
agentsManagedOnly?: true;
|
|
273
|
+
approvedDependencies?: string[];
|
|
274
|
+
impactClosure?: ImpactClosureContract;
|
|
74
275
|
verification?: VerificationContract;
|
|
75
276
|
snapshot?: unknown;
|
|
76
277
|
}
|
|
77
278
|
|
|
78
|
-
export
|
|
79
|
-
|
|
279
|
+
export type RunRequest = RequestEnvelope | ImplementRunRequest;
|
|
280
|
+
|
|
281
|
+
export interface AgentsCheckpointRequest {
|
|
282
|
+
stage: "abel-implement";
|
|
283
|
+
taskId: string;
|
|
284
|
+
agentsImpact: Exclude<AgentsImpact, "none">;
|
|
285
|
+
agentsTarget: string;
|
|
286
|
+
agentsManagedOnly: true;
|
|
287
|
+
stableCheckpoint: true;
|
|
288
|
+
snapshot: unknown;
|
|
289
|
+
diff: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface AgentsCheckpointAttempt {
|
|
293
|
+
changeId: string;
|
|
80
294
|
taskId: string;
|
|
81
295
|
requestId: string;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
296
|
+
snapshot: unknown;
|
|
297
|
+
diff: string;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export interface ImplementApplyOperation {
|
|
301
|
+
resultId: string;
|
|
302
|
+
requestId: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface ImplementDiscardOperation extends ImplementApplyOperation {
|
|
306
|
+
rejection: CandidateRejection;
|
|
89
307
|
}
|
|
90
308
|
|
|
91
309
|
export function isValidRelativePath(p: unknown): p is string {
|
|
@@ -95,12 +313,20 @@ export function isValidRelativePath(p: unknown): p is string {
|
|
|
95
313
|
p.length <= 512 &&
|
|
96
314
|
!NONCANONICAL.test(p) &&
|
|
97
315
|
!p.startsWith("/") &&
|
|
316
|
+
(p === "." ||
|
|
317
|
+
(!p.startsWith("./") &&
|
|
318
|
+
!p.endsWith("/") &&
|
|
319
|
+
!p.split("/").includes("."))) &&
|
|
98
320
|
!p.includes("..") &&
|
|
99
321
|
!p.includes("\\") &&
|
|
100
322
|
!p.includes("\u0000")
|
|
101
323
|
);
|
|
102
324
|
}
|
|
103
325
|
|
|
326
|
+
export function isAgentsPath(p: unknown): p is string {
|
|
327
|
+
return isValidRelativePath(p) && /(?:^|\/)AGENTS\.md$/u.test(p);
|
|
328
|
+
}
|
|
329
|
+
|
|
104
330
|
export function validateVerificationContract(
|
|
105
331
|
value: unknown,
|
|
106
332
|
): { ok: true; value: VerificationContract } | { ok: false; reason: string } {
|
|
@@ -179,6 +405,7 @@ function validateImplementationSnapshot(
|
|
|
179
405
|
const entry = value as Record<string, unknown>;
|
|
180
406
|
if (entry.kind === "file") {
|
|
181
407
|
if (
|
|
408
|
+
!hasExactKeys(entry, ["kind", "sha256", "bytes"]) ||
|
|
182
409
|
typeof entry.sha256 !== "string" ||
|
|
183
410
|
!SNAPSHOT_SHA256.test(entry.sha256) ||
|
|
184
411
|
typeof entry.bytes !== "number" ||
|
|
@@ -189,29 +416,610 @@ function validateImplementationSnapshot(
|
|
|
189
416
|
}
|
|
190
417
|
} else if (entry.kind === "dir") {
|
|
191
418
|
if (
|
|
419
|
+
!hasExactKeys(entry, ["kind", "manifest"]) ||
|
|
192
420
|
typeof entry.manifest !== "string" ||
|
|
193
421
|
!SNAPSHOT_SHA256.test(entry.manifest)
|
|
194
422
|
) {
|
|
195
423
|
return "invalid snapshot";
|
|
196
424
|
}
|
|
197
425
|
} else if (entry.kind === "absent") {
|
|
198
|
-
if (entry.absent !== true)
|
|
426
|
+
if (!hasExactKeys(entry, ["kind", "absent"]) || entry.absent !== true) {
|
|
427
|
+
return "invalid snapshot";
|
|
428
|
+
}
|
|
199
429
|
} else {
|
|
200
430
|
return "invalid snapshot";
|
|
201
431
|
}
|
|
202
432
|
}
|
|
433
|
+
if ([...new Set(paths)].some((path) => !Object.hasOwn(bounds, path))) {
|
|
434
|
+
return "snapshot does not cover declared paths";
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function validateAgentsContract(env: Record<string, unknown>): string | null {
|
|
440
|
+
if (!(AGENTS_IMPACTS as readonly unknown[]).includes(env.agentsImpact)) {
|
|
441
|
+
return "invalid AGENTS impact";
|
|
442
|
+
}
|
|
443
|
+
if (env.agentsManagedOnly !== true) return "AGENTS must be managed-only";
|
|
444
|
+
if (env.agentsImpact === "none") {
|
|
445
|
+
return env.agentsTarget === undefined
|
|
446
|
+
? null
|
|
447
|
+
: "none AGENTS impact cannot declare a target";
|
|
448
|
+
}
|
|
449
|
+
return isAgentsPath(env.agentsTarget)
|
|
450
|
+
? null
|
|
451
|
+
: "AGENTS impact requires an explicit AGENTS target";
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function validateApprovedDependencies(value: unknown): string | null {
|
|
455
|
+
if (
|
|
456
|
+
!Array.isArray(value) ||
|
|
457
|
+
value.some(
|
|
458
|
+
(dependency) =>
|
|
459
|
+
typeof dependency !== "string" ||
|
|
460
|
+
!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/iu.test(
|
|
461
|
+
dependency,
|
|
462
|
+
),
|
|
463
|
+
) ||
|
|
464
|
+
new Set(value).size !== value.length
|
|
465
|
+
) {
|
|
466
|
+
return "invalid approved dependencies";
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function validateImpactClosure(
|
|
472
|
+
value: unknown,
|
|
473
|
+
declared: Record<string, unknown>,
|
|
474
|
+
snapshot: unknown,
|
|
475
|
+
): string | null {
|
|
476
|
+
if (
|
|
477
|
+
!hasExactKeys(value, [
|
|
478
|
+
"changedSurfaces",
|
|
479
|
+
"searchEvidence",
|
|
480
|
+
"relatedTests",
|
|
481
|
+
"affectedSuite",
|
|
482
|
+
])
|
|
483
|
+
) {
|
|
484
|
+
return "invalid impact closure";
|
|
485
|
+
}
|
|
486
|
+
const closure = value as Record<string, unknown>;
|
|
487
|
+
const surfaces = closure.changedSurfaces;
|
|
488
|
+
const searchEvidence = closure.searchEvidence;
|
|
489
|
+
const relatedTests = closure.relatedTests;
|
|
490
|
+
const affectedSuite = closure.affectedSuite;
|
|
491
|
+
if (
|
|
492
|
+
!Array.isArray(surfaces) ||
|
|
493
|
+
surfaces.length === 0 ||
|
|
494
|
+
surfaces.some(
|
|
495
|
+
(surface) => !(IMPACT_SURFACES as readonly unknown[]).includes(surface),
|
|
496
|
+
) ||
|
|
497
|
+
new Set(surfaces).size !== surfaces.length ||
|
|
498
|
+
(surfaces.includes("none") && surfaces.length !== 1) ||
|
|
499
|
+
!Array.isArray(searchEvidence) ||
|
|
500
|
+
searchEvidence.some(
|
|
501
|
+
(evidence) => typeof evidence !== "string" || evidence.length === 0,
|
|
502
|
+
) ||
|
|
503
|
+
!Array.isArray(relatedTests) ||
|
|
504
|
+
!Array.isArray(affectedSuite) ||
|
|
505
|
+
affectedSuite.some(
|
|
506
|
+
(test) => !isValidRelativePath(test) || !isTestPath(test),
|
|
507
|
+
) ||
|
|
508
|
+
new Set(affectedSuite).size !== affectedSuite.length
|
|
509
|
+
) {
|
|
510
|
+
return "invalid impact closure";
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const scope = new Set([
|
|
514
|
+
...((declared.read as string[]) ?? []),
|
|
515
|
+
...((declared.write as string[]) ?? []),
|
|
516
|
+
]);
|
|
517
|
+
const writes = new Set((declared.write as string[]) ?? []);
|
|
518
|
+
const relatedPaths = new Set<string>();
|
|
519
|
+
for (const item of relatedTests) {
|
|
520
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
521
|
+
return "invalid impact closure related test";
|
|
522
|
+
}
|
|
523
|
+
const related = item as Record<string, unknown>;
|
|
524
|
+
const relatedKeys =
|
|
525
|
+
related.disposition === "regression-task"
|
|
526
|
+
? ["path", "disposition", "evidence", "regressionTaskId"]
|
|
527
|
+
: ["path", "disposition", "evidence"];
|
|
528
|
+
if (
|
|
529
|
+
!hasExactKeys(related, relatedKeys) ||
|
|
530
|
+
!isValidRelativePath(related.path) ||
|
|
531
|
+
!isTestPath(related.path) ||
|
|
532
|
+
relatedPaths.has(related.path) ||
|
|
533
|
+
!scope.has(related.path) ||
|
|
534
|
+
!["current-task", "regression-task", "unaffected"].includes(
|
|
535
|
+
String(related.disposition),
|
|
536
|
+
) ||
|
|
537
|
+
typeof related.evidence !== "string" ||
|
|
538
|
+
related.evidence.length === 0
|
|
539
|
+
) {
|
|
540
|
+
return "invalid impact closure related test";
|
|
541
|
+
}
|
|
542
|
+
relatedPaths.add(related.path);
|
|
543
|
+
if (related.disposition === "current-task" && !writes.has(related.path)) {
|
|
544
|
+
return "current-task related test is outside the write set";
|
|
545
|
+
}
|
|
546
|
+
if (
|
|
547
|
+
related.disposition === "regression-task" &&
|
|
548
|
+
(typeof related.regressionTaskId !== "string" ||
|
|
549
|
+
related.regressionTaskId.length === 0)
|
|
550
|
+
) {
|
|
551
|
+
return "regression task identity is required";
|
|
552
|
+
}
|
|
553
|
+
if (
|
|
554
|
+
related.disposition !== "regression-task" &&
|
|
555
|
+
related.regressionTaskId !== undefined
|
|
556
|
+
) {
|
|
557
|
+
return "unexpected regression task identity";
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (surfaces[0] === "none") return null;
|
|
562
|
+
if (
|
|
563
|
+
searchEvidence.length === 0 ||
|
|
564
|
+
relatedTests.length === 0 ||
|
|
565
|
+
affectedSuite.length === 0 ||
|
|
566
|
+
affectedSuite.some((test) => !relatedPaths.has(test))
|
|
567
|
+
) {
|
|
568
|
+
return "public impact closure is incomplete";
|
|
569
|
+
}
|
|
570
|
+
const bounds = snapshot as Record<string, { kind?: unknown }>;
|
|
571
|
+
if (!affectedSuite.some((test) => bounds?.[test]?.kind === "file")) {
|
|
572
|
+
return "affected suite contains no existing test evidence";
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function isTestPath(path: string): boolean {
|
|
578
|
+
return /^(?:test|tests)\//u.test(path);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function validateAgentsCheckpointRequest(
|
|
582
|
+
value: unknown,
|
|
583
|
+
):
|
|
584
|
+
| { ok: true; value: AgentsCheckpointRequest }
|
|
585
|
+
| { ok: false; reason: string } {
|
|
586
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
587
|
+
return { ok: false, reason: "missing AGENTS checkpoint request" };
|
|
588
|
+
}
|
|
589
|
+
const request = value as Record<string, unknown>;
|
|
590
|
+
if (
|
|
591
|
+
request.stage !== "abel-implement" ||
|
|
592
|
+
typeof request.taskId !== "string" ||
|
|
593
|
+
request.taskId.length === 0 ||
|
|
594
|
+
request.taskId.length > 128 ||
|
|
595
|
+
request.agentsImpact === "none" ||
|
|
596
|
+
!(AGENTS_IMPACTS as readonly unknown[]).includes(request.agentsImpact) ||
|
|
597
|
+
!isAgentsPath(request.agentsTarget) ||
|
|
598
|
+
request.agentsManagedOnly !== true ||
|
|
599
|
+
request.stableCheckpoint !== true ||
|
|
600
|
+
typeof request.diff !== "string" ||
|
|
601
|
+
request.diff.length === 0 ||
|
|
602
|
+
request.diff.length > LIMITS.maxCompleteResultBytes
|
|
603
|
+
) {
|
|
604
|
+
return { ok: false, reason: "invalid AGENTS checkpoint contract" };
|
|
605
|
+
}
|
|
606
|
+
const snapshotReason = validateImplementationSnapshot(request.snapshot, [
|
|
607
|
+
request.agentsTarget,
|
|
608
|
+
] as string[]);
|
|
609
|
+
if (snapshotReason !== null) {
|
|
610
|
+
return { ok: false, reason: snapshotReason };
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
ok: true,
|
|
614
|
+
value: request as unknown as AgentsCheckpointRequest,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function validateAgentsCheckpointAttempt(
|
|
619
|
+
value: unknown,
|
|
620
|
+
):
|
|
621
|
+
| { ok: true; value: AgentsCheckpointAttempt }
|
|
622
|
+
| { ok: false; reason: string } {
|
|
623
|
+
if (
|
|
624
|
+
!hasExactKeys(value, [
|
|
625
|
+
"changeId",
|
|
626
|
+
"taskId",
|
|
627
|
+
"requestId",
|
|
628
|
+
"snapshot",
|
|
629
|
+
"diff",
|
|
630
|
+
]) ||
|
|
631
|
+
!validIdentifier(value.changeId) ||
|
|
632
|
+
!validIdentifier(value.taskId) ||
|
|
633
|
+
!validIdentifier(value.requestId) ||
|
|
634
|
+
typeof value.diff !== "string" ||
|
|
635
|
+
value.diff.length === 0 ||
|
|
636
|
+
value.diff.length > LIMITS.maxCompleteResultBytes ||
|
|
637
|
+
!value.snapshot ||
|
|
638
|
+
typeof value.snapshot !== "object" ||
|
|
639
|
+
Array.isArray(value.snapshot) ||
|
|
640
|
+
Object.keys(value.snapshot).length === 0 ||
|
|
641
|
+
validateImplementationSnapshot(
|
|
642
|
+
value.snapshot,
|
|
643
|
+
Object.keys(value.snapshot as Record<string, unknown>),
|
|
644
|
+
) !== null
|
|
645
|
+
) {
|
|
646
|
+
return { ok: false, reason: "invalid AGENTS checkpoint attempt" };
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
ok: true,
|
|
650
|
+
value: structuredClone(value) as unknown as AgentsCheckpointAttempt,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export function validateImplementApplyOperation(
|
|
655
|
+
value: unknown,
|
|
656
|
+
):
|
|
657
|
+
| { ok: true; value: ImplementApplyOperation }
|
|
658
|
+
| { ok: false; reason: string } {
|
|
659
|
+
if (
|
|
660
|
+
!hasExactKeys(value, ["resultId", "requestId"]) ||
|
|
661
|
+
!validIdentifier(value.resultId) ||
|
|
662
|
+
!validIdentifier(value.requestId)
|
|
663
|
+
) {
|
|
664
|
+
return { ok: false, reason: "invalid Implement apply operation" };
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
ok: true,
|
|
668
|
+
value: { resultId: value.resultId, requestId: value.requestId },
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function validateCandidateRejection(
|
|
673
|
+
value: unknown,
|
|
674
|
+
): value is CandidateRejection {
|
|
675
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
const rejection = value as Record<string, unknown>;
|
|
679
|
+
if (rejection.kind === "approval-boundary") {
|
|
680
|
+
return (
|
|
681
|
+
hasExactKeys(rejection, ["kind", "code"]) &&
|
|
682
|
+
(APPROVAL_BOUNDARY_CODES as readonly unknown[]).includes(rejection.code)
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
if (
|
|
686
|
+
rejection.kind !== "artifact" ||
|
|
687
|
+
rejection.code !== "parent-review-rejected" ||
|
|
688
|
+
!hasExactKeys(rejection, ["kind", "code"], ["evidence"])
|
|
689
|
+
) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
return (
|
|
693
|
+
rejection.evidence === undefined ||
|
|
694
|
+
(Array.isArray(rejection.evidence) &&
|
|
695
|
+
rejection.evidence.length <= 8 &&
|
|
696
|
+
rejection.evidence.every(
|
|
697
|
+
(item) =>
|
|
698
|
+
typeof item === "string" && item.length > 0 && item.length <= 512,
|
|
699
|
+
))
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export function validateImplementDiscardOperation(
|
|
704
|
+
value: unknown,
|
|
705
|
+
):
|
|
706
|
+
| { ok: true; value: ImplementDiscardOperation }
|
|
707
|
+
| { ok: false; reason: string } {
|
|
203
708
|
if (
|
|
204
|
-
[
|
|
205
|
-
|
|
709
|
+
!hasExactKeys(value, ["resultId", "requestId", "rejection"]) ||
|
|
710
|
+
!validIdentifier(value.resultId) ||
|
|
711
|
+
!validIdentifier(value.requestId) ||
|
|
712
|
+
!validateCandidateRejection(value.rejection)
|
|
713
|
+
) {
|
|
714
|
+
return { ok: false, reason: "invalid Implement discard operation" };
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
ok: true,
|
|
718
|
+
value: structuredClone(value) as unknown as ImplementDiscardOperation,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function hasExactKeys(
|
|
723
|
+
value: unknown,
|
|
724
|
+
required: readonly string[],
|
|
725
|
+
optional: readonly string[] = [],
|
|
726
|
+
): value is Record<string, unknown> {
|
|
727
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
728
|
+
const record = value as Record<string, unknown>;
|
|
729
|
+
const allowed = new Set([...required, ...optional]);
|
|
730
|
+
return (
|
|
731
|
+
required.every((key) => Object.hasOwn(record, key)) &&
|
|
732
|
+
Object.keys(record).every((key) => allowed.has(key))
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function validIdentifier(value: unknown): value is string {
|
|
737
|
+
return typeof value === "string" && value.length > 0 && value.length <= 128;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function validatePathSet(value: unknown): value is string[] {
|
|
741
|
+
return (
|
|
742
|
+
Array.isArray(value) &&
|
|
743
|
+
value.every(isValidRelativePath) &&
|
|
744
|
+
new Set(value).size === value.length
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function validateRoots(value: unknown): value is string[] {
|
|
749
|
+
if (!validatePathSet(value) || value.length === 0) return false;
|
|
750
|
+
if (
|
|
751
|
+
value.some(
|
|
752
|
+
(root) => (root !== "." && root.startsWith("./")) || root.endsWith("/"),
|
|
206
753
|
)
|
|
207
754
|
) {
|
|
208
|
-
return
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
return !value.some((root, index) =>
|
|
758
|
+
value.some(
|
|
759
|
+
(other, otherIndex) =>
|
|
760
|
+
index !== otherIndex && (root === "." || other.startsWith(`${root}/`)),
|
|
761
|
+
),
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function isWithinRoots(path: string, roots: string[]): boolean {
|
|
766
|
+
return roots.some(
|
|
767
|
+
(root) => root === "." || path === root || path.startsWith(`${root}/`),
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function validatePhaseBoundary(
|
|
772
|
+
value: unknown,
|
|
773
|
+
phase: ImplementationPhase,
|
|
774
|
+
): value is PhaseBoundary {
|
|
775
|
+
if (
|
|
776
|
+
!hasExactKeys(
|
|
777
|
+
value,
|
|
778
|
+
["read", "write", "verification"],
|
|
779
|
+
["verificationLock"],
|
|
780
|
+
)
|
|
781
|
+
) {
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
if (
|
|
785
|
+
!validatePathSet(value.read) ||
|
|
786
|
+
!validatePathSet(value.write) ||
|
|
787
|
+
value.write.some(isAgentsPath) ||
|
|
788
|
+
(value.verificationLock !== undefined &&
|
|
789
|
+
!isValidRelativePath(value.verificationLock))
|
|
790
|
+
) {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
const verificationKeys =
|
|
794
|
+
phase === "red"
|
|
795
|
+
? ["id", "argv", "classification", "expectedFailure", "minTests"]
|
|
796
|
+
: ["id", "argv", "classification", "minTests"];
|
|
797
|
+
if (!hasExactKeys(value.verification, verificationKeys)) return false;
|
|
798
|
+
const verification = validateVerificationContract(value.verification);
|
|
799
|
+
if (!verification.ok) return false;
|
|
800
|
+
const classification = {
|
|
801
|
+
red: "expected-red",
|
|
802
|
+
green: "expected-green",
|
|
803
|
+
refactor: "expected-refactor",
|
|
804
|
+
} as const;
|
|
805
|
+
return verification.value.classification === classification[phase];
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function validateTaskBoundary(
|
|
809
|
+
value: unknown,
|
|
810
|
+
snapshot: unknown,
|
|
811
|
+
): value is TaskBoundary {
|
|
812
|
+
if (
|
|
813
|
+
!hasExactKeys(value, [
|
|
814
|
+
"changeId",
|
|
815
|
+
"taskId",
|
|
816
|
+
"objective",
|
|
817
|
+
"context",
|
|
818
|
+
"roots",
|
|
819
|
+
"phases",
|
|
820
|
+
"scheduling",
|
|
821
|
+
"agents",
|
|
822
|
+
"approvedDependencies",
|
|
823
|
+
"impactClosure",
|
|
824
|
+
]) ||
|
|
825
|
+
!validIdentifier(value.changeId) ||
|
|
826
|
+
!validIdentifier(value.taskId) ||
|
|
827
|
+
typeof value.objective !== "string" ||
|
|
828
|
+
value.objective.length === 0 ||
|
|
829
|
+
value.objective.length > 4096 ||
|
|
830
|
+
!hasExactKeys(value.context, ["agents", "contract"]) ||
|
|
831
|
+
typeof value.context.agents !== "string" ||
|
|
832
|
+
typeof value.context.contract !== "string" ||
|
|
833
|
+
!validateRoots(value.roots) ||
|
|
834
|
+
!hasExactKeys(value.phases, ["red", "green"], ["refactor"]) ||
|
|
835
|
+
!validatePhaseBoundary(value.phases.red, "red") ||
|
|
836
|
+
!validatePhaseBoundary(value.phases.green, "green") ||
|
|
837
|
+
(value.phases.refactor !== undefined &&
|
|
838
|
+
!validatePhaseBoundary(value.phases.refactor, "refactor")) ||
|
|
839
|
+
!hasExactKeys(value.scheduling, ["conflicts", "resources"]) ||
|
|
840
|
+
!validatePathSet(value.scheduling.conflicts) ||
|
|
841
|
+
!validatePathSet(value.scheduling.resources) ||
|
|
842
|
+
!hasExactKeys(value.agents, ["impact", "managedOnly"], ["target"]) ||
|
|
843
|
+
!(AGENTS_IMPACTS as readonly unknown[]).includes(value.agents.impact) ||
|
|
844
|
+
value.agents.managedOnly !== true
|
|
845
|
+
) {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
if (
|
|
849
|
+
value.agents.impact === "none"
|
|
850
|
+
? value.agents.target !== undefined
|
|
851
|
+
: !isAgentsPath(value.agents.target)
|
|
852
|
+
) {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
if (validateApprovedDependencies(value.approvedDependencies) !== null) {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
const phases = [
|
|
859
|
+
value.phases.red,
|
|
860
|
+
value.phases.green,
|
|
861
|
+
...(value.phases.refactor ? [value.phases.refactor] : []),
|
|
862
|
+
];
|
|
863
|
+
const roots = value.roots as string[];
|
|
864
|
+
if (
|
|
865
|
+
phases.some((phase) =>
|
|
866
|
+
[...phase.read, ...phase.write].some(
|
|
867
|
+
(path) => !isWithinRoots(path, roots),
|
|
868
|
+
),
|
|
869
|
+
)
|
|
870
|
+
) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
const declared = {
|
|
874
|
+
read: [...new Set(phases.flatMap((entry) => entry.read))],
|
|
875
|
+
write: [...new Set(phases.flatMap((entry) => entry.write))],
|
|
876
|
+
};
|
|
877
|
+
return (
|
|
878
|
+
validateImpactClosure(value.impactClosure, declared, snapshot) === null
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function validatePhaseAttemptShape(value: unknown): value is PhaseAttempt {
|
|
883
|
+
if (
|
|
884
|
+
!hasExactKeys(value, [
|
|
885
|
+
"changeId",
|
|
886
|
+
"taskId",
|
|
887
|
+
"requestId",
|
|
888
|
+
"phase",
|
|
889
|
+
"snapshot",
|
|
890
|
+
]) ||
|
|
891
|
+
!validIdentifier(value.changeId) ||
|
|
892
|
+
!validIdentifier(value.taskId) ||
|
|
893
|
+
!validIdentifier(value.requestId) ||
|
|
894
|
+
!["red", "green", "refactor"].includes(String(value.phase))
|
|
895
|
+
) {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
if (
|
|
899
|
+
!value.snapshot ||
|
|
900
|
+
typeof value.snapshot !== "object" ||
|
|
901
|
+
Array.isArray(value.snapshot)
|
|
902
|
+
) {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
return (
|
|
906
|
+
validateImplementationSnapshot(
|
|
907
|
+
value.snapshot,
|
|
908
|
+
Object.keys(value.snapshot as Record<string, unknown>),
|
|
909
|
+
) === null
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
export function validatePhaseAttemptAgainstBoundary(
|
|
914
|
+
boundary: TaskBoundary,
|
|
915
|
+
attempt: PhaseAttempt,
|
|
916
|
+
): string | null {
|
|
917
|
+
if (
|
|
918
|
+
attempt.changeId !== boundary.changeId ||
|
|
919
|
+
attempt.taskId !== boundary.taskId
|
|
920
|
+
) {
|
|
921
|
+
return "task attempt identity mismatch";
|
|
922
|
+
}
|
|
923
|
+
const phase = boundary.phases[attempt.phase];
|
|
924
|
+
if (!phase) return "task attempt phase is not declared";
|
|
925
|
+
const snapshotReason = validateImplementationSnapshot(attempt.snapshot, [
|
|
926
|
+
...phase.read,
|
|
927
|
+
...phase.write,
|
|
928
|
+
]);
|
|
929
|
+
if (snapshotReason !== null) return snapshotReason;
|
|
930
|
+
const bounds = attempt.snapshot as Record<string, { kind?: unknown }>;
|
|
931
|
+
if (
|
|
932
|
+
phase.write.some(
|
|
933
|
+
(path) =>
|
|
934
|
+
bounds[path]?.kind !== "file" && bounds[path]?.kind !== "absent",
|
|
935
|
+
)
|
|
936
|
+
) {
|
|
937
|
+
return "write snapshot must bind a regular file or absent path";
|
|
209
938
|
}
|
|
210
939
|
return null;
|
|
211
940
|
}
|
|
212
941
|
|
|
942
|
+
function validateImplementRunRequest(
|
|
943
|
+
value: unknown,
|
|
944
|
+
): { ok: true; value: ImplementRunRequest } | { ok: false; reason: string } {
|
|
945
|
+
const serialized = JSON.stringify(value);
|
|
946
|
+
if (serialized.length > LIMITS.maxEnvelopeBytes) {
|
|
947
|
+
return {
|
|
948
|
+
ok: false,
|
|
949
|
+
reason: `request envelope exceeds ${LIMITS.maxEnvelopeBytes / 1024} KiB`,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
953
|
+
return { ok: false, reason: "missing request envelope" };
|
|
954
|
+
}
|
|
955
|
+
const request = value as Record<string, unknown>;
|
|
956
|
+
if (request.kind === "open-task") {
|
|
957
|
+
if (
|
|
958
|
+
request.attempt &&
|
|
959
|
+
typeof request.attempt === "object" &&
|
|
960
|
+
!Array.isArray(request.attempt) &&
|
|
961
|
+
!Object.hasOwn(request.attempt, "snapshot")
|
|
962
|
+
) {
|
|
963
|
+
return { ok: false, reason: "invalid snapshot" };
|
|
964
|
+
}
|
|
965
|
+
if (
|
|
966
|
+
!hasExactKeys(request, ["stage", "kind", "boundary", "attempt"]) ||
|
|
967
|
+
!validatePhaseAttemptShape(request.attempt) ||
|
|
968
|
+
!validateTaskBoundary(request.boundary, request.attempt.snapshot)
|
|
969
|
+
) {
|
|
970
|
+
return { ok: false, reason: "invalid task open contract" };
|
|
971
|
+
}
|
|
972
|
+
const boundary = request.boundary as TaskBoundary;
|
|
973
|
+
const attempt = request.attempt as PhaseAttempt;
|
|
974
|
+
const attemptReason = validatePhaseAttemptAgainstBoundary(
|
|
975
|
+
boundary,
|
|
976
|
+
attempt,
|
|
977
|
+
);
|
|
978
|
+
if (attempt.phase !== "red" || attemptReason !== null) {
|
|
979
|
+
return {
|
|
980
|
+
ok: false,
|
|
981
|
+
reason:
|
|
982
|
+
attempt.phase !== "red"
|
|
983
|
+
? "task open must begin with Red"
|
|
984
|
+
: (attemptReason ?? "invalid phase attempt"),
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
ok: true,
|
|
989
|
+
value: structuredClone(request) as ImplementRunRequest,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
if (request.kind === "phase-attempt") {
|
|
993
|
+
if (
|
|
994
|
+
!hasExactKeys(request, ["stage", "kind", "attempt"]) ||
|
|
995
|
+
!validatePhaseAttemptShape(request.attempt)
|
|
996
|
+
) {
|
|
997
|
+
return { ok: false, reason: "invalid phase attempt contract" };
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
ok: true,
|
|
1001
|
+
value: structuredClone(request) as ImplementRunRequest,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
return { ok: false, reason: "invalid Implement run kind" };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
213
1007
|
export function validateRequestEnvelope(
|
|
214
1008
|
value: unknown,
|
|
1009
|
+
): { ok: true; value: RunRequest } | { ok: false; reason: string } {
|
|
1010
|
+
if (
|
|
1011
|
+
value &&
|
|
1012
|
+
typeof value === "object" &&
|
|
1013
|
+
!Array.isArray(value) &&
|
|
1014
|
+
(value as Record<string, unknown>).stage === "abel-implement"
|
|
1015
|
+
) {
|
|
1016
|
+
return validateImplementRunRequest(value);
|
|
1017
|
+
}
|
|
1018
|
+
return validateLegacyRequestEnvelope(value);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function validateLegacyRequestEnvelope(
|
|
1022
|
+
value: unknown,
|
|
215
1023
|
): { ok: true; value: RequestEnvelope } | { ok: false; reason: string } {
|
|
216
1024
|
if (value === null || typeof value !== "object") {
|
|
217
1025
|
return { ok: false, reason: "missing request envelope" };
|
|
@@ -310,6 +1118,9 @@ export function validateRequestEnvelope(
|
|
|
310
1118
|
return { ok: false, reason: `invalid declared ${key} set` };
|
|
311
1119
|
}
|
|
312
1120
|
}
|
|
1121
|
+
if ((decl.write as unknown[]).some(isAgentsPath)) {
|
|
1122
|
+
return { ok: false, reason: "subagents cannot declare AGENTS writes" };
|
|
1123
|
+
}
|
|
313
1124
|
if (
|
|
314
1125
|
decl.verificationLock !== undefined &&
|
|
315
1126
|
!isValidRelativePath(decl.verificationLock)
|
|
@@ -329,6 +1140,20 @@ export function validateRequestEnvelope(
|
|
|
329
1140
|
}
|
|
330
1141
|
}
|
|
331
1142
|
if (env.stage === "abel-implement" && env.output === "diff") {
|
|
1143
|
+
const agentsReason = validateAgentsContract(env);
|
|
1144
|
+
if (agentsReason !== null) return { ok: false, reason: agentsReason };
|
|
1145
|
+
const dependenciesReason = validateApprovedDependencies(
|
|
1146
|
+
env.approvedDependencies,
|
|
1147
|
+
);
|
|
1148
|
+
if (dependenciesReason !== null) {
|
|
1149
|
+
return { ok: false, reason: dependenciesReason };
|
|
1150
|
+
}
|
|
1151
|
+
const closureReason = validateImpactClosure(
|
|
1152
|
+
env.impactClosure,
|
|
1153
|
+
decl,
|
|
1154
|
+
env.snapshot,
|
|
1155
|
+
);
|
|
1156
|
+
if (closureReason !== null) return { ok: false, reason: closureReason };
|
|
332
1157
|
if (env.verification === undefined) {
|
|
333
1158
|
return { ok: false, reason: "missing required field: verification" };
|
|
334
1159
|
}
|
|
@@ -592,37 +1417,154 @@ export interface DiffResult {
|
|
|
592
1417
|
diff: string;
|
|
593
1418
|
expectedVerification: string;
|
|
594
1419
|
risks: string[];
|
|
595
|
-
nextStep: string;
|
|
596
1420
|
contractCompliant: boolean;
|
|
597
1421
|
}
|
|
598
1422
|
|
|
1423
|
+
export interface ApplyResult {
|
|
1424
|
+
targets: string[];
|
|
1425
|
+
checkExitCode: 0;
|
|
1426
|
+
applyExitCode: 0;
|
|
1427
|
+
sequence?: number;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
export interface AgentsCheckpointResult {
|
|
1431
|
+
target: string;
|
|
1432
|
+
agentsImpact: Exclude<TaskBoundary["agents"]["impact"], "none">;
|
|
1433
|
+
checkExitCode: 0;
|
|
1434
|
+
applyExitCode: 0;
|
|
1435
|
+
sequence?: number;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
export type ImplementOutcome =
|
|
1439
|
+
| {
|
|
1440
|
+
kind: "deferred";
|
|
1441
|
+
requestId: string;
|
|
1442
|
+
taskId: string;
|
|
1443
|
+
reason: "task-conflict";
|
|
1444
|
+
}
|
|
1445
|
+
| {
|
|
1446
|
+
kind: "candidate";
|
|
1447
|
+
requestId: string;
|
|
1448
|
+
taskId: string;
|
|
1449
|
+
phase: ImplementationPhase;
|
|
1450
|
+
resultId: string;
|
|
1451
|
+
result: DiffResult;
|
|
1452
|
+
}
|
|
1453
|
+
| {
|
|
1454
|
+
kind: "applied";
|
|
1455
|
+
requestId: string;
|
|
1456
|
+
taskId: string;
|
|
1457
|
+
phase: "red" | "green";
|
|
1458
|
+
readyPhase: "green" | "refactor";
|
|
1459
|
+
result: ApplyResult;
|
|
1460
|
+
}
|
|
1461
|
+
| {
|
|
1462
|
+
kind: "checkpoint-required";
|
|
1463
|
+
requestId: string;
|
|
1464
|
+
taskId: string;
|
|
1465
|
+
finalPhase: "green" | "refactor";
|
|
1466
|
+
result: ApplyResult;
|
|
1467
|
+
}
|
|
1468
|
+
| {
|
|
1469
|
+
kind: "retry";
|
|
1470
|
+
requestId: string;
|
|
1471
|
+
taskId: string;
|
|
1472
|
+
scope: "worker" | "checkpoint";
|
|
1473
|
+
phase: ImplementationPhase;
|
|
1474
|
+
cause: "artifact" | "stale";
|
|
1475
|
+
remainingAttempts: 1;
|
|
1476
|
+
}
|
|
1477
|
+
| {
|
|
1478
|
+
kind: "completed";
|
|
1479
|
+
requestId: string;
|
|
1480
|
+
taskId: string;
|
|
1481
|
+
finalPhase: "green" | "refactor";
|
|
1482
|
+
result?: ApplyResult | AgentsCheckpointResult;
|
|
1483
|
+
}
|
|
1484
|
+
| {
|
|
1485
|
+
kind: "blocked";
|
|
1486
|
+
requestId: string;
|
|
1487
|
+
taskId: string;
|
|
1488
|
+
phase: ImplementationPhase;
|
|
1489
|
+
failure: TaskFailure;
|
|
1490
|
+
}
|
|
1491
|
+
| {
|
|
1492
|
+
kind: "cancelled";
|
|
1493
|
+
requestId: string;
|
|
1494
|
+
taskId: string;
|
|
1495
|
+
phase: ImplementationPhase;
|
|
1496
|
+
};
|
|
1497
|
+
|
|
599
1498
|
export function validateEvidenceResult(value: unknown): {
|
|
600
1499
|
ok: boolean;
|
|
601
1500
|
reason?: string;
|
|
1501
|
+
failure?: CandidateFailure;
|
|
602
1502
|
} {
|
|
603
1503
|
if (value === null || typeof value !== "object")
|
|
604
1504
|
return { ok: false, reason: "missing result" };
|
|
605
1505
|
const r = value as Record<string, unknown>;
|
|
606
1506
|
if (r.kind !== "evidence") return { ok: false, reason: "wrong result kind" };
|
|
607
|
-
|
|
1507
|
+
const fields = [
|
|
608
1508
|
"id",
|
|
609
1509
|
"role",
|
|
1510
|
+
"kind",
|
|
610
1511
|
"conclusions",
|
|
611
1512
|
"citations",
|
|
1513
|
+
"constraints",
|
|
1514
|
+
"dependencies",
|
|
1515
|
+
"risks",
|
|
612
1516
|
"blockingQuestions",
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
1517
|
+
"hints",
|
|
1518
|
+
] as const;
|
|
1519
|
+
if (!hasExactKeys(r, fields)) {
|
|
1520
|
+
return { ok: false, reason: "invalid evidence result fields" };
|
|
616
1521
|
}
|
|
617
|
-
if (
|
|
1522
|
+
if (!validIdentifier(r.id) || typeof r.role !== "string")
|
|
618
1523
|
return { ok: false, reason: "invalid identity" };
|
|
619
1524
|
if (!(ROLES as readonly string[]).includes(r.role))
|
|
620
1525
|
return { ok: false, reason: "unknown role" };
|
|
1526
|
+
for (const field of [
|
|
1527
|
+
"conclusions",
|
|
1528
|
+
"constraints",
|
|
1529
|
+
"dependencies",
|
|
1530
|
+
"risks",
|
|
1531
|
+
"blockingQuestions",
|
|
1532
|
+
] as const) {
|
|
1533
|
+
if (
|
|
1534
|
+
!Array.isArray(r[field]) ||
|
|
1535
|
+
!r[field].every((entry) => typeof entry === "string")
|
|
1536
|
+
) {
|
|
1537
|
+
return { ok: false, reason: `invalid evidence field: ${field}` };
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
if (
|
|
1541
|
+
!Array.isArray(r.citations) ||
|
|
1542
|
+
r.citations.some(
|
|
1543
|
+
(citation) =>
|
|
1544
|
+
!hasExactKeys(citation, ["path", "lines"]) ||
|
|
1545
|
+
!isValidRelativePath(citation.path) ||
|
|
1546
|
+
typeof citation.lines !== "string",
|
|
1547
|
+
)
|
|
1548
|
+
) {
|
|
1549
|
+
return { ok: false, reason: "invalid evidence citations" };
|
|
1550
|
+
}
|
|
1551
|
+
if (
|
|
1552
|
+
!hasExactKeys(r.hints, ["writeSet", "verification", "agentsImpact"]) ||
|
|
1553
|
+
!validatePathSet(r.hints.writeSet) ||
|
|
1554
|
+
typeof r.hints.verification !== "string" ||
|
|
1555
|
+
!(AGENTS_IMPACTS as readonly unknown[]).includes(r.hints.agentsImpact)
|
|
1556
|
+
) {
|
|
1557
|
+
return { ok: false, reason: "invalid evidence hints" };
|
|
1558
|
+
}
|
|
621
1559
|
const serialized = JSON.stringify(r);
|
|
622
|
-
if (serialized
|
|
1560
|
+
if (Buffer.byteLength(serialized, "utf8") > LIMITS.maxCompleteResultBytes) {
|
|
623
1561
|
return {
|
|
624
1562
|
ok: false,
|
|
625
1563
|
reason: `result exceeds ${LIMITS.maxCompleteResultBytes} bytes`,
|
|
1564
|
+
failure: {
|
|
1565
|
+
kind: "result-limit",
|
|
1566
|
+
limitBytes: LIMITS.maxCompleteResultBytes,
|
|
1567
|
+
},
|
|
626
1568
|
};
|
|
627
1569
|
}
|
|
628
1570
|
return { ok: true };
|
|
@@ -632,23 +1574,28 @@ export function validateDiffResult(value: unknown): {
|
|
|
632
1574
|
ok: boolean;
|
|
633
1575
|
reason?: string;
|
|
634
1576
|
paths?: string[];
|
|
1577
|
+
failure?: CandidateFailure;
|
|
635
1578
|
} {
|
|
636
1579
|
if (value === null || typeof value !== "object")
|
|
637
1580
|
return { ok: false, reason: "missing result" };
|
|
638
1581
|
const r = value as Record<string, unknown>;
|
|
639
1582
|
if (r.kind !== "diff") return { ok: false, reason: "wrong result kind" };
|
|
640
|
-
|
|
1583
|
+
const fields = [
|
|
641
1584
|
"id",
|
|
642
1585
|
"role",
|
|
1586
|
+
"kind",
|
|
643
1587
|
"taskId",
|
|
644
1588
|
"phase",
|
|
645
1589
|
"summary",
|
|
646
1590
|
"diff",
|
|
647
1591
|
"expectedVerification",
|
|
648
1592
|
"risks",
|
|
649
|
-
"nextStep",
|
|
650
1593
|
"contractCompliant",
|
|
651
|
-
]
|
|
1594
|
+
] as const;
|
|
1595
|
+
if (!hasExactKeys(r, fields)) {
|
|
1596
|
+
return { ok: false, reason: "invalid diff result fields" };
|
|
1597
|
+
}
|
|
1598
|
+
for (const field of fields) {
|
|
652
1599
|
if (r[field] === undefined)
|
|
653
1600
|
return { ok: false, reason: `missing field: ${field}` };
|
|
654
1601
|
}
|
|
@@ -675,9 +1622,7 @@ export function validateDiffResult(value: unknown): {
|
|
|
675
1622
|
typeof r.summary !== "string" ||
|
|
676
1623
|
r.summary.length === 0 ||
|
|
677
1624
|
typeof r.expectedVerification !== "string" ||
|
|
678
|
-
r.expectedVerification.length === 0
|
|
679
|
-
typeof r.nextStep !== "string" ||
|
|
680
|
-
r.nextStep.length === 0
|
|
1625
|
+
r.expectedVerification.length === 0
|
|
681
1626
|
) {
|
|
682
1627
|
return { ok: false, reason: "invalid diff result text" };
|
|
683
1628
|
}
|
|
@@ -696,10 +1641,14 @@ export function validateDiffResult(value: unknown): {
|
|
|
696
1641
|
return { ok: false, reason: `invalid diff: ${(err as Error).message}` };
|
|
697
1642
|
}
|
|
698
1643
|
const serialized = JSON.stringify(r);
|
|
699
|
-
if (serialized
|
|
1644
|
+
if (Buffer.byteLength(serialized, "utf8") > LIMITS.maxCompleteResultBytes) {
|
|
700
1645
|
return {
|
|
701
1646
|
ok: false,
|
|
702
1647
|
reason: `result exceeds ${LIMITS.maxCompleteResultBytes} bytes`,
|
|
1648
|
+
failure: {
|
|
1649
|
+
kind: "result-limit",
|
|
1650
|
+
limitBytes: LIMITS.maxCompleteResultBytes,
|
|
1651
|
+
},
|
|
703
1652
|
};
|
|
704
1653
|
}
|
|
705
1654
|
return { ok: true, paths };
|