@abelxiaoxing/cadence 1.0.1 → 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 +53 -13
- package/skills/abel-workflow/SKILL.md +60 -22
- package/src/candidate-preflight.ts +1468 -0
- package/src/child-session.ts +136 -30
- package/src/contracts.ts +1343 -63
- package/src/file-snapshot.ts +62 -8
- package/src/index.ts +99 -28
- package/src/parent-payload-bridge.ts +442 -0
- package/src/parent-provider.ts +166 -21
- package/src/patch.ts +331 -33
- package/src/result-store.ts +282 -7
- package/src/runtime.ts +1584 -154
- package/src/scheduler.ts +60 -16
- package/src/scoped-tools.ts +27 -6
- package/src/subagent-activity.ts +56 -25
- package/src/submit-tool.ts +15 -3
- package/src/worker.ts +114 -85
package/src/contracts.ts
CHANGED
|
@@ -21,6 +21,23 @@ export const PHASES = [
|
|
|
21
21
|
"review",
|
|
22
22
|
] as const;
|
|
23
23
|
|
|
24
|
+
export const AGENTS_IMPACTS = [
|
|
25
|
+
"none",
|
|
26
|
+
"update-existing",
|
|
27
|
+
"create-index",
|
|
28
|
+
"remove-index",
|
|
29
|
+
] as const;
|
|
30
|
+
export type AgentsImpact = (typeof AGENTS_IMPACTS)[number];
|
|
31
|
+
|
|
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];
|
|
40
|
+
|
|
24
41
|
export const LIMITS = {
|
|
25
42
|
maxActiveChildSessions: 4,
|
|
26
43
|
maxRequestsPerBatch: 8,
|
|
@@ -29,11 +46,214 @@ export const LIMITS = {
|
|
|
29
46
|
maxCompleteResultBytes: 64 * 1024,
|
|
30
47
|
} as const;
|
|
31
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
|
+
|
|
32
179
|
const NONCANONICAL = /(^|\/)\.\.(\/|$)|(^|\/)\/|^\//;
|
|
180
|
+
const SNAPSHOT_SHA256 = /^[a-f0-9]{64}$/;
|
|
181
|
+
|
|
182
|
+
export interface VerificationContract {
|
|
183
|
+
id: string;
|
|
184
|
+
argv: string[];
|
|
185
|
+
classification: "expected-red" | "expected-green" | "expected-refactor";
|
|
186
|
+
expectedFailure?: string;
|
|
187
|
+
minTests: number;
|
|
188
|
+
}
|
|
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
|
+
};
|
|
33
252
|
|
|
34
253
|
export interface RequestEnvelope {
|
|
35
254
|
stage: string;
|
|
36
255
|
role: string;
|
|
256
|
+
taskId?: string;
|
|
37
257
|
id: string;
|
|
38
258
|
phase: string;
|
|
39
259
|
objective: string;
|
|
@@ -47,9 +267,45 @@ export interface RequestEnvelope {
|
|
|
47
267
|
verificationLock?: string;
|
|
48
268
|
};
|
|
49
269
|
output: string;
|
|
270
|
+
agentsImpact?: AgentsImpact;
|
|
271
|
+
agentsTarget?: string;
|
|
272
|
+
agentsManagedOnly?: true;
|
|
273
|
+
approvedDependencies?: string[];
|
|
274
|
+
impactClosure?: ImpactClosureContract;
|
|
275
|
+
verification?: VerificationContract;
|
|
50
276
|
snapshot?: unknown;
|
|
51
277
|
}
|
|
52
278
|
|
|
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;
|
|
294
|
+
taskId: string;
|
|
295
|
+
requestId: string;
|
|
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;
|
|
307
|
+
}
|
|
308
|
+
|
|
53
309
|
export function isValidRelativePath(p: unknown): p is string {
|
|
54
310
|
return (
|
|
55
311
|
typeof p === "string" &&
|
|
@@ -57,14 +313,713 @@ export function isValidRelativePath(p: unknown): p is string {
|
|
|
57
313
|
p.length <= 512 &&
|
|
58
314
|
!NONCANONICAL.test(p) &&
|
|
59
315
|
!p.startsWith("/") &&
|
|
316
|
+
(p === "." ||
|
|
317
|
+
(!p.startsWith("./") &&
|
|
318
|
+
!p.endsWith("/") &&
|
|
319
|
+
!p.split("/").includes("."))) &&
|
|
60
320
|
!p.includes("..") &&
|
|
61
321
|
!p.includes("\\") &&
|
|
62
322
|
!p.includes("\u0000")
|
|
63
323
|
);
|
|
64
324
|
}
|
|
65
325
|
|
|
326
|
+
export function isAgentsPath(p: unknown): p is string {
|
|
327
|
+
return isValidRelativePath(p) && /(?:^|\/)AGENTS\.md$/u.test(p);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function validateVerificationContract(
|
|
331
|
+
value: unknown,
|
|
332
|
+
): { ok: true; value: VerificationContract } | { ok: false; reason: string } {
|
|
333
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
334
|
+
return { ok: false, reason: "invalid verification contract" };
|
|
335
|
+
const contract = value as Record<string, unknown>;
|
|
336
|
+
if (
|
|
337
|
+
typeof contract.id !== "string" ||
|
|
338
|
+
contract.id.length === 0 ||
|
|
339
|
+
contract.id.length > 128 ||
|
|
340
|
+
!Array.isArray(contract.argv) ||
|
|
341
|
+
contract.argv.some(
|
|
342
|
+
(part) =>
|
|
343
|
+
typeof part !== "string" ||
|
|
344
|
+
part.length === 0 ||
|
|
345
|
+
/[;&|`$<>\n\r]/u.test(part) ||
|
|
346
|
+
part.includes("\0"),
|
|
347
|
+
) ||
|
|
348
|
+
!["expected-red", "expected-green", "expected-refactor"].includes(
|
|
349
|
+
String(contract.classification),
|
|
350
|
+
) ||
|
|
351
|
+
!Number.isSafeInteger(contract.minTests) ||
|
|
352
|
+
(contract.minTests as number) < 1
|
|
353
|
+
) {
|
|
354
|
+
return { ok: false, reason: "invalid verification contract" };
|
|
355
|
+
}
|
|
356
|
+
const argv = contract.argv as string[];
|
|
357
|
+
const check =
|
|
358
|
+
argv.length === 3 &&
|
|
359
|
+
argv[0] === "bun" &&
|
|
360
|
+
argv[1] === "run" &&
|
|
361
|
+
argv[2] === "check";
|
|
362
|
+
const target =
|
|
363
|
+
argv.length >= 4 &&
|
|
364
|
+
argv[0] === "bun" &&
|
|
365
|
+
argv[1] === "run" &&
|
|
366
|
+
argv[2] === "test:target" &&
|
|
367
|
+
argv
|
|
368
|
+
.slice(3)
|
|
369
|
+
.every((part) => part.startsWith("test/") && isValidRelativePath(part));
|
|
370
|
+
if (!check && !target)
|
|
371
|
+
return { ok: false, reason: "verification argv is not approved" };
|
|
372
|
+
if (
|
|
373
|
+
contract.classification === "expected-red"
|
|
374
|
+
? typeof contract.expectedFailure !== "string" ||
|
|
375
|
+
contract.expectedFailure.length === 0
|
|
376
|
+
: contract.expectedFailure !== undefined
|
|
377
|
+
) {
|
|
378
|
+
return { ok: false, reason: "invalid expected failure identity" };
|
|
379
|
+
}
|
|
380
|
+
return { ok: true, value: contract as unknown as VerificationContract };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function validateImplementationSnapshot(
|
|
384
|
+
snapshot: unknown,
|
|
385
|
+
paths: string[],
|
|
386
|
+
): string | null {
|
|
387
|
+
if (
|
|
388
|
+
snapshot === undefined ||
|
|
389
|
+
snapshot === null ||
|
|
390
|
+
typeof snapshot !== "object" ||
|
|
391
|
+
Array.isArray(snapshot)
|
|
392
|
+
) {
|
|
393
|
+
return "invalid snapshot";
|
|
394
|
+
}
|
|
395
|
+
const bounds = snapshot as Record<string, unknown>;
|
|
396
|
+
for (const [path, value] of Object.entries(bounds)) {
|
|
397
|
+
if (
|
|
398
|
+
!isValidRelativePath(path) ||
|
|
399
|
+
value === null ||
|
|
400
|
+
typeof value !== "object" ||
|
|
401
|
+
Array.isArray(value)
|
|
402
|
+
) {
|
|
403
|
+
return "invalid snapshot";
|
|
404
|
+
}
|
|
405
|
+
const entry = value as Record<string, unknown>;
|
|
406
|
+
if (entry.kind === "file") {
|
|
407
|
+
if (
|
|
408
|
+
!hasExactKeys(entry, ["kind", "sha256", "bytes"]) ||
|
|
409
|
+
typeof entry.sha256 !== "string" ||
|
|
410
|
+
!SNAPSHOT_SHA256.test(entry.sha256) ||
|
|
411
|
+
typeof entry.bytes !== "number" ||
|
|
412
|
+
!Number.isSafeInteger(entry.bytes) ||
|
|
413
|
+
entry.bytes < 0
|
|
414
|
+
) {
|
|
415
|
+
return "invalid snapshot";
|
|
416
|
+
}
|
|
417
|
+
} else if (entry.kind === "dir") {
|
|
418
|
+
if (
|
|
419
|
+
!hasExactKeys(entry, ["kind", "manifest"]) ||
|
|
420
|
+
typeof entry.manifest !== "string" ||
|
|
421
|
+
!SNAPSHOT_SHA256.test(entry.manifest)
|
|
422
|
+
) {
|
|
423
|
+
return "invalid snapshot";
|
|
424
|
+
}
|
|
425
|
+
} else if (entry.kind === "absent") {
|
|
426
|
+
if (!hasExactKeys(entry, ["kind", "absent"]) || entry.absent !== true) {
|
|
427
|
+
return "invalid snapshot";
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
return "invalid snapshot";
|
|
431
|
+
}
|
|
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 } {
|
|
708
|
+
if (
|
|
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("/"),
|
|
753
|
+
)
|
|
754
|
+
) {
|
|
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";
|
|
938
|
+
}
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
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
|
+
|
|
66
1007
|
export function validateRequestEnvelope(
|
|
67
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,
|
|
68
1023
|
): { ok: true; value: RequestEnvelope } | { ok: false; reason: string } {
|
|
69
1024
|
if (value === null || typeof value !== "object") {
|
|
70
1025
|
return { ok: false, reason: "missing request envelope" };
|
|
@@ -98,6 +1053,23 @@ export function validateRequestEnvelope(
|
|
|
98
1053
|
) {
|
|
99
1054
|
return { ok: false, reason: "invalid request id" };
|
|
100
1055
|
}
|
|
1056
|
+
if (
|
|
1057
|
+
env.taskId !== undefined &&
|
|
1058
|
+
(typeof env.taskId !== "string" ||
|
|
1059
|
+
env.taskId.length === 0 ||
|
|
1060
|
+
env.taskId.length > 128)
|
|
1061
|
+
) {
|
|
1062
|
+
return { ok: false, reason: "invalid task id" };
|
|
1063
|
+
}
|
|
1064
|
+
// Implementation phases use a phase-local request id and carry their stable
|
|
1065
|
+
// task identity explicitly. Other packets keep id as task id.
|
|
1066
|
+
if (
|
|
1067
|
+
env.stage === "abel-implement" &&
|
|
1068
|
+
env.output === "diff" &&
|
|
1069
|
+
env.taskId === undefined
|
|
1070
|
+
) {
|
|
1071
|
+
return { ok: false, reason: "missing required field: taskId" };
|
|
1072
|
+
}
|
|
101
1073
|
if (
|
|
102
1074
|
typeof env.phase !== "string" ||
|
|
103
1075
|
!(PHASES as readonly string[]).includes(env.phase)
|
|
@@ -146,6 +1118,9 @@ export function validateRequestEnvelope(
|
|
|
146
1118
|
return { ok: false, reason: `invalid declared ${key} set` };
|
|
147
1119
|
}
|
|
148
1120
|
}
|
|
1121
|
+
if ((decl.write as unknown[]).some(isAgentsPath)) {
|
|
1122
|
+
return { ok: false, reason: "subagents cannot declare AGENTS writes" };
|
|
1123
|
+
}
|
|
149
1124
|
if (
|
|
150
1125
|
decl.verificationLock !== undefined &&
|
|
151
1126
|
!isValidRelativePath(decl.verificationLock)
|
|
@@ -155,6 +1130,59 @@ export function validateRequestEnvelope(
|
|
|
155
1130
|
if (!(OUTPUT_KINDS as readonly string[]).includes(env.output as string)) {
|
|
156
1131
|
return { ok: false, reason: `invalid output kind: ${String(env.output)}` };
|
|
157
1132
|
}
|
|
1133
|
+
if (env.verification !== undefined) {
|
|
1134
|
+
const verification = validateVerificationContract(env.verification);
|
|
1135
|
+
if (!verification.ok) {
|
|
1136
|
+
return {
|
|
1137
|
+
ok: false,
|
|
1138
|
+
reason: verification.reason,
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
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 };
|
|
1157
|
+
if (env.verification === undefined) {
|
|
1158
|
+
return { ok: false, reason: "missing required field: verification" };
|
|
1159
|
+
}
|
|
1160
|
+
const expectedClassification = {
|
|
1161
|
+
red: "expected-red",
|
|
1162
|
+
green: "expected-green",
|
|
1163
|
+
refactor: "expected-refactor",
|
|
1164
|
+
}[String(env.phase)];
|
|
1165
|
+
if (
|
|
1166
|
+
expectedClassification === undefined ||
|
|
1167
|
+
(env.verification as VerificationContract).classification !==
|
|
1168
|
+
expectedClassification
|
|
1169
|
+
) {
|
|
1170
|
+
return {
|
|
1171
|
+
ok: false,
|
|
1172
|
+
reason:
|
|
1173
|
+
"verification classification does not match implementation phase",
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (env.stage === "abel-implement" && env.output === "diff") {
|
|
1178
|
+
const snapshotReason = validateImplementationSnapshot(env.snapshot, [
|
|
1179
|
+
...(decl.read as string[]),
|
|
1180
|
+
...(decl.write as string[]),
|
|
1181
|
+
]);
|
|
1182
|
+
if (snapshotReason !== null) {
|
|
1183
|
+
return { ok: false, reason: snapshotReason };
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
158
1186
|
if (
|
|
159
1187
|
env.snapshot !== undefined &&
|
|
160
1188
|
(env.snapshot === null || typeof env.snapshot !== "object")
|
|
@@ -172,68 +1200,196 @@ export function diffWritePaths(diffText: string): {
|
|
|
172
1200
|
if (typeof diffText !== "string" || diffText.length === 0) {
|
|
173
1201
|
throw new Error("empty diff");
|
|
174
1202
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const lines = diffText.split("\n");
|
|
179
|
-
if (lines.some((line) => /^copy (from|to) /.test(line))) {
|
|
180
|
-
throw new Error("copy records are not supported");
|
|
181
|
-
}
|
|
182
|
-
if (lines.some((line) => /^rename (from|to) /.test(line))) {
|
|
183
|
-
throw new Error("rename records are not supported");
|
|
184
|
-
}
|
|
185
|
-
const paths: string[] = [];
|
|
186
|
-
let sawHeader = false;
|
|
187
|
-
let pendingOld: string | null = null;
|
|
1203
|
+
const hasFinalLf = diffText.endsWith("\n");
|
|
1204
|
+
const lines = (hasFinalLf ? diffText.slice(0, -1) : diffText).split("\n");
|
|
1205
|
+
|
|
188
1206
|
for (const line of lines) {
|
|
189
|
-
if (/^
|
|
1207
|
+
if (/^(?:GIT binary patch|Binary files )/.test(line)) {
|
|
1208
|
+
throw new Error("binary diff is not supported");
|
|
1209
|
+
}
|
|
1210
|
+
if (/^(?:copy|rename) (?:from|to) /.test(line)) {
|
|
1211
|
+
throw new Error(`${line.split(" ", 1)[0]} records are not supported`);
|
|
1212
|
+
}
|
|
1213
|
+
if (/^(?:old|new) mode /.test(line)) {
|
|
190
1214
|
throw new Error("mode transitions are not supported");
|
|
191
1215
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
1216
|
+
const mode = /^(?:new file mode|deleted file mode) (.+)$/.exec(line);
|
|
1217
|
+
if (mode) requireRegularFileMode(mode[1]);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const paths: string[] = [];
|
|
1221
|
+
let i = 0;
|
|
1222
|
+
while (i < lines.length) {
|
|
1223
|
+
if (!isFileSectionStart(lines[i])) {
|
|
1224
|
+
if (paths.length === 0) {
|
|
1225
|
+
throw new Error("diff has no ordinary text headers");
|
|
196
1226
|
}
|
|
197
|
-
|
|
198
|
-
|
|
1227
|
+
throw new Error(`unconsumed diff content: ${lines[i]}`);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
let gitTarget: string | null = null;
|
|
1231
|
+
if (lines[i].startsWith("diff --git ")) {
|
|
1232
|
+
gitTarget = parseGitTarget(lines[i]);
|
|
1233
|
+
i++;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
let operation: "add" | "delete" | null = null;
|
|
1237
|
+
const mode = /^(new file mode|deleted file mode) (.+)$/.exec(
|
|
1238
|
+
lines[i] ?? "",
|
|
1239
|
+
);
|
|
1240
|
+
if (mode) {
|
|
1241
|
+
operation = mode[1] === "new file mode" ? "add" : "delete";
|
|
1242
|
+
i++;
|
|
1243
|
+
}
|
|
1244
|
+
if (lines[i]?.startsWith("index ")) {
|
|
1245
|
+
parseIndexMetadata(lines[i]);
|
|
1246
|
+
i++;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (!lines[i]?.startsWith("--- ")) {
|
|
1250
|
+
throw new Error("file section is missing an ordinary --- header");
|
|
1251
|
+
}
|
|
1252
|
+
const oldPath = parseHeaderPath(lines[i].slice(4), "a");
|
|
1253
|
+
i++;
|
|
1254
|
+
if (!lines[i]?.startsWith("+++ ")) {
|
|
1255
|
+
throw new Error("diff header pair is malformed");
|
|
1256
|
+
}
|
|
1257
|
+
const newPath = parseHeaderPath(lines[i].slice(4), "b");
|
|
1258
|
+
i++;
|
|
1259
|
+
|
|
1260
|
+
if (oldPath === null && newPath === null) {
|
|
1261
|
+
throw new Error("diff header pair has no target");
|
|
1262
|
+
}
|
|
1263
|
+
if (oldPath !== null && newPath !== null && oldPath !== newPath) {
|
|
1264
|
+
throw new Error("mismatched old/new diff targets");
|
|
1265
|
+
}
|
|
1266
|
+
const target = oldPath ?? newPath;
|
|
1267
|
+
if (target === null) throw new Error("diff header pair has no target");
|
|
1268
|
+
if (gitTarget !== null && gitTarget !== target) {
|
|
1269
|
+
throw new Error("mismatched diff --git and text targets");
|
|
1270
|
+
}
|
|
1271
|
+
if (
|
|
1272
|
+
(operation === "add" && oldPath !== null) ||
|
|
1273
|
+
(operation === "delete" && newPath !== null)
|
|
1274
|
+
) {
|
|
1275
|
+
throw new Error("create/delete metadata does not match text headers");
|
|
1276
|
+
}
|
|
1277
|
+
if (paths.includes(target)) throw new Error("duplicate diff targets");
|
|
1278
|
+
paths.push(target);
|
|
1279
|
+
|
|
1280
|
+
let sawHunk = false;
|
|
1281
|
+
while (lines[i]?.startsWith("@@")) {
|
|
1282
|
+
const hunk = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*)?$/.exec(
|
|
1283
|
+
lines[i],
|
|
1284
|
+
);
|
|
1285
|
+
if (!hunk) throw new Error(`invalid hunk header: ${lines[i]}`);
|
|
1286
|
+
const range = [
|
|
1287
|
+
Number(hunk[1]),
|
|
1288
|
+
Number(hunk[2] ?? "1"),
|
|
1289
|
+
Number(hunk[3]),
|
|
1290
|
+
Number(hunk[4] ?? "1"),
|
|
1291
|
+
];
|
|
1292
|
+
if (!range.every(Number.isSafeInteger)) {
|
|
1293
|
+
throw new Error("hunk range exceeds supported integer bounds");
|
|
199
1294
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
1295
|
+
let oldRemaining = range[1];
|
|
1296
|
+
let newRemaining = range[3];
|
|
1297
|
+
sawHunk = true;
|
|
1298
|
+
i++;
|
|
1299
|
+
|
|
1300
|
+
while (oldRemaining > 0 || newRemaining > 0) {
|
|
1301
|
+
if (i >= lines.length) throw new Error("hunk body underflow");
|
|
1302
|
+
const body = lines[i];
|
|
1303
|
+
switch (body[0]) {
|
|
1304
|
+
case " ":
|
|
1305
|
+
if (oldRemaining === 0 || newRemaining === 0) {
|
|
1306
|
+
throw new Error("hunk body overflow");
|
|
1307
|
+
}
|
|
1308
|
+
oldRemaining--;
|
|
1309
|
+
newRemaining--;
|
|
1310
|
+
break;
|
|
1311
|
+
case "-":
|
|
1312
|
+
if (oldRemaining === 0) throw new Error("hunk body overflow");
|
|
1313
|
+
oldRemaining--;
|
|
1314
|
+
break;
|
|
1315
|
+
case "+":
|
|
1316
|
+
if (newRemaining === 0) throw new Error("hunk body overflow");
|
|
1317
|
+
newRemaining--;
|
|
1318
|
+
break;
|
|
1319
|
+
default:
|
|
1320
|
+
throw new Error("hunk body underflow");
|
|
218
1321
|
}
|
|
1322
|
+
i++;
|
|
1323
|
+
if (lines[i] === "\") i++;
|
|
219
1324
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
sawHeader = true;
|
|
1325
|
+
}
|
|
1326
|
+
if (!sawHunk) {
|
|
1327
|
+
throw new Error("file section has no valid unified-diff hunk");
|
|
224
1328
|
}
|
|
225
1329
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
1330
|
+
|
|
1331
|
+
if (!hasFinalLf) throw new Error("diff must end with LF");
|
|
229
1332
|
return { paths, kind: "text" };
|
|
230
1333
|
}
|
|
231
1334
|
|
|
232
|
-
function
|
|
1335
|
+
function isFileSectionStart(line: string | undefined): boolean {
|
|
1336
|
+
return (
|
|
1337
|
+
line?.startsWith("diff --git ") === true ||
|
|
1338
|
+
line?.startsWith("new file mode ") === true ||
|
|
1339
|
+
line?.startsWith("deleted file mode ") === true ||
|
|
1340
|
+
line?.startsWith("index ") === true ||
|
|
1341
|
+
line?.startsWith("--- ") === true
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function parseGitTarget(line: string): string {
|
|
1346
|
+
const prefix = "diff --git a/";
|
|
1347
|
+
if (!line.startsWith(prefix)) throw new Error("malformed diff --git header");
|
|
1348
|
+
const rest = line.slice(prefix.length);
|
|
1349
|
+
for (
|
|
1350
|
+
let separator = rest.indexOf(" b/");
|
|
1351
|
+
separator >= 0;
|
|
1352
|
+
separator = rest.indexOf(" b/", separator + 1)
|
|
1353
|
+
) {
|
|
1354
|
+
const oldPath = rest.slice(0, separator);
|
|
1355
|
+
const newPath = rest.slice(separator + 3);
|
|
1356
|
+
if (oldPath === newPath) return requireCanonicalDiffPath(oldPath);
|
|
1357
|
+
}
|
|
1358
|
+
throw new Error("mismatched diff --git targets");
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function parseIndexMetadata(line: string): void {
|
|
1362
|
+
const index = /^index [0-9a-f]+\.\.[0-9a-f]+(?: ([0-7]{6}))?$/.exec(line);
|
|
1363
|
+
if (!index) throw new Error("malformed index metadata");
|
|
1364
|
+
if (index[1]) requireRegularFileMode(index[1]);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function requireRegularFileMode(mode: string): void {
|
|
1368
|
+
if (mode === "160000") {
|
|
1369
|
+
throw new Error("submodule mode is not supported: 160000");
|
|
1370
|
+
}
|
|
1371
|
+
if (mode !== "100644" && mode !== "100755") {
|
|
1372
|
+
throw new Error(`non-regular file mode is not supported: ${mode}`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function parseHeaderPath(header: string, prefix: "a" | "b"): string | null {
|
|
233
1377
|
if (header === "/dev/null") return null;
|
|
234
|
-
const
|
|
235
|
-
if (
|
|
236
|
-
throw new Error(
|
|
1378
|
+
const other = prefix === "a" ? "b" : "a";
|
|
1379
|
+
if (header.startsWith(`${other}/`)) {
|
|
1380
|
+
throw new Error("mismatched old/new diff header prefixes");
|
|
1381
|
+
}
|
|
1382
|
+
const path = header.startsWith(`${prefix}/`) ? header.slice(2) : header;
|
|
1383
|
+
return requireCanonicalDiffPath(path);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function requireCanonicalDiffPath(path: string): string {
|
|
1387
|
+
if (
|
|
1388
|
+
!isValidRelativePath(path) ||
|
|
1389
|
+
path.trim() !== path ||
|
|
1390
|
+
/[\r\n\t]/.test(path)
|
|
1391
|
+
) {
|
|
1392
|
+
throw new Error(`noncanonical or escaping diff path: ${path}`);
|
|
237
1393
|
}
|
|
238
1394
|
return path;
|
|
239
1395
|
}
|
|
@@ -261,37 +1417,154 @@ export interface DiffResult {
|
|
|
261
1417
|
diff: string;
|
|
262
1418
|
expectedVerification: string;
|
|
263
1419
|
risks: string[];
|
|
264
|
-
nextStep: string;
|
|
265
1420
|
contractCompliant: boolean;
|
|
266
1421
|
}
|
|
267
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
|
+
|
|
268
1498
|
export function validateEvidenceResult(value: unknown): {
|
|
269
1499
|
ok: boolean;
|
|
270
1500
|
reason?: string;
|
|
1501
|
+
failure?: CandidateFailure;
|
|
271
1502
|
} {
|
|
272
1503
|
if (value === null || typeof value !== "object")
|
|
273
1504
|
return { ok: false, reason: "missing result" };
|
|
274
1505
|
const r = value as Record<string, unknown>;
|
|
275
1506
|
if (r.kind !== "evidence") return { ok: false, reason: "wrong result kind" };
|
|
276
|
-
|
|
1507
|
+
const fields = [
|
|
277
1508
|
"id",
|
|
278
1509
|
"role",
|
|
1510
|
+
"kind",
|
|
279
1511
|
"conclusions",
|
|
280
1512
|
"citations",
|
|
1513
|
+
"constraints",
|
|
1514
|
+
"dependencies",
|
|
1515
|
+
"risks",
|
|
281
1516
|
"blockingQuestions",
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
1517
|
+
"hints",
|
|
1518
|
+
] as const;
|
|
1519
|
+
if (!hasExactKeys(r, fields)) {
|
|
1520
|
+
return { ok: false, reason: "invalid evidence result fields" };
|
|
285
1521
|
}
|
|
286
|
-
if (
|
|
1522
|
+
if (!validIdentifier(r.id) || typeof r.role !== "string")
|
|
287
1523
|
return { ok: false, reason: "invalid identity" };
|
|
288
1524
|
if (!(ROLES as readonly string[]).includes(r.role))
|
|
289
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
|
+
}
|
|
290
1559
|
const serialized = JSON.stringify(r);
|
|
291
|
-
if (serialized
|
|
1560
|
+
if (Buffer.byteLength(serialized, "utf8") > LIMITS.maxCompleteResultBytes) {
|
|
292
1561
|
return {
|
|
293
1562
|
ok: false,
|
|
294
1563
|
reason: `result exceeds ${LIMITS.maxCompleteResultBytes} bytes`,
|
|
1564
|
+
failure: {
|
|
1565
|
+
kind: "result-limit",
|
|
1566
|
+
limitBytes: LIMITS.maxCompleteResultBytes,
|
|
1567
|
+
},
|
|
295
1568
|
};
|
|
296
1569
|
}
|
|
297
1570
|
return { ok: true };
|
|
@@ -301,23 +1574,28 @@ export function validateDiffResult(value: unknown): {
|
|
|
301
1574
|
ok: boolean;
|
|
302
1575
|
reason?: string;
|
|
303
1576
|
paths?: string[];
|
|
1577
|
+
failure?: CandidateFailure;
|
|
304
1578
|
} {
|
|
305
1579
|
if (value === null || typeof value !== "object")
|
|
306
1580
|
return { ok: false, reason: "missing result" };
|
|
307
1581
|
const r = value as Record<string, unknown>;
|
|
308
1582
|
if (r.kind !== "diff") return { ok: false, reason: "wrong result kind" };
|
|
309
|
-
|
|
1583
|
+
const fields = [
|
|
310
1584
|
"id",
|
|
311
1585
|
"role",
|
|
1586
|
+
"kind",
|
|
312
1587
|
"taskId",
|
|
313
1588
|
"phase",
|
|
314
1589
|
"summary",
|
|
315
1590
|
"diff",
|
|
316
1591
|
"expectedVerification",
|
|
317
1592
|
"risks",
|
|
318
|
-
"nextStep",
|
|
319
1593
|
"contractCompliant",
|
|
320
|
-
]
|
|
1594
|
+
] as const;
|
|
1595
|
+
if (!hasExactKeys(r, fields)) {
|
|
1596
|
+
return { ok: false, reason: "invalid diff result fields" };
|
|
1597
|
+
}
|
|
1598
|
+
for (const field of fields) {
|
|
321
1599
|
if (r[field] === undefined)
|
|
322
1600
|
return { ok: false, reason: `missing field: ${field}` };
|
|
323
1601
|
}
|
|
@@ -344,9 +1622,7 @@ export function validateDiffResult(value: unknown): {
|
|
|
344
1622
|
typeof r.summary !== "string" ||
|
|
345
1623
|
r.summary.length === 0 ||
|
|
346
1624
|
typeof r.expectedVerification !== "string" ||
|
|
347
|
-
r.expectedVerification.length === 0
|
|
348
|
-
typeof r.nextStep !== "string" ||
|
|
349
|
-
r.nextStep.length === 0
|
|
1625
|
+
r.expectedVerification.length === 0
|
|
350
1626
|
) {
|
|
351
1627
|
return { ok: false, reason: "invalid diff result text" };
|
|
352
1628
|
}
|
|
@@ -365,10 +1641,14 @@ export function validateDiffResult(value: unknown): {
|
|
|
365
1641
|
return { ok: false, reason: `invalid diff: ${(err as Error).message}` };
|
|
366
1642
|
}
|
|
367
1643
|
const serialized = JSON.stringify(r);
|
|
368
|
-
if (serialized
|
|
1644
|
+
if (Buffer.byteLength(serialized, "utf8") > LIMITS.maxCompleteResultBytes) {
|
|
369
1645
|
return {
|
|
370
1646
|
ok: false,
|
|
371
1647
|
reason: `result exceeds ${LIMITS.maxCompleteResultBytes} bytes`,
|
|
1648
|
+
failure: {
|
|
1649
|
+
kind: "result-limit",
|
|
1650
|
+
limitBytes: LIMITS.maxCompleteResultBytes,
|
|
1651
|
+
},
|
|
372
1652
|
};
|
|
373
1653
|
}
|
|
374
1654
|
return { ok: true, paths };
|