@abelxiaoxing/cadence 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -1
- package/package.json +1 -1
- package/prompts/abel-design.md +14 -2
- package/prompts/abel-implement.md +10 -2
- package/skills/abel-workflow/SKILL.md +36 -0
- package/src/candidate-preflight.ts +237 -90
- package/src/child-session.ts +1 -0
- package/src/contracts.ts +482 -41
- package/src/patch.ts +13 -1
- package/src/result-store.ts +6 -9
- package/src/runtime.ts +66 -10
- package/src/verification-capability.ts +481 -0
package/src/contracts.ts
CHANGED
|
@@ -133,6 +133,14 @@ export const APPROVAL_BOUNDARY_CODES = [
|
|
|
133
133
|
"verification-contract-insufficient",
|
|
134
134
|
"agents-contract-insufficient",
|
|
135
135
|
] as const;
|
|
136
|
+
export const VERIFICATION_ADAPTER_CODES = [
|
|
137
|
+
"input-missing",
|
|
138
|
+
"local-executable-missing",
|
|
139
|
+
"runner-missing",
|
|
140
|
+
"script-command-mismatch",
|
|
141
|
+
"script-missing",
|
|
142
|
+
"script-unsafe",
|
|
143
|
+
] as const;
|
|
136
144
|
export const CHILD_TRANSPORT_CODES = [
|
|
137
145
|
"child-no-final-assistant",
|
|
138
146
|
"child-provider-stream-aborted",
|
|
@@ -153,6 +161,8 @@ export type ArtifactFailureCode = (typeof ARTIFACT_FAILURE_CODES)[number];
|
|
|
153
161
|
export type StaleFailureCode = (typeof STALE_FAILURE_CODES)[number];
|
|
154
162
|
export type EnvironmentFailureCode = (typeof ENVIRONMENT_FAILURE_CODES)[number];
|
|
155
163
|
export type ApprovalBoundaryCode = (typeof APPROVAL_BOUNDARY_CODES)[number];
|
|
164
|
+
export type VerificationAdapterCode =
|
|
165
|
+
(typeof VERIFICATION_ADAPTER_CODES)[number];
|
|
156
166
|
export type ChildTransportCode = (typeof CHILD_TRANSPORT_CODES)[number];
|
|
157
167
|
export type FailureStage = (typeof FAILURE_STAGES)[number];
|
|
158
168
|
|
|
@@ -172,6 +182,7 @@ export type CandidateFailure =
|
|
|
172
182
|
| { kind: "artifact"; code: ArtifactFailureCode; evidence?: string[] }
|
|
173
183
|
| { kind: "stale"; code: StaleFailureCode }
|
|
174
184
|
| EnvironmentFailure
|
|
185
|
+
| { kind: "verification-adapter"; code: VerificationAdapterCode }
|
|
175
186
|
| { kind: "approval-boundary"; code: ApprovalBoundaryCode }
|
|
176
187
|
| { kind: "cancelled"; code: "cancelled" }
|
|
177
188
|
| { kind: "result-limit"; limitBytes: number };
|
|
@@ -194,6 +205,7 @@ export type CandidateRejection =
|
|
|
194
205
|
|
|
195
206
|
export type TaskFailure =
|
|
196
207
|
| { kind: "approval-boundary"; code: ApprovalBoundaryCode }
|
|
208
|
+
| { kind: "verification-adapter"; code: VerificationAdapterCode }
|
|
197
209
|
| {
|
|
198
210
|
kind: "attempts-exhausted";
|
|
199
211
|
cause: "artifact" | "stale" | "transport";
|
|
@@ -219,14 +231,84 @@ export type ApplyCandidateResult =
|
|
|
219
231
|
const NONCANONICAL = /(^|\/)\.\.(\/|$)|(^|\/)\/|^\//;
|
|
220
232
|
const SNAPSHOT_SHA256 = /^[a-f0-9]{64}$/;
|
|
221
233
|
|
|
222
|
-
export
|
|
234
|
+
export type VerificationClassification =
|
|
235
|
+
| "expected-red"
|
|
236
|
+
| "expected-green"
|
|
237
|
+
| "expected-refactor";
|
|
238
|
+
|
|
239
|
+
interface VerificationBase {
|
|
223
240
|
id: string;
|
|
224
|
-
|
|
225
|
-
classification: "expected-red" | "expected-green" | "expected-refactor";
|
|
241
|
+
classification: VerificationClassification;
|
|
226
242
|
expectedFailure?: string;
|
|
243
|
+
/** Internal marker retained only for normalized 1.0.x argv compatibility. */
|
|
244
|
+
legacy?: true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export type PackageManager = "bun" | "npm" | "pnpm" | "yarn";
|
|
248
|
+
|
|
249
|
+
export interface PackageScriptRunner {
|
|
250
|
+
kind: "package-script";
|
|
251
|
+
packageManager: PackageManager;
|
|
252
|
+
script: string;
|
|
253
|
+
/** Exact Gate-B-approved package.json script value. Omitted only for legacy input. */
|
|
254
|
+
command?: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export type ExecutableRunner =
|
|
258
|
+
| PackageScriptRunner
|
|
259
|
+
| { kind: "local-binary"; executable: string }
|
|
260
|
+
| { kind: "npx"; executable: string; noInstall: true };
|
|
261
|
+
|
|
262
|
+
export interface VitestVerificationContract extends VerificationBase {
|
|
263
|
+
kind: "vitest";
|
|
264
|
+
runner: ExecutableRunner;
|
|
265
|
+
testFiles: string[];
|
|
266
|
+
args: string[];
|
|
227
267
|
minTests: number;
|
|
228
268
|
}
|
|
229
269
|
|
|
270
|
+
export interface PackageScriptVerificationContract extends VerificationBase {
|
|
271
|
+
kind: "package-script";
|
|
272
|
+
packageManager: PackageManager;
|
|
273
|
+
script: string;
|
|
274
|
+
/** Exact Gate-B-approved package.json script value. Omitted only for legacy input. */
|
|
275
|
+
command?: string;
|
|
276
|
+
args: string[];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface StaticCheckVerificationContract extends VerificationBase {
|
|
280
|
+
kind: "static-check";
|
|
281
|
+
runner: ExecutableRunner | { kind: "node"; script: string };
|
|
282
|
+
args: string[];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export type AtomicVerificationContract =
|
|
286
|
+
| VitestVerificationContract
|
|
287
|
+
| PackageScriptVerificationContract
|
|
288
|
+
| StaticCheckVerificationContract;
|
|
289
|
+
|
|
290
|
+
export interface VerificationStepsContract {
|
|
291
|
+
kind: "steps";
|
|
292
|
+
id: string;
|
|
293
|
+
classification: VerificationClassification;
|
|
294
|
+
steps: AtomicVerificationContract[];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** @deprecated Accepted only as 1.0.x compatibility input and normalized immediately. */
|
|
298
|
+
export interface LegacyVerificationContract extends VerificationBase {
|
|
299
|
+
argv: string[];
|
|
300
|
+
minTests: number;
|
|
301
|
+
kind?: never;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export type StructuredVerificationContract =
|
|
305
|
+
| AtomicVerificationContract
|
|
306
|
+
| VerificationStepsContract;
|
|
307
|
+
|
|
308
|
+
export type VerificationContract =
|
|
309
|
+
| StructuredVerificationContract
|
|
310
|
+
| LegacyVerificationContract;
|
|
311
|
+
|
|
230
312
|
export interface ImpactClosureContract {
|
|
231
313
|
changedSurfaces: ImpactSurface[];
|
|
232
314
|
searchEvidence: string[];
|
|
@@ -367,57 +449,387 @@ export function isAgentsPath(p: unknown): p is string {
|
|
|
367
449
|
return isValidRelativePath(p) && /(?:^|\/)AGENTS\.md$/u.test(p);
|
|
368
450
|
}
|
|
369
451
|
|
|
370
|
-
|
|
452
|
+
const UNSAFE_VERIFICATION_TOKEN = /[;&|`$<>\n\r\0]/u;
|
|
453
|
+
const VERIFICATION_NAME = /^[a-z0-9][a-z0-9._:@/-]*$/iu;
|
|
454
|
+
const EXECUTABLE_NAME = /^[a-z0-9][a-z0-9._-]*$/iu;
|
|
455
|
+
|
|
456
|
+
function validVerificationToken(value: unknown): value is string {
|
|
457
|
+
const optionValue =
|
|
458
|
+
typeof value === "string" && value.includes("=")
|
|
459
|
+
? value.slice(value.indexOf("=") + 1)
|
|
460
|
+
: value;
|
|
461
|
+
return (
|
|
462
|
+
typeof value === "string" &&
|
|
463
|
+
value.length > 0 &&
|
|
464
|
+
value.length <= 512 &&
|
|
465
|
+
value.trim() === value &&
|
|
466
|
+
!UNSAFE_VERIFICATION_TOKEN.test(value) &&
|
|
467
|
+
!value.includes("\\") &&
|
|
468
|
+
!value.startsWith("/") &&
|
|
469
|
+
!/^[a-z]:\//iu.test(value) &&
|
|
470
|
+
!/(^|\/)\.\.(\/|$)/u.test(value) &&
|
|
471
|
+
typeof optionValue === "string" &&
|
|
472
|
+
!optionValue.startsWith("/") &&
|
|
473
|
+
!/^[a-z]:\//iu.test(optionValue) &&
|
|
474
|
+
!/(^|\/)\.\.(\/|$)/u.test(optionValue)
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function validVerificationArgs(value: unknown): value is string[] {
|
|
479
|
+
return (
|
|
480
|
+
Array.isArray(value) &&
|
|
481
|
+
value.length <= 128 &&
|
|
482
|
+
value.every(validVerificationToken)
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function validScriptCommand(value: unknown): value is string {
|
|
487
|
+
return (
|
|
488
|
+
typeof value === "string" &&
|
|
489
|
+
value.length > 0 &&
|
|
490
|
+
value.length <= 1024 &&
|
|
491
|
+
value.trim() === value &&
|
|
492
|
+
!UNSAFE_VERIFICATION_TOKEN.test(value)
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function validVerificationIdentity(contract: Record<string, unknown>): boolean {
|
|
497
|
+
if (
|
|
498
|
+
typeof contract.id !== "string" ||
|
|
499
|
+
contract.id.length === 0 ||
|
|
500
|
+
contract.id.length > 128 ||
|
|
501
|
+
!["expected-red", "expected-green", "expected-refactor"].includes(
|
|
502
|
+
String(contract.classification),
|
|
503
|
+
)
|
|
504
|
+
) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
return contract.classification === "expected-red"
|
|
508
|
+
? typeof contract.expectedFailure === "string" &&
|
|
509
|
+
contract.expectedFailure.length > 0 &&
|
|
510
|
+
contract.expectedFailure.length <= 512
|
|
511
|
+
: contract.expectedFailure === undefined;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function validatePackageScriptRunner(
|
|
515
|
+
value: unknown,
|
|
516
|
+
requireCommand: boolean,
|
|
517
|
+
): value is PackageScriptRunner {
|
|
518
|
+
if (
|
|
519
|
+
!hasExactKeys(
|
|
520
|
+
value,
|
|
521
|
+
requireCommand
|
|
522
|
+
? ["kind", "packageManager", "script", "command"]
|
|
523
|
+
: ["kind", "packageManager", "script"],
|
|
524
|
+
requireCommand ? [] : ["command"],
|
|
525
|
+
) ||
|
|
526
|
+
value.kind !== "package-script" ||
|
|
527
|
+
!["bun", "npm", "pnpm", "yarn"].includes(String(value.packageManager)) ||
|
|
528
|
+
typeof value.script !== "string" ||
|
|
529
|
+
!VERIFICATION_NAME.test(value.script) ||
|
|
530
|
+
value.script.includes("/") ||
|
|
531
|
+
(value.command !== undefined && !validScriptCommand(value.command))
|
|
532
|
+
) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
return !requireCommand || value.command !== undefined;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function validateExecutableRunner(
|
|
539
|
+
value: unknown,
|
|
540
|
+
requirePackageCommand: boolean,
|
|
541
|
+
): value is ExecutableRunner {
|
|
542
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
543
|
+
const runner = value as Record<string, unknown>;
|
|
544
|
+
if (runner.kind === "package-script") {
|
|
545
|
+
return validatePackageScriptRunner(runner, requirePackageCommand);
|
|
546
|
+
}
|
|
547
|
+
if (
|
|
548
|
+
runner.kind === "local-binary" &&
|
|
549
|
+
hasExactKeys(runner, ["kind", "executable"])
|
|
550
|
+
) {
|
|
551
|
+
return (
|
|
552
|
+
typeof runner.executable === "string" &&
|
|
553
|
+
EXECUTABLE_NAME.test(runner.executable)
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
return (
|
|
557
|
+
runner.kind === "npx" &&
|
|
558
|
+
hasExactKeys(runner, ["kind", "executable", "noInstall"]) &&
|
|
559
|
+
typeof runner.executable === "string" &&
|
|
560
|
+
EXECUTABLE_NAME.test(runner.executable) &&
|
|
561
|
+
runner.noInstall === true
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function validateVitestRunner(
|
|
566
|
+
value: ExecutableRunner,
|
|
567
|
+
legacy: boolean,
|
|
568
|
+
): boolean {
|
|
569
|
+
if (legacy) return true;
|
|
570
|
+
if (value.kind === "package-script") {
|
|
571
|
+
const tokens = value.command?.split(/\s+/u) ?? [];
|
|
572
|
+
return (
|
|
573
|
+
tokens[0] === "vitest" ||
|
|
574
|
+
(tokens[0] === "npx" &&
|
|
575
|
+
tokens[1] === "--no-install" &&
|
|
576
|
+
tokens[2] === "vitest")
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
return value.executable === "vitest";
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function validateAtomicVerification(
|
|
583
|
+
value: unknown,
|
|
584
|
+
requirePackageCommand: boolean,
|
|
585
|
+
): value is AtomicVerificationContract {
|
|
586
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
587
|
+
const contract = value as Record<string, unknown>;
|
|
588
|
+
const legacy = contract.legacy === true;
|
|
589
|
+
const packageCommandRequired = requirePackageCommand && !legacy;
|
|
590
|
+
const expectedFailure =
|
|
591
|
+
contract.classification === "expected-red" ? ["expectedFailure"] : [];
|
|
592
|
+
if (contract.kind === "vitest") {
|
|
593
|
+
const legacyRunner = contract.runner as Record<string, unknown>;
|
|
594
|
+
const executableRunner = contract.runner as ExecutableRunner;
|
|
595
|
+
return (
|
|
596
|
+
hasExactKeys(contract, [
|
|
597
|
+
"kind",
|
|
598
|
+
"id",
|
|
599
|
+
"runner",
|
|
600
|
+
"testFiles",
|
|
601
|
+
"args",
|
|
602
|
+
"classification",
|
|
603
|
+
...expectedFailure,
|
|
604
|
+
"minTests",
|
|
605
|
+
...(legacy ? ["legacy"] : []),
|
|
606
|
+
]) &&
|
|
607
|
+
validVerificationIdentity(contract) &&
|
|
608
|
+
validateExecutableRunner(contract.runner, packageCommandRequired) &&
|
|
609
|
+
validateVitestRunner(executableRunner, legacy) &&
|
|
610
|
+
Array.isArray(contract.testFiles) &&
|
|
611
|
+
contract.testFiles.length > 0 &&
|
|
612
|
+
contract.testFiles.length <= 64 &&
|
|
613
|
+
contract.testFiles.every(isValidRelativePath) &&
|
|
614
|
+
new Set(contract.testFiles).size === contract.testFiles.length &&
|
|
615
|
+
validVerificationArgs(contract.args) &&
|
|
616
|
+
Number.isSafeInteger(contract.minTests) &&
|
|
617
|
+
(contract.minTests as number) >= 1 &&
|
|
618
|
+
(!legacy ||
|
|
619
|
+
(hasExactKeys(legacyRunner, ["kind", "packageManager", "script"]) &&
|
|
620
|
+
legacyRunner.kind === "package-script" &&
|
|
621
|
+
legacyRunner.packageManager === "bun" &&
|
|
622
|
+
legacyRunner.script === "test:target" &&
|
|
623
|
+
contract.testFiles.every((part) => part.startsWith("test/")) &&
|
|
624
|
+
contract.args.length === 0))
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
if (contract.kind === "package-script") {
|
|
628
|
+
const runner = {
|
|
629
|
+
kind: "package-script",
|
|
630
|
+
packageManager: contract.packageManager,
|
|
631
|
+
script: contract.script,
|
|
632
|
+
...(contract.command === undefined ? {} : { command: contract.command }),
|
|
633
|
+
};
|
|
634
|
+
return (
|
|
635
|
+
hasExactKeys(
|
|
636
|
+
contract,
|
|
637
|
+
[
|
|
638
|
+
"kind",
|
|
639
|
+
"id",
|
|
640
|
+
"packageManager",
|
|
641
|
+
"script",
|
|
642
|
+
...(packageCommandRequired ? ["command"] : []),
|
|
643
|
+
"args",
|
|
644
|
+
"classification",
|
|
645
|
+
...expectedFailure,
|
|
646
|
+
...(legacy ? ["legacy"] : []),
|
|
647
|
+
],
|
|
648
|
+
packageCommandRequired ? [] : ["command"],
|
|
649
|
+
) &&
|
|
650
|
+
validVerificationIdentity(contract) &&
|
|
651
|
+
validatePackageScriptRunner(runner, packageCommandRequired) &&
|
|
652
|
+
validVerificationArgs(contract.args) &&
|
|
653
|
+
(!legacy ||
|
|
654
|
+
(contract.packageManager === "bun" &&
|
|
655
|
+
contract.script === "check" &&
|
|
656
|
+
contract.command === undefined &&
|
|
657
|
+
contract.args.length === 0))
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
if (contract.kind !== "static-check") return false;
|
|
661
|
+
const runner = contract.runner as Record<string, unknown> | undefined;
|
|
662
|
+
const validRunner =
|
|
663
|
+
validateExecutableRunner(runner, requirePackageCommand) ||
|
|
664
|
+
(hasExactKeys(runner, ["kind", "script"]) &&
|
|
665
|
+
runner.kind === "node" &&
|
|
666
|
+
isValidRelativePath(runner.script));
|
|
667
|
+
return (
|
|
668
|
+
hasExactKeys(contract, [
|
|
669
|
+
"kind",
|
|
670
|
+
"id",
|
|
671
|
+
"runner",
|
|
672
|
+
"args",
|
|
673
|
+
"classification",
|
|
674
|
+
...expectedFailure,
|
|
675
|
+
]) &&
|
|
676
|
+
validVerificationIdentity(contract) &&
|
|
677
|
+
validRunner &&
|
|
678
|
+
validVerificationArgs(contract.args)
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function validateStructuredVerification(
|
|
371
683
|
value: unknown,
|
|
372
|
-
):
|
|
373
|
-
if (value
|
|
374
|
-
return { ok: false, reason: "invalid verification contract" };
|
|
684
|
+
): value is StructuredVerificationContract {
|
|
685
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
375
686
|
const contract = value as Record<string, unknown>;
|
|
687
|
+
if (contract.kind !== "steps") {
|
|
688
|
+
return validateAtomicVerification(contract, true);
|
|
689
|
+
}
|
|
376
690
|
if (
|
|
691
|
+
!hasExactKeys(contract, ["kind", "id", "classification", "steps"]) ||
|
|
377
692
|
typeof contract.id !== "string" ||
|
|
378
693
|
contract.id.length === 0 ||
|
|
379
694
|
contract.id.length > 128 ||
|
|
380
|
-
!Array.isArray(contract.argv) ||
|
|
381
|
-
contract.argv.some(
|
|
382
|
-
(part) =>
|
|
383
|
-
typeof part !== "string" ||
|
|
384
|
-
part.length === 0 ||
|
|
385
|
-
/[;&|`$<>\n\r]/u.test(part) ||
|
|
386
|
-
part.includes("\0"),
|
|
387
|
-
) ||
|
|
388
695
|
!["expected-red", "expected-green", "expected-refactor"].includes(
|
|
389
696
|
String(contract.classification),
|
|
390
697
|
) ||
|
|
698
|
+
!Array.isArray(contract.steps) ||
|
|
699
|
+
contract.steps.length === 0 ||
|
|
700
|
+
contract.steps.length > 8 ||
|
|
701
|
+
!contract.steps.every((step) => validateAtomicVerification(step, true))
|
|
702
|
+
) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
const steps = contract.steps as AtomicVerificationContract[];
|
|
706
|
+
return (
|
|
707
|
+
steps
|
|
708
|
+
.slice(0, -1)
|
|
709
|
+
.every((step) => step.classification === "expected-green") &&
|
|
710
|
+
steps.at(-1)?.classification === contract.classification &&
|
|
711
|
+
new Set(steps.map((step) => step.id)).size === steps.length
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function normalizeLegacyVerification(
|
|
716
|
+
contract: Record<string, unknown>,
|
|
717
|
+
): StructuredVerificationContract | null {
|
|
718
|
+
if (
|
|
719
|
+
!hasExactKeys(
|
|
720
|
+
contract,
|
|
721
|
+
["id", "argv", "classification", "minTests"],
|
|
722
|
+
["expectedFailure"],
|
|
723
|
+
) ||
|
|
724
|
+
!validVerificationIdentity(contract) ||
|
|
391
725
|
!Number.isSafeInteger(contract.minTests) ||
|
|
392
|
-
(contract.minTests as number) < 1
|
|
726
|
+
(contract.minTests as number) < 1 ||
|
|
727
|
+
!Array.isArray(contract.argv) ||
|
|
728
|
+
!contract.argv.every(validVerificationToken)
|
|
393
729
|
) {
|
|
394
|
-
return
|
|
730
|
+
return null;
|
|
395
731
|
}
|
|
396
732
|
const argv = contract.argv as string[];
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
733
|
+
const base = {
|
|
734
|
+
id: contract.id as string,
|
|
735
|
+
classification: contract.classification as VerificationClassification,
|
|
736
|
+
...(contract.expectedFailure === undefined
|
|
737
|
+
? {}
|
|
738
|
+
: { expectedFailure: contract.expectedFailure as string }),
|
|
739
|
+
};
|
|
740
|
+
if (
|
|
403
741
|
argv.length >= 4 &&
|
|
404
742
|
argv[0] === "bun" &&
|
|
405
743
|
argv[1] === "run" &&
|
|
406
744
|
argv[2] === "test:target" &&
|
|
407
745
|
argv
|
|
408
746
|
.slice(3)
|
|
409
|
-
.every((part) => part.startsWith("test/") && isValidRelativePath(part))
|
|
410
|
-
|
|
411
|
-
return {
|
|
747
|
+
.every((part) => part.startsWith("test/") && isValidRelativePath(part))
|
|
748
|
+
) {
|
|
749
|
+
return {
|
|
750
|
+
...base,
|
|
751
|
+
kind: "vitest",
|
|
752
|
+
runner: {
|
|
753
|
+
kind: "package-script",
|
|
754
|
+
packageManager: "bun",
|
|
755
|
+
script: "test:target",
|
|
756
|
+
},
|
|
757
|
+
testFiles: argv.slice(3),
|
|
758
|
+
args: [],
|
|
759
|
+
minTests: contract.minTests as number,
|
|
760
|
+
legacy: true,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
412
763
|
if (
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
764
|
+
argv.length === 3 &&
|
|
765
|
+
argv[0] === "bun" &&
|
|
766
|
+
argv[1] === "run" &&
|
|
767
|
+
argv[2] === "check"
|
|
417
768
|
) {
|
|
418
|
-
return {
|
|
769
|
+
return {
|
|
770
|
+
...base,
|
|
771
|
+
kind: "package-script",
|
|
772
|
+
packageManager: "bun",
|
|
773
|
+
script: "check",
|
|
774
|
+
args: [],
|
|
775
|
+
legacy: true,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export function validateVerificationContract(
|
|
782
|
+
value: unknown,
|
|
783
|
+
):
|
|
784
|
+
| { ok: true; value: StructuredVerificationContract }
|
|
785
|
+
| { ok: false; reason: string } {
|
|
786
|
+
if (validateStructuredVerification(value)) {
|
|
787
|
+
return { ok: true, value: structuredClone(value) };
|
|
419
788
|
}
|
|
420
|
-
|
|
789
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
790
|
+
const legacy = normalizeLegacyVerification(
|
|
791
|
+
value as Record<string, unknown>,
|
|
792
|
+
);
|
|
793
|
+
if (legacy) return { ok: true, value: legacy };
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
ok: false,
|
|
797
|
+
reason: "verification contract is unsupported by the Implement runtime",
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export function verificationSteps(
|
|
802
|
+
verification: VerificationContract,
|
|
803
|
+
): AtomicVerificationContract[] {
|
|
804
|
+
const normalized = validateVerificationContract(verification);
|
|
805
|
+
if (!normalized.ok) return [];
|
|
806
|
+
return normalized.value.kind === "steps"
|
|
807
|
+
? structuredClone(normalized.value.steps)
|
|
808
|
+
: [structuredClone(normalized.value)];
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export function verificationInputPaths(
|
|
812
|
+
verification: VerificationContract,
|
|
813
|
+
): string[] {
|
|
814
|
+
return [
|
|
815
|
+
...new Set(
|
|
816
|
+
verificationSteps(verification).flatMap((step) =>
|
|
817
|
+
step.kind === "vitest"
|
|
818
|
+
? step.testFiles
|
|
819
|
+
: step.kind === "static-check" && step.runner.kind === "node"
|
|
820
|
+
? [step.runner.script]
|
|
821
|
+
: [],
|
|
822
|
+
),
|
|
823
|
+
),
|
|
824
|
+
];
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export function cloneVerificationContract(
|
|
828
|
+
verification: VerificationContract,
|
|
829
|
+
): StructuredVerificationContract {
|
|
830
|
+
const normalized = validateVerificationContract(verification);
|
|
831
|
+
if (!normalized.ok) throw new Error(normalized.reason);
|
|
832
|
+
return structuredClone(normalized.value);
|
|
421
833
|
}
|
|
422
834
|
|
|
423
835
|
function validateImplementationSnapshot(
|
|
@@ -830,11 +1242,6 @@ function validatePhaseBoundary(
|
|
|
830
1242
|
) {
|
|
831
1243
|
return false;
|
|
832
1244
|
}
|
|
833
|
-
const verificationKeys =
|
|
834
|
-
phase === "red"
|
|
835
|
-
? ["id", "argv", "classification", "expectedFailure", "minTests"]
|
|
836
|
-
: ["id", "argv", "classification", "minTests"];
|
|
837
|
-
if (!hasExactKeys(value.verification, verificationKeys)) return false;
|
|
838
1245
|
const verification = validateVerificationContract(value.verification);
|
|
839
1246
|
if (!verification.ok) return false;
|
|
840
1247
|
const classification = {
|
|
@@ -903,13 +1310,26 @@ function validateTaskBoundary(
|
|
|
903
1310
|
const roots = value.roots as string[];
|
|
904
1311
|
if (
|
|
905
1312
|
phases.some((phase) =>
|
|
906
|
-
[
|
|
907
|
-
|
|
908
|
-
|
|
1313
|
+
[
|
|
1314
|
+
...phase.read,
|
|
1315
|
+
...phase.write,
|
|
1316
|
+
...verificationInputPaths(phase.verification),
|
|
1317
|
+
].some((path) => !isWithinRoots(path, roots)),
|
|
909
1318
|
)
|
|
910
1319
|
) {
|
|
911
1320
|
return false;
|
|
912
1321
|
}
|
|
1322
|
+
if (
|
|
1323
|
+
phases.some((phase) => {
|
|
1324
|
+
if (phase.verification.kind === undefined) return false;
|
|
1325
|
+
const declared = new Set([...phase.read, ...phase.write]);
|
|
1326
|
+
return verificationInputPaths(phase.verification).some(
|
|
1327
|
+
(input) => !declared.has(input),
|
|
1328
|
+
);
|
|
1329
|
+
})
|
|
1330
|
+
) {
|
|
1331
|
+
return false;
|
|
1332
|
+
}
|
|
913
1333
|
const declared = {
|
|
914
1334
|
read: [...new Set(phases.flatMap((entry) => entry.read))],
|
|
915
1335
|
write: [...new Set(phases.flatMap((entry) => entry.write))],
|
|
@@ -919,6 +1339,22 @@ function validateTaskBoundary(
|
|
|
919
1339
|
);
|
|
920
1340
|
}
|
|
921
1341
|
|
|
1342
|
+
function normalizedTaskBoundary(value: TaskBoundary): TaskBoundary {
|
|
1343
|
+
const clone = structuredClone(value);
|
|
1344
|
+
clone.phases.red.verification = cloneVerificationContract(
|
|
1345
|
+
clone.phases.red.verification,
|
|
1346
|
+
);
|
|
1347
|
+
clone.phases.green.verification = cloneVerificationContract(
|
|
1348
|
+
clone.phases.green.verification,
|
|
1349
|
+
);
|
|
1350
|
+
if (clone.phases.refactor) {
|
|
1351
|
+
clone.phases.refactor.verification = cloneVerificationContract(
|
|
1352
|
+
clone.phases.refactor.verification,
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
return clone;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
922
1358
|
function validatePhaseAttemptShape(value: unknown): value is PhaseAttempt {
|
|
923
1359
|
if (
|
|
924
1360
|
!hasExactKeys(value, [
|
|
@@ -1026,7 +1462,12 @@ function validateImplementRunRequest(
|
|
|
1026
1462
|
}
|
|
1027
1463
|
return {
|
|
1028
1464
|
ok: true,
|
|
1029
|
-
value:
|
|
1465
|
+
value: {
|
|
1466
|
+
stage: "abel-implement",
|
|
1467
|
+
kind: "open-task",
|
|
1468
|
+
boundary: normalizedTaskBoundary(boundary),
|
|
1469
|
+
attempt: structuredClone(attempt),
|
|
1470
|
+
},
|
|
1030
1471
|
};
|
|
1031
1472
|
}
|
|
1032
1473
|
if (request.kind === "phase-attempt") {
|
package/src/patch.ts
CHANGED
|
@@ -48,6 +48,11 @@ async function git(
|
|
|
48
48
|
settled = true;
|
|
49
49
|
resolve(result);
|
|
50
50
|
};
|
|
51
|
+
const fail = (error: Error) => {
|
|
52
|
+
if (settled) return;
|
|
53
|
+
settled = true;
|
|
54
|
+
reject(error);
|
|
55
|
+
};
|
|
51
56
|
child.stdout.on("data", (b: Buffer) => stdout.push(b));
|
|
52
57
|
child.stderr.on("data", (b: Buffer) => stderr.push(b));
|
|
53
58
|
child.on("error", (error) => {
|
|
@@ -59,7 +64,7 @@ async function git(
|
|
|
59
64
|
cancelled: true,
|
|
60
65
|
});
|
|
61
66
|
} else {
|
|
62
|
-
|
|
67
|
+
fail(error);
|
|
63
68
|
}
|
|
64
69
|
});
|
|
65
70
|
child.on("close", (code) =>
|
|
@@ -70,6 +75,11 @@ async function git(
|
|
|
70
75
|
cancelled: signal?.aborted ?? false,
|
|
71
76
|
}),
|
|
72
77
|
);
|
|
78
|
+
child.stdin.on("error", (error: NodeJS.ErrnoException) => {
|
|
79
|
+
// A command may reject its input and exit before Node finishes writing it.
|
|
80
|
+
// Its exit status remains the authoritative command result.
|
|
81
|
+
if (error.code !== "EPIPE") fail(error);
|
|
82
|
+
});
|
|
73
83
|
child.stdin.end(input);
|
|
74
84
|
});
|
|
75
85
|
}
|
|
@@ -349,6 +359,8 @@ export async function applyRetainedPatch(input: {
|
|
|
349
359
|
return failure({ kind: preflight.kind, code: preflight.code });
|
|
350
360
|
case "environment":
|
|
351
361
|
return failure({ kind: preflight.kind, code: preflight.code });
|
|
362
|
+
case "verification-adapter":
|
|
363
|
+
return failure({ kind: preflight.kind, code: preflight.code });
|
|
352
364
|
case "approval-boundary":
|
|
353
365
|
return failure({ kind: preflight.kind, code: preflight.code });
|
|
354
366
|
case "cancelled":
|
package/src/result-store.ts
CHANGED
|
@@ -4,7 +4,10 @@ import type {
|
|
|
4
4
|
BaselineEntry,
|
|
5
5
|
VerificationContract,
|
|
6
6
|
} from "./candidate-preflight.ts";
|
|
7
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
cloneVerificationContract,
|
|
9
|
+
type ImplementationPhase,
|
|
10
|
+
} from "./contracts.ts";
|
|
8
11
|
import type { Bound, DirBound, FileBound } from "./file-snapshot.ts";
|
|
9
12
|
import { snapshotFiles } from "./file-snapshot.ts";
|
|
10
13
|
|
|
@@ -105,10 +108,7 @@ function cloneRetained(retained: RetainedResult): RetainedResult {
|
|
|
105
108
|
baseline: retained.baseline.map((entry) => ({ ...entry })),
|
|
106
109
|
};
|
|
107
110
|
if (retained.verification) {
|
|
108
|
-
clone.verification =
|
|
109
|
-
...retained.verification,
|
|
110
|
-
argv: [...retained.verification.argv],
|
|
111
|
-
};
|
|
111
|
+
clone.verification = cloneVerificationContract(retained.verification);
|
|
112
112
|
}
|
|
113
113
|
if (retained.packageManifest) {
|
|
114
114
|
clone.packageManifest = { ...retained.packageManifest };
|
|
@@ -147,10 +147,7 @@ export class ResultStore {
|
|
|
147
147
|
...(input.verification === undefined
|
|
148
148
|
? {}
|
|
149
149
|
: {
|
|
150
|
-
verification:
|
|
151
|
-
...input.verification,
|
|
152
|
-
argv: [...input.verification.argv],
|
|
153
|
-
},
|
|
150
|
+
verification: cloneVerificationContract(input.verification),
|
|
154
151
|
}),
|
|
155
152
|
...(input.packageManifest === undefined
|
|
156
153
|
? {}
|