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