@abelxiaoxing/cadence 1.3.0 → 1.4.1
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 +172 -13
- package/agents/design-explorer.md +17 -11
- package/agents/diagnosis-worker.md +9 -1
- package/agents/implementation-worker.md +3 -1
- package/config/plan-draft.example.json +122 -0
- package/config/plan-draft.multiple-tasks.example.json +345 -0
- package/package.json +9 -5
- package/prompts/abel-design.md +141 -38
- package/prompts/abel-diagnose.md +10 -1
- package/prompts/abel-implement.md +118 -20
- package/prompts/abel-init.md +9 -2
- package/src/activity-contracts.ts +82 -0
- package/src/apply-transaction.ts +16 -14
- package/src/artifact-store.ts +17 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +33 -12
- package/src/candidate-workspace.ts +74 -0
- package/src/canonical.ts +16 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +273 -0
- package/src/change-verification.ts +896 -0
- package/src/child-model.ts +129 -0
- package/src/child-session.ts +296 -328
- package/src/contracts.ts +325 -109
- package/src/control-contracts.ts +51 -1
- package/src/delivery-compiler.ts +241 -141
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +182 -15
- package/src/design-diagnostics.ts +150 -0
- package/src/design-journal.ts +216 -72
- package/src/durable-contracts.ts +210 -0
- package/src/durable-workflow.ts +963 -0
- package/src/evidence-draft.ts +122 -0
- package/src/implement-graph.ts +14 -16
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +341 -1405
- package/src/isolation-backend.ts +84 -23
- package/src/model-source.ts +22 -0
- package/src/package-candidate.ts +224 -0
- package/src/package-delivery.ts +486 -0
- package/src/package-verification.ts +509 -0
- package/src/package-workflow.ts +253 -0
- package/src/packet-runtime.ts +27 -48
- package/src/parent-provider.ts +86 -198
- package/src/phase-execution.ts +2248 -0
- package/src/pi-adapter.ts +19 -0
- package/src/plan-draft-summary.ts +146 -0
- package/src/plan-draft.ts +493 -0
- package/src/route-policy.ts +1 -11
- package/src/run-state.ts +4 -16
- package/src/run-store.ts +5 -144
- package/src/scoped-grep-worker.mjs +23 -0
- package/src/scoped-tools.ts +312 -116
- package/src/sqlite-schema.ts +171 -0
- package/src/storage-schema.ts +352 -0
- package/src/subagent-activity.ts +46 -67
- package/src/submit-tool.ts +40 -61
- package/src/task-ledger.ts +14 -55
- package/src/transport-budget.ts +32 -0
- package/src/verification-capability.ts +298 -102
- package/src/verification-environment.ts +213 -0
- package/src/verification-identity.ts +77 -0
- package/src/worker-broker.ts +134 -91
- package/src/workflow-engine.ts +28 -8285
- package/src/workflow-policy.ts +1388 -0
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +4086 -0
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-entry.mjs +58 -0
- package/src/workspace-io-worker.mjs +991 -0
- package/src/workspace-io.ts +84 -0
- package/src/workspace-store.ts +125 -19
- package/skills/abel-workflow/SKILL.md +0 -205
- package/src/empty-resource-loader.ts +0 -38
- package/src/parent-payload-bridge.ts +0 -523
package/src/index.ts
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
|
+
import { compareCanonicalStrings } from "./canonical.ts";
|
|
2
|
+
import { CHANGE_CONTRACT_SCHEMA } from "./change-contract-schema.ts";
|
|
3
|
+
import {
|
|
4
|
+
projectDesignDiagnostic,
|
|
5
|
+
type SafeDesignDiagnostic,
|
|
6
|
+
} from "./design-diagnostics.ts";
|
|
7
|
+
import { openPackageWorkflowService } from "./package-workflow.ts";
|
|
8
|
+
import { packageContext } from "./pi-adapter.ts";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
type PackageDeliverySourceOptions,
|
|
12
|
+
type PackageWorkflowDeliverySource,
|
|
13
|
+
packageDeliverySource,
|
|
14
|
+
} from "./package-delivery.ts";
|
|
15
|
+
export { executePackageVerification } from "./package-verification.ts";
|
|
16
|
+
|
|
1
17
|
// Private workflow orchestration extension. Registers the abel_dispatch tool
|
|
2
18
|
// on load, then keeps it inactive by default by removing only that name from
|
|
3
19
|
// the active set at session start. Eligible-stage activation is wired by the
|
|
4
20
|
// workflow routing (abel-design/implement/diagnose provenance) in the prompts
|
|
5
21
|
// integration; abel-init and ordinary prompts never activate dispatch.
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
lstatSync,
|
|
10
|
-
mkdirSync,
|
|
11
|
-
mkdtempSync,
|
|
12
|
-
readFileSync,
|
|
13
|
-
realpathSync,
|
|
14
|
-
rmSync,
|
|
15
|
-
} from "node:fs";
|
|
16
|
-
import { homedir } from "node:os";
|
|
17
|
-
import path, { dirname, join } from "node:path";
|
|
22
|
+
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
18
24
|
import { fileURLToPath } from "node:url";
|
|
19
25
|
import type {
|
|
20
26
|
AgentToolResult,
|
|
@@ -25,55 +31,28 @@ import type {
|
|
|
25
31
|
ToolRenderResultOptions,
|
|
26
32
|
} from "@earendil-works/pi-coding-agent";
|
|
27
33
|
import { type Activation, activateTool, deactivateTool } from "./activation.ts";
|
|
28
|
-
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
type AtomicVerificationContract,
|
|
32
|
-
type DesignEvidenceResult,
|
|
33
|
-
isValidRelativePath,
|
|
34
|
-
LIMITS,
|
|
35
|
-
type StructuredVerificationContract,
|
|
36
|
-
verificationSteps,
|
|
37
|
-
} from "./contracts.ts";
|
|
34
|
+
|
|
35
|
+
import type { DesignEvidenceResult } from "./contracts.ts";
|
|
38
36
|
import {
|
|
39
37
|
CONTROL_COMMAND_PARAMETERS,
|
|
40
38
|
canonicalizeControlCommandToolInput,
|
|
41
39
|
validateControlCommand,
|
|
42
40
|
} from "./control-contracts.ts";
|
|
43
41
|
import {
|
|
44
|
-
assessDeliveryTraceability,
|
|
45
|
-
compileImplementPlan,
|
|
46
42
|
DeliveryValidationError,
|
|
47
43
|
DesignPlanValidationError,
|
|
48
|
-
type GateApprovalProof,
|
|
49
|
-
parseGateAReceipt,
|
|
50
|
-
parseImplementPlan,
|
|
51
|
-
parseReadyReceipt,
|
|
52
44
|
} from "./delivery-compiler.ts";
|
|
53
45
|
import {
|
|
54
|
-
|
|
46
|
+
DesignControlValidationError,
|
|
55
47
|
DesignFinalizationError,
|
|
56
48
|
validateDesignControlRequest,
|
|
57
49
|
} from "./design-control.ts";
|
|
58
|
-
import { canonicalJson
|
|
59
|
-
import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
|
|
60
|
-
import {
|
|
61
|
-
inspectOpenSpecDelivery,
|
|
62
|
-
type OpenSpecDeliveryInspection,
|
|
63
|
-
} from "./openspec-cli.ts";
|
|
50
|
+
import { canonicalJson } from "./implement-graph.ts";
|
|
64
51
|
import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
|
|
65
|
-
|
|
66
|
-
import { runtimeForWorkerRoute } from "./parent-provider.ts";
|
|
67
|
-
import {
|
|
68
|
-
inspectRoutePolicy,
|
|
69
|
-
loadRoutePolicy,
|
|
70
|
-
type RoutePolicyResolution,
|
|
71
|
-
unavailableRoutePolicy,
|
|
72
|
-
type WorkerRoutePolicy,
|
|
73
|
-
} from "./route-policy.ts";
|
|
52
|
+
|
|
74
53
|
import { RunStoreFormatError, RunStoreMigrationError } from "./run-store.ts";
|
|
75
|
-
|
|
76
|
-
import {
|
|
54
|
+
|
|
55
|
+
import { StateRootError } from "./state-root.ts";
|
|
77
56
|
import {
|
|
78
57
|
ACTIVITY_DETAILS_KEY,
|
|
79
58
|
ActivityController,
|
|
@@ -81,17 +60,6 @@ import {
|
|
|
81
60
|
renderActivityResult,
|
|
82
61
|
type WorkflowActivityUpdate,
|
|
83
62
|
} from "./subagent-activity.ts";
|
|
84
|
-
import { classifyCandidateContextRequest } from "./submit-tool.ts";
|
|
85
|
-
import {
|
|
86
|
-
bindCurrentVerificationCapability,
|
|
87
|
-
isVerificationCapabilityCurrent,
|
|
88
|
-
type VerificationRunnerBinding,
|
|
89
|
-
} from "./verification-capability.ts";
|
|
90
|
-
import {
|
|
91
|
-
openDurableWorkflowEngine,
|
|
92
|
-
type WorkflowAvailableDelivery,
|
|
93
|
-
type WorkflowDeliverySource,
|
|
94
|
-
} from "./workflow-engine.ts";
|
|
95
63
|
|
|
96
64
|
export {
|
|
97
65
|
inspectOpenSpecDelivery,
|
|
@@ -293,9 +261,9 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
|
|
|
293
261
|
operationId: { type: "string" },
|
|
294
262
|
gate: { type: "string", enum: ["gate-a"] },
|
|
295
263
|
contract: {
|
|
296
|
-
type: "string",
|
|
264
|
+
anyOf: [{ type: "string" }, CHANGE_CONTRACT_SCHEMA],
|
|
297
265
|
description:
|
|
298
|
-
"
|
|
266
|
+
"ChangeContract: {goal, acceptance:[{id,statement,verification}], constraints:[{id,statement}], policy:{writeRoots,dependencies,verificationModes}}. Pass the example's changeContract object, not its whole plan. Legacy prose remains readable; code hashes authority.",
|
|
299
267
|
},
|
|
300
268
|
},
|
|
301
269
|
required: ["operation", "runId", "operationId", "gate", "contract"],
|
|
@@ -325,7 +293,7 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
|
|
|
325
293
|
content: {
|
|
326
294
|
type: "string",
|
|
327
295
|
maxLength: 16 * 1024 * 1024,
|
|
328
|
-
description:
|
|
296
|
+
description: `Exact bounded UTF-8 artifact content. For plan-draft.json, omit tracking, phase verificationInputs and relatedTests disposition to derive them. Read the complete example at ${join(PACKAGE_ROOT, "config/plan-draft.example.json")}.`,
|
|
329
297
|
},
|
|
330
298
|
},
|
|
331
299
|
required: ["operation", "runId", "operationId", "path", "content"],
|
|
@@ -363,11 +331,13 @@ function invokedPrompt(text: string): EligiblePrompt | undefined {
|
|
|
363
331
|
return ELIGIBLE_PROMPTS.find((candidate) => candidate === name);
|
|
364
332
|
}
|
|
365
333
|
|
|
366
|
-
|
|
334
|
+
type WorkflowPrompt = EligiblePrompt | "abel-init";
|
|
335
|
+
|
|
336
|
+
function promptMarker(name: WorkflowPrompt): string {
|
|
367
337
|
return `<!-- ABEL:PROMPT:${name} -->`;
|
|
368
338
|
}
|
|
369
339
|
|
|
370
|
-
function hasPackageProvenance(pi: ExtensionAPI, name:
|
|
340
|
+
function hasPackageProvenance(pi: ExtensionAPI, name: WorkflowPrompt): boolean {
|
|
371
341
|
const commands = pi
|
|
372
342
|
.getCommands()
|
|
373
343
|
.filter((candidate) => candidate.name === name);
|
|
@@ -382,7 +352,7 @@ function hasPackageProvenance(pi: ExtensionAPI, name: EligiblePrompt): boolean {
|
|
|
382
352
|
|
|
383
353
|
function hasExpandedPromptMarker(
|
|
384
354
|
prompt: string,
|
|
385
|
-
name:
|
|
355
|
+
name: WorkflowPrompt,
|
|
386
356
|
): boolean {
|
|
387
357
|
const requestEnd = prompt.lastIndexOf("</abel-request>");
|
|
388
358
|
if (requestEnd < 0) return false;
|
|
@@ -438,7 +408,7 @@ type SafeDesignFailure = {
|
|
|
438
408
|
kind: "design-control-failure";
|
|
439
409
|
operation: string;
|
|
440
410
|
code: string;
|
|
441
|
-
diagnostics:
|
|
411
|
+
diagnostics: readonly SafeDesignDiagnostic[];
|
|
442
412
|
};
|
|
443
413
|
|
|
444
414
|
type DesignFailureBoundary =
|
|
@@ -487,6 +457,13 @@ function classifyDesignFailure(
|
|
|
487
457
|
field?: string;
|
|
488
458
|
} {
|
|
489
459
|
const message = error instanceof Error ? error.message : "";
|
|
460
|
+
if (error instanceof DesignControlValidationError) {
|
|
461
|
+
return {
|
|
462
|
+
code: "invalid-design-control-request",
|
|
463
|
+
category: "validation",
|
|
464
|
+
retryable: false,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
490
467
|
if (error instanceof DesignPlanValidationError) {
|
|
491
468
|
return {
|
|
492
469
|
code: "design-plan-validation-invalid",
|
|
@@ -609,53 +586,11 @@ function safeDesignFailure(
|
|
|
609
586
|
: [];
|
|
610
587
|
const diagnostics = rawDiagnostics
|
|
611
588
|
.flatMap((candidate) => {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
return SAFE_DESIGN_ERROR_CODE.test(diagnosticCode)
|
|
615
|
-
? [{ code: diagnosticCode }]
|
|
616
|
-
: [];
|
|
617
|
-
}
|
|
618
|
-
if (
|
|
619
|
-
candidate === null ||
|
|
620
|
-
typeof candidate !== "object" ||
|
|
621
|
-
Array.isArray(candidate)
|
|
622
|
-
) {
|
|
623
|
-
return [];
|
|
624
|
-
}
|
|
625
|
-
const admitted = Object.fromEntries(
|
|
626
|
-
Object.entries(candidate as Record<string, unknown>)
|
|
627
|
-
.filter((entry): entry is [string, string | boolean] => {
|
|
628
|
-
if (entry[0] === "retryable") {
|
|
629
|
-
return typeof entry[1] === "boolean";
|
|
630
|
-
}
|
|
631
|
-
return (
|
|
632
|
-
[
|
|
633
|
-
"code",
|
|
634
|
-
"taskId",
|
|
635
|
-
"phase",
|
|
636
|
-
"field",
|
|
637
|
-
"category",
|
|
638
|
-
"owner",
|
|
639
|
-
"verificationId",
|
|
640
|
-
"outputId",
|
|
641
|
-
"dependencyTaskId",
|
|
642
|
-
"producerTaskId",
|
|
643
|
-
"producerPhase",
|
|
644
|
-
"command",
|
|
645
|
-
"reason",
|
|
646
|
-
"systemCode",
|
|
647
|
-
"exitCode",
|
|
648
|
-
].includes(entry[0]) &&
|
|
649
|
-
typeof entry[1] === "string" &&
|
|
650
|
-
SAFE_DESIGN_ERROR_CODE.test(entry[1])
|
|
651
|
-
);
|
|
652
|
-
})
|
|
653
|
-
.sort(([left], [right]) => left.localeCompare(right)),
|
|
654
|
-
);
|
|
655
|
-
return typeof admitted.code === "string" ? [admitted] : [];
|
|
589
|
+
const diagnostic = projectDesignDiagnostic(candidate);
|
|
590
|
+
return diagnostic ? [diagnostic] : [];
|
|
656
591
|
})
|
|
657
592
|
.sort((left, right) =>
|
|
658
|
-
canonicalJson(left)
|
|
593
|
+
compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
|
|
659
594
|
)
|
|
660
595
|
.filter(
|
|
661
596
|
(candidate, index, values) =>
|
|
@@ -687,6 +622,11 @@ export interface WorkflowControlEngine {
|
|
|
687
622
|
onActivity?: (event: WorkflowActivityUpdate) => void,
|
|
688
623
|
): Promise<Record<string, unknown>>;
|
|
689
624
|
executeDesign?(request: unknown): Promise<Record<string, unknown>>;
|
|
625
|
+
executeAmendment?(
|
|
626
|
+
change: string,
|
|
627
|
+
batchId: string,
|
|
628
|
+
request: unknown,
|
|
629
|
+
): Promise<Record<string, unknown>>;
|
|
690
630
|
assertDesignRun?(runId: string): void;
|
|
691
631
|
recordDesignEvidence?(input: {
|
|
692
632
|
runId: string;
|
|
@@ -697,1240 +637,22 @@ export interface WorkflowControlEngine {
|
|
|
697
637
|
|
|
698
638
|
export type WorkflowControlEngineFactory = (
|
|
699
639
|
ctx: ExtensionContext,
|
|
700
|
-
parentPayloadBridge: ParentPayloadBridge,
|
|
701
640
|
) => WorkflowControlEngine | Promise<WorkflowControlEngine>;
|
|
702
641
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
export interface PackageDeliverySourceOptions {
|
|
706
|
-
inspectOpenSpec?: (
|
|
707
|
-
consumerRoot: string,
|
|
708
|
-
change: string,
|
|
709
|
-
) => Promise<OpenSpecDeliveryInspection>;
|
|
710
|
-
verifyGateProof?: (input: {
|
|
711
|
-
change: string;
|
|
712
|
-
gate: "gate-a" | "gate-b";
|
|
713
|
-
proof: GateApprovalProof;
|
|
714
|
-
}) => boolean;
|
|
715
|
-
verifyFinalizedDelivery?: (input: {
|
|
716
|
-
change: string;
|
|
717
|
-
deliveryRevision: number;
|
|
718
|
-
receiptHash: string;
|
|
719
|
-
gateA: GateApprovalProof;
|
|
720
|
-
gateB: GateApprovalProof;
|
|
721
|
-
planCanonicalHash: string;
|
|
722
|
-
}) => boolean;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
export interface PackageWorkflowDeliverySource extends WorkflowDeliverySource {
|
|
726
|
-
discoverLatest(input: {
|
|
727
|
-
stage: "abel-implement";
|
|
728
|
-
change: string;
|
|
729
|
-
}): Promise<WorkflowAvailableDelivery | undefined>;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function sha256(value: Uint8Array | string): string {
|
|
733
|
-
return createHash("sha256").update(value).digest("hex");
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function proofVerified(verify: () => boolean): boolean {
|
|
737
|
-
try {
|
|
738
|
-
return verify() === true;
|
|
739
|
-
} catch {
|
|
740
|
-
return false;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function normalizedTrackingArtifact(
|
|
745
|
-
bytes: Uint8Array,
|
|
746
|
-
taskIds: readonly string[],
|
|
747
|
-
): Buffer | undefined {
|
|
748
|
-
let text: string;
|
|
749
|
-
try {
|
|
750
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
751
|
-
} catch {
|
|
752
|
-
return undefined;
|
|
753
|
-
}
|
|
754
|
-
const identities = taskIds.map((taskId) => ({
|
|
755
|
-
taskId,
|
|
756
|
-
pattern: new RegExp(
|
|
757
|
-
`(?:^|[^A-Za-z0-9._:-])${taskId.replace(
|
|
758
|
-
/[.*+?^${}()|[\]\\]/gu,
|
|
759
|
-
"\\$&",
|
|
760
|
-
)}(?![A-Za-z0-9._:-])`,
|
|
761
|
-
"u",
|
|
762
|
-
),
|
|
763
|
-
matches: 0,
|
|
764
|
-
}));
|
|
765
|
-
const normalized = text
|
|
766
|
-
.split(/(?<=\n)/u)
|
|
767
|
-
.map((segment) => {
|
|
768
|
-
const line = segment.endsWith("\n") ? segment.slice(0, -1) : segment;
|
|
769
|
-
if (!/^\s*-\s+\[[ xX]\]/u.test(line)) return segment;
|
|
770
|
-
const matching = identities.filter(({ pattern }) => pattern.test(line));
|
|
771
|
-
if (matching.length !== 1) return segment;
|
|
772
|
-
matching[0].matches += 1;
|
|
773
|
-
return segment.replace(/^(\s*-\s+)\[[xX]\]/u, "$1[ ]");
|
|
774
|
-
})
|
|
775
|
-
.join("");
|
|
776
|
-
if (identities.some(({ matches }) => matches !== 1)) return undefined;
|
|
777
|
-
return Buffer.from(normalized, "utf8");
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function readPackageDeliveryFile(
|
|
781
|
-
root: string,
|
|
782
|
-
relative: string,
|
|
783
|
-
maximumBytes = PACKAGE_DELIVERY_MAX_BYTES,
|
|
784
|
-
): Buffer {
|
|
785
|
-
const observation = observeSafePath(root, relative);
|
|
786
|
-
if (observation.kind !== "file") {
|
|
787
|
-
throw new Error("delivery-file-unavailable");
|
|
788
|
-
}
|
|
789
|
-
const target = path.join(root, ...relative.split("/"));
|
|
790
|
-
const stat = lstatSync(target);
|
|
791
|
-
if (stat.size < 1 || stat.size > maximumBytes) {
|
|
792
|
-
throw new Error("delivery-file-size-invalid");
|
|
793
|
-
}
|
|
794
|
-
return readFileSync(target);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
export function packageDeliverySource(
|
|
798
|
-
consumerRoot: string,
|
|
799
|
-
options: PackageDeliverySourceOptions = {},
|
|
800
|
-
): PackageWorkflowDeliverySource {
|
|
801
|
-
const inspect = options.inspectOpenSpec ?? inspectOpenSpecDelivery;
|
|
802
|
-
const verifyGateProof = options.verifyGateProof;
|
|
803
|
-
const verifyFinalizedDelivery = options.verifyFinalizedDelivery;
|
|
804
|
-
return {
|
|
805
|
-
async discoverLatest(input) {
|
|
806
|
-
if (
|
|
807
|
-
input.stage !== "abel-implement" ||
|
|
808
|
-
!verifyGateProof ||
|
|
809
|
-
!verifyFinalizedDelivery
|
|
810
|
-
) {
|
|
811
|
-
return undefined;
|
|
812
|
-
}
|
|
813
|
-
try {
|
|
814
|
-
const changeRoot = `openspec/changes/${input.change}`;
|
|
815
|
-
const receiptBytes = readPackageDeliveryFile(
|
|
816
|
-
consumerRoot,
|
|
817
|
-
`${changeRoot}/ready.yaml`,
|
|
818
|
-
4 * 1024 * 1024,
|
|
819
|
-
);
|
|
820
|
-
const receipt = parseReadyReceipt(receiptBytes);
|
|
821
|
-
if (receipt.change !== input.change) return undefined;
|
|
822
|
-
const gateABytes = readPackageDeliveryFile(
|
|
823
|
-
consumerRoot,
|
|
824
|
-
`${changeRoot}/${receipt.approvals.gateA.path}`,
|
|
825
|
-
4 * 1024 * 1024,
|
|
826
|
-
);
|
|
827
|
-
if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
|
|
828
|
-
return undefined;
|
|
829
|
-
}
|
|
830
|
-
const gateA = parseGateAReceipt(gateABytes);
|
|
831
|
-
if (
|
|
832
|
-
gateA.change !== receipt.change ||
|
|
833
|
-
gateA.schema !== receipt.schema ||
|
|
834
|
-
!proofVerified(() =>
|
|
835
|
-
verifyGateProof({
|
|
836
|
-
change: input.change,
|
|
837
|
-
gate: "gate-a",
|
|
838
|
-
proof: gateA.approval,
|
|
839
|
-
}),
|
|
840
|
-
) ||
|
|
841
|
-
!proofVerified(() =>
|
|
842
|
-
verifyGateProof({
|
|
843
|
-
change: input.change,
|
|
844
|
-
gate: "gate-b",
|
|
845
|
-
proof: receipt.approvals.gateB,
|
|
846
|
-
}),
|
|
847
|
-
)
|
|
848
|
-
) {
|
|
849
|
-
return undefined;
|
|
850
|
-
}
|
|
851
|
-
const receiptHash = sha256(receiptBytes);
|
|
852
|
-
if (
|
|
853
|
-
!proofVerified(() =>
|
|
854
|
-
verifyFinalizedDelivery({
|
|
855
|
-
change: input.change,
|
|
856
|
-
deliveryRevision: receipt.deliveryRevision,
|
|
857
|
-
receiptHash,
|
|
858
|
-
gateA: gateA.approval,
|
|
859
|
-
gateB: receipt.approvals.gateB,
|
|
860
|
-
planCanonicalHash: receipt.plan.canonicalHash,
|
|
861
|
-
}),
|
|
862
|
-
)
|
|
863
|
-
) {
|
|
864
|
-
return undefined;
|
|
865
|
-
}
|
|
866
|
-
return {
|
|
867
|
-
deliveryRevision: receipt.deliveryRevision,
|
|
868
|
-
receiptHash,
|
|
869
|
-
};
|
|
870
|
-
} catch {
|
|
871
|
-
return undefined;
|
|
872
|
-
}
|
|
873
|
-
},
|
|
874
|
-
async load(input) {
|
|
875
|
-
if (input.stage !== "abel-implement") {
|
|
876
|
-
throw new DeliveryValidationError(["delivery-stage-invalid"]);
|
|
877
|
-
}
|
|
878
|
-
const changeRoot = `openspec/changes/${input.change}`;
|
|
879
|
-
const receiptRelative = `${changeRoot}/ready.yaml`;
|
|
880
|
-
const planRelative = `${changeRoot}/implement-plan.json`;
|
|
881
|
-
let receiptBytes: Buffer;
|
|
882
|
-
try {
|
|
883
|
-
receiptBytes = readPackageDeliveryFile(
|
|
884
|
-
consumerRoot,
|
|
885
|
-
receiptRelative,
|
|
886
|
-
4 * 1024 * 1024,
|
|
887
|
-
);
|
|
888
|
-
} catch {
|
|
889
|
-
throw new DeliveryValidationError(["delivery-receipt-unavailable"]);
|
|
890
|
-
}
|
|
891
|
-
let receipt: ReturnType<typeof parseReadyReceipt>;
|
|
892
|
-
try {
|
|
893
|
-
receipt = parseReadyReceipt(receiptBytes);
|
|
894
|
-
} catch {
|
|
895
|
-
throw new DeliveryValidationError([
|
|
896
|
-
"delivery-receipt-invalid",
|
|
897
|
-
"delivery-recompile-required",
|
|
898
|
-
]);
|
|
899
|
-
}
|
|
900
|
-
const diagnostics = new Set<string>();
|
|
901
|
-
if (receipt.change !== input.change) {
|
|
902
|
-
diagnostics.add("delivery-change-mismatch");
|
|
903
|
-
}
|
|
904
|
-
const receiptHash = sha256(receiptBytes);
|
|
905
|
-
if (
|
|
906
|
-
(input.deliveryRevision !== undefined &&
|
|
907
|
-
input.deliveryRevision !== receipt.deliveryRevision) ||
|
|
908
|
-
(input.receiptHash !== undefined && input.receiptHash !== receiptHash)
|
|
909
|
-
) {
|
|
910
|
-
diagnostics.add("delivery-revision-mismatch");
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
let inspection: OpenSpecDeliveryInspection | undefined;
|
|
914
|
-
try {
|
|
915
|
-
inspection = await inspect(consumerRoot, input.change);
|
|
916
|
-
} catch {
|
|
917
|
-
diagnostics.add("delivery-openspec-unavailable");
|
|
918
|
-
}
|
|
919
|
-
if (inspection) {
|
|
920
|
-
if (!inspection.strictValid) {
|
|
921
|
-
diagnostics.add("delivery-openspec-strict-invalid");
|
|
922
|
-
}
|
|
923
|
-
if (!inspection.planningComplete) {
|
|
924
|
-
diagnostics.add("delivery-openspec-incomplete");
|
|
925
|
-
}
|
|
926
|
-
if (inspection.schema !== receipt.schema) {
|
|
927
|
-
diagnostics.add("delivery-schema-mismatch");
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
let gateABytes: Buffer | undefined;
|
|
932
|
-
let gateA: ReturnType<typeof parseGateAReceipt> | undefined;
|
|
933
|
-
try {
|
|
934
|
-
gateABytes = readPackageDeliveryFile(
|
|
935
|
-
consumerRoot,
|
|
936
|
-
`${changeRoot}/${receipt.approvals.gateA.path}`,
|
|
937
|
-
4 * 1024 * 1024,
|
|
938
|
-
);
|
|
939
|
-
if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
|
|
940
|
-
diagnostics.add("delivery-gate-a-hash-mismatch");
|
|
941
|
-
}
|
|
942
|
-
gateA = parseGateAReceipt(gateABytes);
|
|
943
|
-
if (
|
|
944
|
-
gateA.change !== receipt.change ||
|
|
945
|
-
gateA.schema !== receipt.schema
|
|
946
|
-
) {
|
|
947
|
-
diagnostics.add("delivery-gate-a-binding-mismatch");
|
|
948
|
-
}
|
|
949
|
-
} catch {
|
|
950
|
-
diagnostics.add("delivery-gate-a-invalid");
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
const artifactBytes = new Map<string, Buffer>();
|
|
954
|
-
const artifactHashes = new Map(
|
|
955
|
-
receipt.artifacts.map((artifact) => [
|
|
956
|
-
artifact.path,
|
|
957
|
-
artifact.rawSha256,
|
|
958
|
-
]),
|
|
959
|
-
);
|
|
960
|
-
for (const artifact of receipt.artifacts) {
|
|
961
|
-
try {
|
|
962
|
-
const bytes = readPackageDeliveryFile(
|
|
963
|
-
consumerRoot,
|
|
964
|
-
`${changeRoot}/${artifact.path}`,
|
|
965
|
-
);
|
|
966
|
-
artifactBytes.set(artifact.path, bytes);
|
|
967
|
-
if (
|
|
968
|
-
artifact.path !== receipt.traceability.taskPath &&
|
|
969
|
-
sha256(bytes) !== artifact.rawSha256
|
|
970
|
-
) {
|
|
971
|
-
diagnostics.add(`delivery-artifact-hash-mismatch:${artifact.path}`);
|
|
972
|
-
}
|
|
973
|
-
} catch {
|
|
974
|
-
diagnostics.add(`delivery-artifact-unavailable:${artifact.path}`);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
if (inspection) {
|
|
978
|
-
const covered = new Set(receipt.artifacts.map((entry) => entry.path));
|
|
979
|
-
const expected = new Set(inspection.artifactPaths);
|
|
980
|
-
for (const relative of expected) {
|
|
981
|
-
if (!isValidRelativePath(relative) || !covered.has(relative)) {
|
|
982
|
-
diagnostics.add(`delivery-artifact-unbound:${relative}`);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
for (const relative of covered) {
|
|
986
|
-
if (!expected.has(relative)) {
|
|
987
|
-
diagnostics.add(`delivery-artifact-not-in-openspec:${relative}`);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
if (gateA) {
|
|
992
|
-
if (!verifyGateProof) {
|
|
993
|
-
diagnostics.add("delivery-gate-proof-verifier-unavailable");
|
|
994
|
-
} else if (
|
|
995
|
-
!proofVerified(() =>
|
|
996
|
-
verifyGateProof({
|
|
997
|
-
change: input.change,
|
|
998
|
-
gate: "gate-a",
|
|
999
|
-
proof: gateA.approval,
|
|
1000
|
-
}),
|
|
1001
|
-
)
|
|
1002
|
-
) {
|
|
1003
|
-
diagnostics.add("delivery-gate-a-proof-invalid");
|
|
1004
|
-
}
|
|
1005
|
-
for (const artifact of gateA.artifacts) {
|
|
1006
|
-
if (artifactHashes.get(artifact.path) !== artifact.rawSha256) {
|
|
1007
|
-
diagnostics.add(
|
|
1008
|
-
`delivery-gate-a-artifact-mismatch:${artifact.path}`,
|
|
1009
|
-
);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
if (
|
|
1014
|
-
verifyGateProof &&
|
|
1015
|
-
!proofVerified(() =>
|
|
1016
|
-
verifyGateProof({
|
|
1017
|
-
change: input.change,
|
|
1018
|
-
gate: "gate-b",
|
|
1019
|
-
proof: receipt.approvals.gateB,
|
|
1020
|
-
}),
|
|
1021
|
-
)
|
|
1022
|
-
) {
|
|
1023
|
-
diagnostics.add("delivery-gate-b-proof-invalid");
|
|
1024
|
-
}
|
|
1025
|
-
if (gateA) {
|
|
1026
|
-
if (!verifyFinalizedDelivery) {
|
|
1027
|
-
diagnostics.add("delivery-finalization-verifier-unavailable");
|
|
1028
|
-
} else if (
|
|
1029
|
-
!proofVerified(() =>
|
|
1030
|
-
verifyFinalizedDelivery({
|
|
1031
|
-
change: input.change,
|
|
1032
|
-
deliveryRevision: receipt.deliveryRevision,
|
|
1033
|
-
receiptHash,
|
|
1034
|
-
gateA: gateA.approval,
|
|
1035
|
-
gateB: receipt.approvals.gateB,
|
|
1036
|
-
planCanonicalHash: receipt.plan.canonicalHash,
|
|
1037
|
-
}),
|
|
1038
|
-
)
|
|
1039
|
-
) {
|
|
1040
|
-
diagnostics.add("delivery-finalization-proof-invalid");
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
let plan: ReturnType<typeof parseImplementPlan> | undefined;
|
|
1045
|
-
let planBytes: Buffer | undefined;
|
|
1046
|
-
try {
|
|
1047
|
-
planBytes = readPackageDeliveryFile(consumerRoot, planRelative);
|
|
1048
|
-
plan = parseImplementPlan(planBytes);
|
|
1049
|
-
} catch {
|
|
1050
|
-
diagnostics.add("delivery-plan-invalid");
|
|
1051
|
-
}
|
|
1052
|
-
if (plan && planBytes) {
|
|
1053
|
-
if (plan.changeId !== input.change) {
|
|
1054
|
-
diagnostics.add("delivery-plan-change-mismatch");
|
|
1055
|
-
}
|
|
1056
|
-
let compiled: ReturnType<typeof compileImplementPlan> | undefined;
|
|
1057
|
-
try {
|
|
1058
|
-
compiled = compileImplementPlan(plan, { consumerRoot });
|
|
1059
|
-
} catch {
|
|
1060
|
-
diagnostics.add("delivery-verification-closure-invalid");
|
|
1061
|
-
}
|
|
1062
|
-
if (
|
|
1063
|
-
receipt.plan.path !== "implement-plan.json" ||
|
|
1064
|
-
receipt.plan.rawSha256 !== sha256(planBytes) ||
|
|
1065
|
-
receipt.plan.canonicalHash !== hashCanonicalValue(plan)
|
|
1066
|
-
) {
|
|
1067
|
-
diagnostics.add("delivery-plan-binding-invalid");
|
|
1068
|
-
}
|
|
1069
|
-
if (
|
|
1070
|
-
compiled &&
|
|
1071
|
-
canonicalJson(compiled.closure) !==
|
|
1072
|
-
canonicalJson(receipt.verificationClosure)
|
|
1073
|
-
) {
|
|
1074
|
-
diagnostics.add("delivery-verification-closure-mismatch");
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
if (plan) {
|
|
1079
|
-
const tasksBytes = artifactBytes.get(receipt.traceability.taskPath);
|
|
1080
|
-
const expectedTasksHash = artifactHashes.get(
|
|
1081
|
-
receipt.traceability.taskPath,
|
|
1082
|
-
);
|
|
1083
|
-
if (
|
|
1084
|
-
tasksBytes &&
|
|
1085
|
-
expectedTasksHash &&
|
|
1086
|
-
sha256(tasksBytes) !== expectedTasksHash
|
|
1087
|
-
) {
|
|
1088
|
-
const normalized = normalizedTrackingArtifact(
|
|
1089
|
-
tasksBytes,
|
|
1090
|
-
plan.tracking.taskIds,
|
|
1091
|
-
);
|
|
1092
|
-
if (!normalized || sha256(normalized) !== expectedTasksHash) {
|
|
1093
|
-
diagnostics.add(
|
|
1094
|
-
`delivery-artifact-hash-mismatch:${receipt.traceability.taskPath}`,
|
|
1095
|
-
);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
const specs = [...artifactBytes]
|
|
1099
|
-
.filter(
|
|
1100
|
-
([relative]) =>
|
|
1101
|
-
relative.startsWith("specs/") && relative.endsWith("/spec.md"),
|
|
1102
|
-
)
|
|
1103
|
-
.flatMap(([relative, bytes]) => {
|
|
1104
|
-
try {
|
|
1105
|
-
return [
|
|
1106
|
-
{
|
|
1107
|
-
path: relative,
|
|
1108
|
-
text: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
1109
|
-
},
|
|
1110
|
-
];
|
|
1111
|
-
} catch {
|
|
1112
|
-
diagnostics.add(`delivery-artifact-encoding-invalid:${relative}`);
|
|
1113
|
-
return [];
|
|
1114
|
-
}
|
|
1115
|
-
});
|
|
1116
|
-
if (!tasksBytes || specs.length === 0) {
|
|
1117
|
-
diagnostics.add("delivery-traceability-input-unavailable");
|
|
1118
|
-
} else {
|
|
1119
|
-
try {
|
|
1120
|
-
const traceability = assessDeliveryTraceability({
|
|
1121
|
-
tasksMarkdown: new TextDecoder("utf-8", { fatal: true }).decode(
|
|
1122
|
-
tasksBytes,
|
|
1123
|
-
),
|
|
1124
|
-
specs,
|
|
1125
|
-
plan,
|
|
1126
|
-
});
|
|
1127
|
-
if (!traceability.ok) {
|
|
1128
|
-
for (const diagnostic of traceability.diagnostics) {
|
|
1129
|
-
diagnostics.add(diagnostic);
|
|
1130
|
-
}
|
|
1131
|
-
} else if (
|
|
1132
|
-
canonicalJson(traceability.value) !==
|
|
1133
|
-
canonicalJson(receipt.traceability)
|
|
1134
|
-
) {
|
|
1135
|
-
diagnostics.add("delivery-traceability-binding-mismatch");
|
|
1136
|
-
}
|
|
1137
|
-
} catch {
|
|
1138
|
-
diagnostics.add("delivery-traceability-invalid");
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
if (!plan || !gateA || diagnostics.size > 0) {
|
|
1143
|
-
throw new DeliveryValidationError([...diagnostics]);
|
|
1144
|
-
}
|
|
1145
|
-
return {
|
|
1146
|
-
gate: "gate-b",
|
|
1147
|
-
revision: receipt.deliveryRevision,
|
|
1148
|
-
receiptHash,
|
|
1149
|
-
plan,
|
|
1150
|
-
approvalProofs: {
|
|
1151
|
-
gateA: structuredClone(gateA.approval),
|
|
1152
|
-
gateB: structuredClone(receipt.approvals.gateB),
|
|
1153
|
-
},
|
|
1154
|
-
};
|
|
1155
|
-
},
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
function packageScriptInvocation(
|
|
1160
|
-
packageManager: string,
|
|
1161
|
-
script: string,
|
|
1162
|
-
args: string[],
|
|
1163
|
-
): string[] {
|
|
1164
|
-
return packageManager === "npm" || packageManager === "pnpm"
|
|
1165
|
-
? ["run", script, "--", ...args]
|
|
1166
|
-
: ["run", script, ...args];
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
function bindingFor(
|
|
1170
|
-
bindings: readonly VerificationRunnerBinding[],
|
|
1171
|
-
command: string,
|
|
1172
|
-
): VerificationRunnerBinding | undefined {
|
|
1173
|
-
return bindings.find((binding) => binding.command === command);
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
function prepareRunnerBindings(
|
|
1177
|
-
root: string,
|
|
1178
|
-
bindings: readonly VerificationRunnerBinding[],
|
|
1179
|
-
): {
|
|
1180
|
-
bindings: VerificationRunnerBinding[];
|
|
1181
|
-
mounts: Array<{ source: string; target: string }>;
|
|
1182
|
-
cleanupRoot?: string;
|
|
1183
|
-
} {
|
|
1184
|
-
if (!bindings.some((binding) => binding.mountSource)) {
|
|
1185
|
-
return {
|
|
1186
|
-
bindings: bindings.map((binding) => ({
|
|
1187
|
-
...binding,
|
|
1188
|
-
...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
|
|
1189
|
-
})),
|
|
1190
|
-
mounts: [],
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1193
|
-
const cleanupRoot = mkdtempSync(path.join(root, ".cadence-runners-"));
|
|
1194
|
-
try {
|
|
1195
|
-
const mounted = new Map<
|
|
1196
|
-
string,
|
|
1197
|
-
{ source: string; target: string; sandboxTarget: string }
|
|
1198
|
-
>();
|
|
1199
|
-
const prepared = bindings.map((binding) => {
|
|
1200
|
-
if (!binding.mountSource) return { ...binding };
|
|
1201
|
-
const source = path.resolve(binding.mountSource);
|
|
1202
|
-
const sourceStat = lstatSync(source);
|
|
1203
|
-
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
|
|
1204
|
-
throw new Error("runner-mount-unavailable");
|
|
1205
|
-
}
|
|
1206
|
-
let mount = mounted.get(source);
|
|
1207
|
-
if (!mount) {
|
|
1208
|
-
const target = path.join(cleanupRoot, String(mounted.size));
|
|
1209
|
-
mkdirSync(target, { mode: 0o700 });
|
|
1210
|
-
const sandboxRelative = path
|
|
1211
|
-
.relative(root, target)
|
|
1212
|
-
.split(path.sep)
|
|
1213
|
-
.join("/");
|
|
1214
|
-
mount = {
|
|
1215
|
-
source,
|
|
1216
|
-
target,
|
|
1217
|
-
sandboxTarget: `/workspace/${sandboxRelative}`,
|
|
1218
|
-
};
|
|
1219
|
-
mounted.set(source, mount);
|
|
1220
|
-
}
|
|
1221
|
-
const executable = realpathSync(binding.executablePath);
|
|
1222
|
-
const relative = path.relative(source, executable);
|
|
1223
|
-
if (
|
|
1224
|
-
relative === "" ||
|
|
1225
|
-
relative.startsWith("..") ||
|
|
1226
|
-
path.isAbsolute(relative)
|
|
1227
|
-
) {
|
|
1228
|
-
throw new Error("runner-mount-unavailable");
|
|
1229
|
-
}
|
|
1230
|
-
return {
|
|
1231
|
-
command: binding.command,
|
|
1232
|
-
executablePath: path.posix.join(
|
|
1233
|
-
mount.sandboxTarget,
|
|
1234
|
-
...relative.split(path.sep),
|
|
1235
|
-
),
|
|
1236
|
-
...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
|
|
1237
|
-
};
|
|
1238
|
-
});
|
|
1239
|
-
return {
|
|
1240
|
-
bindings: prepared,
|
|
1241
|
-
mounts: [...mounted.values()].map(({ source, sandboxTarget }) => ({
|
|
1242
|
-
source,
|
|
1243
|
-
target: sandboxTarget,
|
|
1244
|
-
})),
|
|
1245
|
-
cleanupRoot,
|
|
1246
|
-
};
|
|
1247
|
-
} catch (error) {
|
|
1248
|
-
rmSync(cleanupRoot, { recursive: true, force: true });
|
|
1249
|
-
throw error;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
function verificationInvocation(
|
|
1254
|
-
step: AtomicVerificationContract,
|
|
1255
|
-
bindings: readonly VerificationRunnerBinding[],
|
|
1256
|
-
): { executable: string; args: string[] } | undefined {
|
|
1257
|
-
const reporter = step.kind === "vitest" ? ["--reporter=json"] : [];
|
|
1258
|
-
if (step.kind === "package-script") {
|
|
1259
|
-
const binding = bindingFor(bindings, step.packageManager);
|
|
1260
|
-
return binding
|
|
1261
|
-
? {
|
|
1262
|
-
executable: binding.executablePath,
|
|
1263
|
-
args: [
|
|
1264
|
-
...(binding.fixedArgs ?? []),
|
|
1265
|
-
...packageScriptInvocation(
|
|
1266
|
-
step.packageManager,
|
|
1267
|
-
step.script,
|
|
1268
|
-
step.args,
|
|
1269
|
-
),
|
|
1270
|
-
],
|
|
1271
|
-
}
|
|
1272
|
-
: undefined;
|
|
1273
|
-
}
|
|
1274
|
-
const runner = step.runner;
|
|
1275
|
-
if (runner.kind === "package-script") {
|
|
1276
|
-
const binding = bindingFor(bindings, runner.packageManager);
|
|
1277
|
-
const args =
|
|
1278
|
-
step.kind === "vitest"
|
|
1279
|
-
? [...step.testFiles, ...step.args, ...reporter]
|
|
1280
|
-
: step.args;
|
|
1281
|
-
return binding
|
|
1282
|
-
? {
|
|
1283
|
-
executable: binding.executablePath,
|
|
1284
|
-
args: [
|
|
1285
|
-
...(binding.fixedArgs ?? []),
|
|
1286
|
-
...packageScriptInvocation(
|
|
1287
|
-
runner.packageManager,
|
|
1288
|
-
runner.script,
|
|
1289
|
-
args,
|
|
1290
|
-
),
|
|
1291
|
-
],
|
|
1292
|
-
}
|
|
1293
|
-
: undefined;
|
|
1294
|
-
}
|
|
1295
|
-
if (runner.kind === "local-binary") {
|
|
1296
|
-
const binding = bindingFor(bindings, runner.executable);
|
|
1297
|
-
return {
|
|
1298
|
-
executable:
|
|
1299
|
-
binding?.executablePath ??
|
|
1300
|
-
`/workspace/node_modules/.bin/${runner.executable}`,
|
|
1301
|
-
args: [
|
|
1302
|
-
...(binding?.fixedArgs ?? []),
|
|
1303
|
-
...(step.kind === "vitest"
|
|
1304
|
-
? [...step.testFiles, ...step.args, ...reporter]
|
|
1305
|
-
: step.args),
|
|
1306
|
-
],
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
const command = runner.kind === "node" ? "node" : "npx";
|
|
1310
|
-
const binding = bindingFor(bindings, command);
|
|
1311
|
-
if (!binding) return undefined;
|
|
1312
|
-
return {
|
|
1313
|
-
executable: binding.executablePath,
|
|
1314
|
-
args:
|
|
1315
|
-
runner.kind === "node"
|
|
1316
|
-
? [...(binding.fixedArgs ?? []), runner.script, ...step.args]
|
|
1317
|
-
: [
|
|
1318
|
-
...(binding.fixedArgs ?? []),
|
|
1319
|
-
"--no-install",
|
|
1320
|
-
runner.executable,
|
|
1321
|
-
...(step.kind === "vitest" ? step.testFiles : []),
|
|
1322
|
-
...step.args,
|
|
1323
|
-
...reporter,
|
|
1324
|
-
],
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
function vitestReport(stdout: string):
|
|
1329
|
-
| {
|
|
1330
|
-
total: number;
|
|
1331
|
-
failed: number;
|
|
1332
|
-
success: boolean;
|
|
1333
|
-
failures: string[];
|
|
1334
|
-
failedText: string[];
|
|
1335
|
-
}
|
|
1336
|
-
| undefined {
|
|
1337
|
-
let value: unknown;
|
|
1338
|
-
try {
|
|
1339
|
-
value = JSON.parse(stdout);
|
|
1340
|
-
} catch {
|
|
1341
|
-
return undefined;
|
|
1342
|
-
}
|
|
1343
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1344
|
-
return undefined;
|
|
1345
|
-
}
|
|
1346
|
-
const report = value as Record<string, unknown>;
|
|
1347
|
-
if (
|
|
1348
|
-
!Number.isSafeInteger(report.numTotalTests) ||
|
|
1349
|
-
!Number.isSafeInteger(report.numFailedTests) ||
|
|
1350
|
-
typeof report.success !== "boolean"
|
|
1351
|
-
) {
|
|
1352
|
-
return undefined;
|
|
1353
|
-
}
|
|
1354
|
-
const failedAssertions = (
|
|
1355
|
-
Array.isArray(report.testResults) ? report.testResults : []
|
|
1356
|
-
).flatMap((suite): Array<{ identity: string; text: string[] }> => {
|
|
1357
|
-
if (!suite || typeof suite !== "object" || Array.isArray(suite)) return [];
|
|
1358
|
-
const record = suite as Record<string, unknown>;
|
|
1359
|
-
const suiteName =
|
|
1360
|
-
typeof record.name === "string"
|
|
1361
|
-
? record.name.replace(/^\/workspace\//u, "")
|
|
1362
|
-
: "unknown-suite";
|
|
1363
|
-
if (!Array.isArray(record.assertionResults)) return [];
|
|
1364
|
-
return record.assertionResults.flatMap(
|
|
1365
|
-
(assertion): Array<{ identity: string; text: string[] }> => {
|
|
1366
|
-
if (
|
|
1367
|
-
!assertion ||
|
|
1368
|
-
typeof assertion !== "object" ||
|
|
1369
|
-
Array.isArray(assertion)
|
|
1370
|
-
) {
|
|
1371
|
-
return [];
|
|
1372
|
-
}
|
|
1373
|
-
const entry = assertion as Record<string, unknown>;
|
|
1374
|
-
if (entry.status !== "failed") return [];
|
|
1375
|
-
const title =
|
|
1376
|
-
typeof entry.fullName === "string"
|
|
1377
|
-
? entry.fullName
|
|
1378
|
-
: typeof entry.title === "string"
|
|
1379
|
-
? entry.title
|
|
1380
|
-
: "unknown-assertion";
|
|
1381
|
-
const diagnostics = Array.isArray(entry.failureMessages)
|
|
1382
|
-
? entry.failureMessages.filter(
|
|
1383
|
-
(message): message is string => typeof message === "string",
|
|
1384
|
-
)
|
|
1385
|
-
: [];
|
|
1386
|
-
return [
|
|
1387
|
-
{
|
|
1388
|
-
identity: `${suiteName}\0${title}`,
|
|
1389
|
-
text: [title, ...diagnostics],
|
|
1390
|
-
},
|
|
1391
|
-
];
|
|
1392
|
-
},
|
|
1393
|
-
);
|
|
1394
|
-
});
|
|
1395
|
-
return {
|
|
1396
|
-
total: report.numTotalTests as number,
|
|
1397
|
-
failed: report.numFailedTests as number,
|
|
1398
|
-
success: report.success,
|
|
1399
|
-
failures: failedAssertions.map((failure) => failure.identity),
|
|
1400
|
-
failedText: failedAssertions.flatMap((failure) => failure.text),
|
|
1401
|
-
};
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
function normalizedFailureIdentities(input: {
|
|
1405
|
-
step: AtomicVerificationContract;
|
|
1406
|
-
report?: ReturnType<typeof vitestReport>;
|
|
1407
|
-
output: string;
|
|
1408
|
-
exitCode: number;
|
|
1409
|
-
}): string[] {
|
|
1410
|
-
const vitestFailures = input.report?.failures ?? [];
|
|
1411
|
-
if (vitestFailures.length > 0) {
|
|
1412
|
-
return [...new Set(vitestFailures)]
|
|
1413
|
-
.sort()
|
|
1414
|
-
.map((failure) => sha256(`vitest-failure\0${failure}`));
|
|
1415
|
-
}
|
|
1416
|
-
const normalizedOutput = input.output
|
|
1417
|
-
.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu"), "")
|
|
1418
|
-
.replace(/\r\n?/gu, "\n")
|
|
1419
|
-
.replace(/\b\d+(?:\.\d+)?(?:ms|s)\b/gu, "<duration>")
|
|
1420
|
-
.slice(0, 64 * 1024);
|
|
1421
|
-
return [
|
|
1422
|
-
sha256(
|
|
1423
|
-
canonicalJson({
|
|
1424
|
-
verificationId: input.step.id,
|
|
1425
|
-
exitCode: input.exitCode,
|
|
1426
|
-
output: normalizedOutput,
|
|
1427
|
-
}),
|
|
1428
|
-
),
|
|
1429
|
-
];
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
export async function executePackageVerification(input: {
|
|
1433
|
-
root: string;
|
|
1434
|
-
dependencyOwner: string;
|
|
1435
|
-
verification: StructuredVerificationContract;
|
|
1436
|
-
signal: AbortSignal;
|
|
1437
|
-
}) {
|
|
1438
|
-
const capability = bindCurrentVerificationCapability(
|
|
1439
|
-
input.root,
|
|
1440
|
-
input.verification,
|
|
1441
|
-
{ dependencyOwner: input.dependencyOwner },
|
|
1442
|
-
);
|
|
1443
|
-
if (!capability.ok) {
|
|
1444
|
-
return {
|
|
1445
|
-
ok: false as const,
|
|
1446
|
-
kind: "paused" as const,
|
|
1447
|
-
code: capability.diagnostic.code,
|
|
1448
|
-
};
|
|
1449
|
-
}
|
|
1450
|
-
let runners: ReturnType<typeof prepareRunnerBindings>;
|
|
1451
|
-
try {
|
|
1452
|
-
runners = prepareRunnerBindings(
|
|
1453
|
-
input.root,
|
|
1454
|
-
capability.value.runnerBindings,
|
|
1455
|
-
);
|
|
1456
|
-
} catch {
|
|
1457
|
-
return {
|
|
1458
|
-
ok: false as const,
|
|
1459
|
-
kind: "paused" as const,
|
|
1460
|
-
code: "runner-mount-unavailable",
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1463
|
-
try {
|
|
1464
|
-
const nodeModules = path.join(input.dependencyOwner, "node_modules");
|
|
1465
|
-
const dependency = lstatSync(nodeModules, { throwIfNoEntry: false });
|
|
1466
|
-
const mounts = [] as Array<{
|
|
1467
|
-
source: string;
|
|
1468
|
-
target: string;
|
|
1469
|
-
writable?: boolean;
|
|
1470
|
-
}>;
|
|
1471
|
-
if (dependency) {
|
|
1472
|
-
if (!dependency.isDirectory() || dependency.isSymbolicLink()) {
|
|
1473
|
-
return {
|
|
1474
|
-
ok: false as const,
|
|
1475
|
-
kind: "paused" as const,
|
|
1476
|
-
code: "dependency-path-unsafe",
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1479
|
-
const target = path.join(input.root, "node_modules");
|
|
1480
|
-
const targetStat = lstatSync(target, { throwIfNoEntry: false });
|
|
1481
|
-
if (
|
|
1482
|
-
targetStat &&
|
|
1483
|
-
(!targetStat.isDirectory() || targetStat.isSymbolicLink())
|
|
1484
|
-
) {
|
|
1485
|
-
return {
|
|
1486
|
-
ok: false as const,
|
|
1487
|
-
kind: "paused" as const,
|
|
1488
|
-
code: "dependency-path-unsafe",
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
if (!targetStat) mkdirSync(target, { mode: 0o700 });
|
|
1492
|
-
mounts.push({ source: nodeModules, target: "/workspace/node_modules" });
|
|
1493
|
-
}
|
|
1494
|
-
mounts.push(...runners.mounts);
|
|
1495
|
-
const isolation = new BubblewrapIsolationBackend();
|
|
1496
|
-
const steps = verificationSteps(input.verification);
|
|
1497
|
-
let final:
|
|
1498
|
-
| {
|
|
1499
|
-
exitCode: number;
|
|
1500
|
-
classification: AtomicVerificationContract["classification"];
|
|
1501
|
-
diagnostic: { kind: "assertion" | "compiler"; id: string };
|
|
1502
|
-
}
|
|
1503
|
-
| undefined;
|
|
1504
|
-
for (const step of steps) {
|
|
1505
|
-
const invocation = verificationInvocation(step, runners.bindings);
|
|
1506
|
-
if (!invocation) {
|
|
1507
|
-
return {
|
|
1508
|
-
ok: false as const,
|
|
1509
|
-
kind: "paused" as const,
|
|
1510
|
-
code: "runner-missing",
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
const executed = await isolation.run({
|
|
1514
|
-
root: input.root,
|
|
1515
|
-
executable: invocation.executable,
|
|
1516
|
-
args: invocation.args,
|
|
1517
|
-
mounts,
|
|
1518
|
-
environment: { CI: "1" },
|
|
1519
|
-
signal: input.signal,
|
|
1520
|
-
});
|
|
1521
|
-
if (!executed.ok) {
|
|
1522
|
-
if (executed.state === "cancelled") throw input.signal.reason;
|
|
1523
|
-
return {
|
|
1524
|
-
ok: false as const,
|
|
1525
|
-
kind: "paused" as const,
|
|
1526
|
-
code: executed.code,
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
|
-
const report =
|
|
1530
|
-
step.kind === "vitest" ? vitestReport(executed.stdout) : undefined;
|
|
1531
|
-
const output = `${executed.stdout}\n${executed.stderr}`;
|
|
1532
|
-
const identity =
|
|
1533
|
-
step.classification !== "expected-red" ||
|
|
1534
|
-
(step.kind === "vitest"
|
|
1535
|
-
? report?.failedText.some((text) =>
|
|
1536
|
-
text.includes(step.expectedFailure ?? ""),
|
|
1537
|
-
) === true
|
|
1538
|
-
: output.includes(step.expectedFailure ?? ""));
|
|
1539
|
-
const accepted =
|
|
1540
|
-
step.classification === "expected-red"
|
|
1541
|
-
? executed.exitCode !== 0 &&
|
|
1542
|
-
identity &&
|
|
1543
|
-
(step.kind !== "vitest" ||
|
|
1544
|
-
(report !== undefined &&
|
|
1545
|
-
report.failed > 0 &&
|
|
1546
|
-
report.total >= step.minTests))
|
|
1547
|
-
: executed.exitCode === 0 &&
|
|
1548
|
-
(step.kind !== "vitest" ||
|
|
1549
|
-
(report?.success === true && report.total >= step.minTests));
|
|
1550
|
-
if (!accepted) {
|
|
1551
|
-
return {
|
|
1552
|
-
ok: false as const,
|
|
1553
|
-
kind: "retryable" as const,
|
|
1554
|
-
code:
|
|
1555
|
-
step.classification === "expected-red" && executed.exitCode === 0
|
|
1556
|
-
? "red-not-witnessed"
|
|
1557
|
-
: "verification-rejected",
|
|
1558
|
-
failureIdentities: normalizedFailureIdentities({
|
|
1559
|
-
step,
|
|
1560
|
-
report,
|
|
1561
|
-
output,
|
|
1562
|
-
exitCode: executed.exitCode,
|
|
1563
|
-
}),
|
|
1564
|
-
};
|
|
1565
|
-
}
|
|
1566
|
-
if (!isVerificationCapabilityCurrent(input.root, capability.value)) {
|
|
1567
|
-
return {
|
|
1568
|
-
ok: false as const,
|
|
1569
|
-
kind: "retryable" as const,
|
|
1570
|
-
code: "verification-input-unavailable",
|
|
1571
|
-
};
|
|
1572
|
-
}
|
|
1573
|
-
final = {
|
|
1574
|
-
exitCode: executed.exitCode,
|
|
1575
|
-
classification: step.classification,
|
|
1576
|
-
diagnostic: {
|
|
1577
|
-
kind: step.kind === "vitest" ? "assertion" : "compiler",
|
|
1578
|
-
id: step.id,
|
|
1579
|
-
},
|
|
1580
|
-
};
|
|
1581
|
-
}
|
|
1582
|
-
return final
|
|
1583
|
-
? { ok: true as const, ...final }
|
|
1584
|
-
: {
|
|
1585
|
-
ok: false as const,
|
|
1586
|
-
kind: "paused" as const,
|
|
1587
|
-
code: "verification-contract-unsupported",
|
|
1588
|
-
};
|
|
1589
|
-
} finally {
|
|
1590
|
-
if (runners.cleanupRoot) {
|
|
1591
|
-
rmSync(runners.cleanupRoot, { recursive: true, force: true });
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
function affectedVerification(
|
|
1597
|
-
task: Parameters<
|
|
1598
|
-
Parameters<typeof openDurableWorkflowEngine>[0]["proposeCandidate"]
|
|
1599
|
-
>[0]["task"],
|
|
1600
|
-
): StructuredVerificationContract {
|
|
1601
|
-
if (task.affectedVerification) {
|
|
1602
|
-
return structuredClone(task.affectedVerification);
|
|
1603
|
-
}
|
|
1604
|
-
const finalPhase = task.phases.refactor ?? task.phases.green;
|
|
1605
|
-
const verification = structuredClone(finalPhase.verification);
|
|
1606
|
-
const affected = [...new Set(task.impactClosure.affectedSuite)].sort();
|
|
1607
|
-
if (affected.length === 0) return verification;
|
|
1608
|
-
if (verification.kind === "vitest") {
|
|
1609
|
-
return { ...verification, testFiles: affected };
|
|
1610
|
-
}
|
|
1611
|
-
if (verification.kind === "steps") {
|
|
1612
|
-
return {
|
|
1613
|
-
...verification,
|
|
1614
|
-
steps: verification.steps.map((step) =>
|
|
1615
|
-
step.kind === "vitest" ? { ...step, testFiles: affected } : step,
|
|
1616
|
-
),
|
|
1617
|
-
};
|
|
1618
|
-
}
|
|
1619
|
-
return verification;
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
function childRequestId(
|
|
1623
|
-
operationId: string,
|
|
1624
|
-
taskId: string,
|
|
1625
|
-
phase: string,
|
|
1626
|
-
): string {
|
|
1627
|
-
return `child-${sha256(`${operationId}\0${taskId}\0${phase}`).slice(0, 40)}`;
|
|
1628
|
-
}
|
|
1629
|
-
|
|
642
|
+
/** Pi adapter: project host capabilities before entering the package services. */
|
|
1630
643
|
export function openPackageWorkflowControlEngine(
|
|
1631
644
|
initialContext: ExtensionContext,
|
|
1632
|
-
parentPayloadBridge: ParentPayloadBridge,
|
|
1633
645
|
): WorkflowControlEngine {
|
|
1634
|
-
const
|
|
1635
|
-
const routeResolution = loadRoutePolicy({
|
|
1636
|
-
cwd: consumerRoot,
|
|
1637
|
-
home: homedir(),
|
|
1638
|
-
...(initialContext.model ? { parentModel: initialContext.model } : {}),
|
|
1639
|
-
});
|
|
1640
|
-
const stateRoot = resolveStateRoot({
|
|
1641
|
-
consumerRoot,
|
|
1642
|
-
xdgStateHome: process.env.XDG_STATE_HOME,
|
|
1643
|
-
});
|
|
1644
|
-
const contexts = new AsyncLocalStorage<ExtensionContext>();
|
|
1645
|
-
const design = DesignController.open({
|
|
1646
|
-
consumerRoot,
|
|
1647
|
-
stateRoot,
|
|
1648
|
-
inspectOpenSpec: inspectOpenSpecDelivery,
|
|
1649
|
-
});
|
|
1650
|
-
const implementationAgent = loadAgentDefinitions().find(
|
|
1651
|
-
(agent) => agent.role === "implementation-worker",
|
|
1652
|
-
);
|
|
1653
|
-
if (!implementationAgent) {
|
|
1654
|
-
throw new Error("implementation-worker-agent-unavailable");
|
|
1655
|
-
}
|
|
1656
|
-
const engine = openDurableWorkflowEngine({
|
|
1657
|
-
consumerRoot,
|
|
1658
|
-
stateRoot,
|
|
1659
|
-
deliverySource: packageDeliverySource(consumerRoot, {
|
|
1660
|
-
verifyGateProof: (input) => design.verifyGateProof(input),
|
|
1661
|
-
verifyFinalizedDelivery: (input) => design.verifyFinalizedDelivery(input),
|
|
1662
|
-
}),
|
|
1663
|
-
routePolicy: routeResolution.ok
|
|
1664
|
-
? routeResolution.policy
|
|
1665
|
-
: unavailableRoutePolicy(),
|
|
1666
|
-
proposeCandidate: async (input) => {
|
|
1667
|
-
const context = contexts.getStore();
|
|
1668
|
-
if (!context) {
|
|
1669
|
-
return { kind: "paused", code: "parent-context-unavailable" };
|
|
1670
|
-
}
|
|
1671
|
-
const phaseRuntime = await runtimeForWorkerRoute(
|
|
1672
|
-
input.route as WorkerRoutePolicy,
|
|
1673
|
-
context,
|
|
1674
|
-
parentPayloadBridge,
|
|
1675
|
-
input.signal,
|
|
1676
|
-
process.env,
|
|
1677
|
-
{ onResponse: input.onHeaders },
|
|
1678
|
-
);
|
|
1679
|
-
if (!phaseRuntime.ok) {
|
|
1680
|
-
if (phaseRuntime.failure.kind === "cancelled") {
|
|
1681
|
-
return { kind: "operation-cancelled", code: "cancelled" };
|
|
1682
|
-
}
|
|
1683
|
-
throw new Error(phaseRuntime.failure.code);
|
|
1684
|
-
}
|
|
1685
|
-
const phase = input.task.phases[input.phase];
|
|
1686
|
-
if (!phase) {
|
|
1687
|
-
return { kind: "paused", code: "task-phase-unavailable" };
|
|
1688
|
-
}
|
|
1689
|
-
const taskPhases = Object.values(input.task.phases);
|
|
1690
|
-
const executionBoundary = input.artifactCorrection
|
|
1691
|
-
? {
|
|
1692
|
-
read: [
|
|
1693
|
-
...new Set(
|
|
1694
|
-
taskPhases.flatMap((boundary) => [
|
|
1695
|
-
...boundary.read,
|
|
1696
|
-
...boundary.write,
|
|
1697
|
-
...boundary.delete,
|
|
1698
|
-
]),
|
|
1699
|
-
),
|
|
1700
|
-
].sort(),
|
|
1701
|
-
write: [
|
|
1702
|
-
...new Set(taskPhases.flatMap((boundary) => boundary.write)),
|
|
1703
|
-
].sort(),
|
|
1704
|
-
delete: [
|
|
1705
|
-
...new Set(taskPhases.flatMap((boundary) => boundary.delete)),
|
|
1706
|
-
].sort(),
|
|
1707
|
-
}
|
|
1708
|
-
: {
|
|
1709
|
-
read: [...phase.read],
|
|
1710
|
-
write: [...phase.write],
|
|
1711
|
-
delete: [...phase.delete],
|
|
1712
|
-
};
|
|
1713
|
-
const phaseContract = {
|
|
1714
|
-
candidateId: input.candidateArtifact.identity.candidateId,
|
|
1715
|
-
taskId: input.taskId,
|
|
1716
|
-
phase: input.phase,
|
|
1717
|
-
readSet: executionBoundary.read,
|
|
1718
|
-
writeSet: executionBoundary.write,
|
|
1719
|
-
deleteSet: executionBoundary.delete,
|
|
1720
|
-
verification: structuredClone(phase.verification),
|
|
1721
|
-
agentsImpact: input.task.agents.impact,
|
|
1722
|
-
agentsTarget: input.task.agents.target ?? null,
|
|
1723
|
-
agentsManagedOnly: true,
|
|
1724
|
-
agentsWriteAllowed: false,
|
|
1725
|
-
impactClosure: structuredClone(input.task.impactClosure),
|
|
1726
|
-
...(input.artifactCorrection
|
|
1727
|
-
? { artifactCorrection: structuredClone(input.artifactCorrection) }
|
|
1728
|
-
: {}),
|
|
1729
|
-
...(input.contextRequest
|
|
1730
|
-
? { requestedContext: structuredClone(input.contextRequest) }
|
|
1731
|
-
: {}),
|
|
1732
|
-
...(input.repair
|
|
1733
|
-
? {
|
|
1734
|
-
repair: {
|
|
1735
|
-
attempt: input.repair.attempt,
|
|
1736
|
-
attribution: input.repair.attribution,
|
|
1737
|
-
failureIdentities: [...input.repair.failureIdentities],
|
|
1738
|
-
verification: structuredClone(input.task.repairVerification),
|
|
1739
|
-
inBoundaryOnly: true,
|
|
1740
|
-
},
|
|
1741
|
-
}
|
|
1742
|
-
: {}),
|
|
1743
|
-
};
|
|
1744
|
-
const child = await runChildSession({
|
|
1745
|
-
cwd: input.workspaceRoot,
|
|
1746
|
-
modelRuntime: phaseRuntime.modelRuntime,
|
|
1747
|
-
model: phaseRuntime.model,
|
|
1748
|
-
systemPrompt: [
|
|
1749
|
-
implementationAgent.content,
|
|
1750
|
-
input.task.objective,
|
|
1751
|
-
input.task.context.agents,
|
|
1752
|
-
input.task.context.contract,
|
|
1753
|
-
`<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
|
|
1754
|
-
].join("\n\n"),
|
|
1755
|
-
requestId: childRequestId(input.operationId, input.taskId, input.phase),
|
|
1756
|
-
taskId: input.taskId,
|
|
1757
|
-
role: "implementation-worker",
|
|
1758
|
-
phase: input.phase,
|
|
1759
|
-
output: "diff",
|
|
1760
|
-
roots: input.task.roots.map((root) =>
|
|
1761
|
-
path.resolve(input.workspaceRoot, root),
|
|
1762
|
-
),
|
|
1763
|
-
allowedPaths: [
|
|
1764
|
-
...new Set([
|
|
1765
|
-
...executionBoundary.read,
|
|
1766
|
-
...executionBoundary.write,
|
|
1767
|
-
...executionBoundary.delete,
|
|
1768
|
-
]),
|
|
1769
|
-
],
|
|
1770
|
-
timeoutMs: LIMITS.phaseTimeoutMs,
|
|
1771
|
-
signal: input.signal,
|
|
1772
|
-
failureOverride: phaseRuntime.failureOverride,
|
|
1773
|
-
ledgerProjection: input.ledgerProjection,
|
|
1774
|
-
candidateArtifact: input.candidateArtifact,
|
|
1775
|
-
onStreamProgress: input.onProgress,
|
|
1776
|
-
});
|
|
1777
|
-
if (!child.ok) {
|
|
1778
|
-
if (child.failure.kind === "transport") {
|
|
1779
|
-
throw new Error(child.failure.code);
|
|
1780
|
-
}
|
|
1781
|
-
if (child.failure.kind === "cancelled") {
|
|
1782
|
-
return { kind: "operation-cancelled", code: "cancelled" };
|
|
1783
|
-
}
|
|
1784
|
-
if (child.failure.kind === "approval-boundary") {
|
|
1785
|
-
return { kind: "approval-needed", code: child.failure.code };
|
|
1786
|
-
}
|
|
1787
|
-
if (child.failure.kind === "result-limit") {
|
|
1788
|
-
return { kind: "paused", code: "needs-task-split" };
|
|
1789
|
-
}
|
|
1790
|
-
const attemptDiagnostic = {
|
|
1791
|
-
finalCategory: child.classification.finalCategory,
|
|
1792
|
-
submitAttempts: child.classification.attempts,
|
|
1793
|
-
schema: child.classification.schema,
|
|
1794
|
-
identityMismatch: Object.entries(child.classification.identity)
|
|
1795
|
-
.filter(([, matches]) => !matches)
|
|
1796
|
-
.map(([dimension]) => dimension),
|
|
1797
|
-
};
|
|
1798
|
-
return child.failure.kind === "environment" ||
|
|
1799
|
-
child.failure.kind === "verification-adapter"
|
|
1800
|
-
? { kind: "paused", code: child.failure.code, attemptDiagnostic }
|
|
1801
|
-
: { kind: "retryable", code: child.failure.code, attemptDiagnostic };
|
|
1802
|
-
}
|
|
1803
|
-
const result = child.result;
|
|
1804
|
-
if (result.kind === "context-request") {
|
|
1805
|
-
return classifyCandidateContextRequest(result, {
|
|
1806
|
-
phase: input.phase,
|
|
1807
|
-
readPaths: phase.read,
|
|
1808
|
-
writePaths: phase.write,
|
|
1809
|
-
deletePaths: phase.delete,
|
|
1810
|
-
taskPaths: [
|
|
1811
|
-
...new Set(
|
|
1812
|
-
Object.values(input.task.phases).flatMap((boundary) => [
|
|
1813
|
-
...boundary.read,
|
|
1814
|
-
...boundary.write,
|
|
1815
|
-
...boundary.delete,
|
|
1816
|
-
]),
|
|
1817
|
-
),
|
|
1818
|
-
],
|
|
1819
|
-
redWritePaths: input.task.phases.red.write,
|
|
1820
|
-
agents: {
|
|
1821
|
-
impact: input.task.agents.impact,
|
|
1822
|
-
...(input.task.agents.target
|
|
1823
|
-
? { target: input.task.agents.target }
|
|
1824
|
-
: {}),
|
|
1825
|
-
},
|
|
1826
|
-
});
|
|
1827
|
-
}
|
|
1828
|
-
if (result.kind !== "sealed-candidate") {
|
|
1829
|
-
return { kind: "retryable", code: "candidate-diff-invalid" };
|
|
1830
|
-
}
|
|
1831
|
-
return {
|
|
1832
|
-
kind: "sealed-candidate",
|
|
1833
|
-
candidateId: result.candidateId,
|
|
1834
|
-
artifactHash: result.artifactHash,
|
|
1835
|
-
bytes: result.bytes,
|
|
1836
|
-
paths: [...result.paths],
|
|
1837
|
-
};
|
|
1838
|
-
},
|
|
1839
|
-
verifyPhase: (input) =>
|
|
1840
|
-
executePackageVerification({
|
|
1841
|
-
root: input.root,
|
|
1842
|
-
dependencyOwner: consumerRoot,
|
|
1843
|
-
verification: input.verification,
|
|
1844
|
-
signal: input.signal,
|
|
1845
|
-
}),
|
|
1846
|
-
verifyChange: async (input) => {
|
|
1847
|
-
const verifications = input.verification
|
|
1848
|
-
? [input.verification]
|
|
1849
|
-
: input.plan.tasks.map((task) => affectedVerification(task));
|
|
1850
|
-
for (const verification of verifications) {
|
|
1851
|
-
const result = await executePackageVerification({
|
|
1852
|
-
root: input.root,
|
|
1853
|
-
dependencyOwner: consumerRoot,
|
|
1854
|
-
verification,
|
|
1855
|
-
signal: input.signal,
|
|
1856
|
-
});
|
|
1857
|
-
if (!result.ok) {
|
|
1858
|
-
const adapter = new Set([
|
|
1859
|
-
"runner-missing",
|
|
1860
|
-
"script-missing",
|
|
1861
|
-
"script-command-mismatch",
|
|
1862
|
-
"local-executable-missing",
|
|
1863
|
-
]).has(result.code);
|
|
1864
|
-
return {
|
|
1865
|
-
ok: false,
|
|
1866
|
-
kind: adapter
|
|
1867
|
-
? ("verification-adapter" as const)
|
|
1868
|
-
: result.kind === "paused"
|
|
1869
|
-
? ("environment" as const)
|
|
1870
|
-
: ("verification" as const),
|
|
1871
|
-
code: result.code,
|
|
1872
|
-
...(result.failureIdentities
|
|
1873
|
-
? { failureIdentities: result.failureIdentities }
|
|
1874
|
-
: {}),
|
|
1875
|
-
};
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
return {
|
|
1879
|
-
ok: true,
|
|
1880
|
-
exitCode: 0,
|
|
1881
|
-
classification: "expected-green",
|
|
1882
|
-
};
|
|
1883
|
-
},
|
|
1884
|
-
});
|
|
646
|
+
const service = openPackageWorkflowService(packageContext(initialContext));
|
|
1885
647
|
return {
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
cwd: consumerRoot,
|
|
1894
|
-
home: homedir(),
|
|
1895
|
-
...(context.model ? { parentModel: context.model } : {}),
|
|
1896
|
-
});
|
|
1897
|
-
engine.updateRoutePolicy(
|
|
1898
|
-
operationRouteResolution.ok
|
|
1899
|
-
? operationRouteResolution.policy
|
|
1900
|
-
: unavailableRoutePolicy(),
|
|
648
|
+
...service,
|
|
649
|
+
execute(command, context = initialContext, signal, onActivity) {
|
|
650
|
+
return service.execute(
|
|
651
|
+
command,
|
|
652
|
+
packageContext(context),
|
|
653
|
+
signal,
|
|
654
|
+
onActivity,
|
|
1901
655
|
);
|
|
1902
|
-
const validation = validateControlCommand(command);
|
|
1903
|
-
if (!validation.ok) {
|
|
1904
|
-
const error = new Error(validation.code);
|
|
1905
|
-
error.name = "ControlCommandError";
|
|
1906
|
-
throw error;
|
|
1907
|
-
}
|
|
1908
|
-
return contexts.run(context, async () => {
|
|
1909
|
-
const outcome = await engine.execute(
|
|
1910
|
-
validation.value,
|
|
1911
|
-
signal,
|
|
1912
|
-
onActivity,
|
|
1913
|
-
);
|
|
1914
|
-
const routePolicy = visibleRoutePolicyStatus(
|
|
1915
|
-
operationRouteResolution,
|
|
1916
|
-
engine.routePolicyStatus(),
|
|
1917
|
-
);
|
|
1918
|
-
return { ...outcome, routePolicy };
|
|
1919
|
-
});
|
|
1920
|
-
},
|
|
1921
|
-
executeDesign(request: unknown) {
|
|
1922
|
-
return design.execute(request);
|
|
1923
|
-
},
|
|
1924
|
-
assertDesignRun(runId: string) {
|
|
1925
|
-
design.assertDesignRun(runId);
|
|
1926
|
-
},
|
|
1927
|
-
recordDesignEvidence(input) {
|
|
1928
|
-
return design.recordEvidence(input);
|
|
1929
|
-
},
|
|
1930
|
-
async close() {
|
|
1931
|
-
contexts.disable();
|
|
1932
|
-
await engine.close();
|
|
1933
|
-
design.close();
|
|
1934
656
|
},
|
|
1935
657
|
};
|
|
1936
658
|
}
|
|
@@ -1968,27 +690,17 @@ function runStoreResetError(error: RunStoreFormatError): Error {
|
|
|
1968
690
|
return reset;
|
|
1969
691
|
}
|
|
1970
692
|
|
|
1971
|
-
function visibleRoutePolicyStatus(
|
|
1972
|
-
resolution: RoutePolicyResolution,
|
|
1973
|
-
brokerStatus: Record<string, unknown> | undefined,
|
|
1974
|
-
): Record<string, unknown> {
|
|
1975
|
-
if (!resolution.ok) return inspectRoutePolicy(resolution);
|
|
1976
|
-
const inspected = brokerStatus ?? inspectRoutePolicy(resolution);
|
|
1977
|
-
return {
|
|
1978
|
-
...inspected,
|
|
1979
|
-
source: { kind: resolution.source.kind },
|
|
1980
|
-
};
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
693
|
export function registerWorkflowControl(
|
|
1984
694
|
pi: ExtensionAPI,
|
|
1985
695
|
engineFactory: WorkflowControlEngineFactory = openPackageWorkflowControlEngine,
|
|
1986
696
|
): void {
|
|
1987
|
-
const parentPayloadBridge = new ParentPayloadBridge();
|
|
1988
697
|
const activity = new ActivityController();
|
|
1989
698
|
const engines = new Map<string, Promise<WorkflowControlEngine>>();
|
|
699
|
+
const designOperations = new Set<Promise<void>>();
|
|
1990
700
|
const designFailures = new Map<string, SafeDesignFailure>();
|
|
1991
701
|
let pendingPrompt: EligiblePrompt | undefined;
|
|
702
|
+
let pendingInit = false;
|
|
703
|
+
let exitingStage = false;
|
|
1992
704
|
let activePrompt: EligiblePrompt | undefined;
|
|
1993
705
|
let designToolSnapshot: string[] | undefined;
|
|
1994
706
|
const activation = new (class implements Activation {
|
|
@@ -2016,13 +728,13 @@ export function registerWorkflowControl(
|
|
|
2016
728
|
return true;
|
|
2017
729
|
}
|
|
2018
730
|
})();
|
|
2019
|
-
const packetRuntime = new PacketRuntime({ activation
|
|
731
|
+
const packetRuntime = new PacketRuntime({ activation });
|
|
2020
732
|
|
|
2021
733
|
const engineFor = (ctx: ExtensionContext) => {
|
|
2022
734
|
const key = ctx.cwd;
|
|
2023
735
|
const existing = engines.get(key);
|
|
2024
736
|
if (existing) return existing;
|
|
2025
|
-
const opened = Promise.resolve(engineFactory(ctx
|
|
737
|
+
const opened = Promise.resolve(engineFactory(ctx));
|
|
2026
738
|
engines.set(key, opened);
|
|
2027
739
|
void opened.catch(() => {
|
|
2028
740
|
if (engines.get(key) === opened) engines.delete(key);
|
|
@@ -2030,16 +742,19 @@ export function registerWorkflowControl(
|
|
|
2030
742
|
return opened;
|
|
2031
743
|
};
|
|
2032
744
|
const closeEngines = async () => {
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
await Promise.
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
745
|
+
// Design close() is synchronous and does not own an operation queue.
|
|
746
|
+
// Keep its journal open through finalization's commit/lease-cleanup path
|
|
747
|
+
// and the dispatch result handling before closing any engine storage.
|
|
748
|
+
await Promise.all([...designOperations]);
|
|
749
|
+
const closed = await Promise.allSettled(
|
|
750
|
+
[...engines].map(async ([key, opened]) => {
|
|
751
|
+
const engine = await opened;
|
|
752
|
+
await engine.close();
|
|
753
|
+
if (engines.get(key) === opened) engines.delete(key);
|
|
754
|
+
}),
|
|
2042
755
|
);
|
|
756
|
+
const failed = closed.find((result) => result.status === "rejected");
|
|
757
|
+
if (failed?.status === "rejected") throw failed.reason;
|
|
2043
758
|
};
|
|
2044
759
|
const restoreDesignTools = () => {
|
|
2045
760
|
if (!designToolSnapshot) return;
|
|
@@ -2075,7 +790,16 @@ export function registerWorkflowControl(
|
|
|
2075
790
|
activePrompt = undefined;
|
|
2076
791
|
await packetRuntime.drain();
|
|
2077
792
|
deactivate();
|
|
2078
|
-
|
|
793
|
+
};
|
|
794
|
+
const exitStage = async () => {
|
|
795
|
+
if (exitingStage) throw new Error("stage-control-mismatch");
|
|
796
|
+
exitingStage = true;
|
|
797
|
+
try {
|
|
798
|
+
await closeEngines();
|
|
799
|
+
await deactivateStage();
|
|
800
|
+
} finally {
|
|
801
|
+
exitingStage = false;
|
|
802
|
+
}
|
|
2079
803
|
};
|
|
2080
804
|
|
|
2081
805
|
// Pi validates tool arguments before execute(), so expose only the schema
|
|
@@ -2102,6 +826,61 @@ export function registerWorkflowControl(
|
|
|
2102
826
|
} as const;
|
|
2103
827
|
let registeredParameterKind: "command" | "packet" | undefined;
|
|
2104
828
|
|
|
829
|
+
// Stage exit is session control, not a durable Implement command. Keep the
|
|
830
|
+
// six engine commands unchanged and admit only this exact extra envelope.
|
|
831
|
+
const FINISH_PARAMETERS = {
|
|
832
|
+
type: "object",
|
|
833
|
+
properties: { action: { type: "string", enum: ["finish"] } },
|
|
834
|
+
required: ["action"],
|
|
835
|
+
additionalProperties: false,
|
|
836
|
+
} as const;
|
|
837
|
+
const AMEND_PARAMETERS = {
|
|
838
|
+
type: "object",
|
|
839
|
+
properties: {
|
|
840
|
+
action: { type: "string", enum: ["amend"] },
|
|
841
|
+
change: { type: "string" },
|
|
842
|
+
batchId: { type: "string" },
|
|
843
|
+
request: DESIGN_CONTROL_REQUEST_SCHEMA,
|
|
844
|
+
},
|
|
845
|
+
required: ["action", "change", "batchId", "request"],
|
|
846
|
+
additionalProperties: false,
|
|
847
|
+
} as const;
|
|
848
|
+
const IMPLEMENT_PARAMETERS = {
|
|
849
|
+
...CONTROL_COMMAND_PARAMETERS,
|
|
850
|
+
properties: {
|
|
851
|
+
...CONTROL_COMMAND_PARAMETERS.properties,
|
|
852
|
+
...AMEND_PARAMETERS.properties,
|
|
853
|
+
action: { type: "string", enum: ["finish", "amend"] },
|
|
854
|
+
},
|
|
855
|
+
required: [],
|
|
856
|
+
anyOf: [
|
|
857
|
+
...CONTROL_COMMAND_PARAMETERS.anyOf,
|
|
858
|
+
FINISH_PARAMETERS,
|
|
859
|
+
AMEND_PARAMETERS,
|
|
860
|
+
],
|
|
861
|
+
} as const;
|
|
862
|
+
const prepareImplementArguments = (args: unknown): unknown => {
|
|
863
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return args;
|
|
864
|
+
const record = { ...(args as Record<string, unknown>) };
|
|
865
|
+
// Strict providers can pad fields belonging to the other union branch.
|
|
866
|
+
if (record.action !== "amend") {
|
|
867
|
+
for (const key of ["batchId", "request"]) {
|
|
868
|
+
if (record[key] === null || record[key] === undefined)
|
|
869
|
+
delete record[key];
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (record.action === "finish" || record.action === "amend") {
|
|
873
|
+
for (const key of Object.keys(CONTROL_COMMAND_PARAMETERS.properties)) {
|
|
874
|
+
if (record[key] === null || record[key] === undefined)
|
|
875
|
+
delete record[key];
|
|
876
|
+
}
|
|
877
|
+
return record;
|
|
878
|
+
}
|
|
879
|
+
if (record.action === null || record.action === undefined)
|
|
880
|
+
delete record.action;
|
|
881
|
+
return canonicalizeControlCommandToolInput(record);
|
|
882
|
+
};
|
|
883
|
+
|
|
2105
884
|
const registerDispatchTool = (kind: "command" | "packet") => {
|
|
2106
885
|
if (registeredParameterKind === kind) return;
|
|
2107
886
|
registeredParameterKind = kind;
|
|
@@ -2110,17 +889,16 @@ export function registerWorkflowControl(
|
|
|
2110
889
|
label: "Abel Control",
|
|
2111
890
|
description:
|
|
2112
891
|
kind === "command"
|
|
2113
|
-
?
|
|
892
|
+
? 'Private stage-bound Abel workflow control. Accepts durable Implement commands, batch-bound {"action":"amend"} artifact revisions, or {"action":"finish"} to leave and preserve resumable work.'
|
|
2114
893
|
: "Private stage-bound Abel packet control. Accepts bounded Design and Diagnose packet operations.",
|
|
2115
894
|
executionMode: "parallel",
|
|
2116
895
|
...(kind === "command"
|
|
2117
896
|
? {
|
|
2118
897
|
prepareArguments: (args: unknown) =>
|
|
2119
|
-
|
|
898
|
+
prepareImplementArguments(args) as never,
|
|
2120
899
|
}
|
|
2121
900
|
: {}),
|
|
2122
|
-
parameters:
|
|
2123
|
-
kind === "command" ? CONTROL_COMMAND_PARAMETERS : PACKET_PARAMETERS,
|
|
901
|
+
parameters: kind === "command" ? IMPLEMENT_PARAMETERS : PACKET_PARAMETERS,
|
|
2124
902
|
async execute(
|
|
2125
903
|
toolCallId: string,
|
|
2126
904
|
params: unknown,
|
|
@@ -2128,10 +906,127 @@ export function registerWorkflowControl(
|
|
|
2128
906
|
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
2129
907
|
ctx: ExtensionContext,
|
|
2130
908
|
) {
|
|
909
|
+
if (
|
|
910
|
+
exitingStage ||
|
|
911
|
+
!activation.isActive() ||
|
|
912
|
+
activePrompt === undefined
|
|
913
|
+
) {
|
|
914
|
+
throw new Error("stage-control-mismatch");
|
|
915
|
+
}
|
|
916
|
+
if (kind === "command") params = prepareImplementArguments(params);
|
|
2131
917
|
const record =
|
|
2132
918
|
params && typeof params === "object" && !Array.isArray(params)
|
|
2133
919
|
? (params as Record<string, unknown>)
|
|
2134
920
|
: undefined;
|
|
921
|
+
if (record?.action === "amend") {
|
|
922
|
+
if (
|
|
923
|
+
activePrompt !== "abel-implement" ||
|
|
924
|
+
Object.keys(record).some(
|
|
925
|
+
(key) =>
|
|
926
|
+
!["action", "change", "batchId", "request"].includes(key),
|
|
927
|
+
) ||
|
|
928
|
+
typeof record.change !== "string" ||
|
|
929
|
+
typeof record.batchId !== "string"
|
|
930
|
+
) {
|
|
931
|
+
throw new Error("stage-control-mismatch");
|
|
932
|
+
}
|
|
933
|
+
const parsed = validateDesignControlRequest(record.request);
|
|
934
|
+
if (!parsed.ok) {
|
|
935
|
+
const error = new DesignControlValidationError(parsed.diagnostics);
|
|
936
|
+
designFailures.set(
|
|
937
|
+
toolCallId,
|
|
938
|
+
safeDesignFailure(error, "validation"),
|
|
939
|
+
);
|
|
940
|
+
throw error;
|
|
941
|
+
}
|
|
942
|
+
const request = parsed.value;
|
|
943
|
+
if (
|
|
944
|
+
request.operation === "bind-change" ||
|
|
945
|
+
(request.operation === "start" &&
|
|
946
|
+
(!("change" in request) || request.change !== record.change))
|
|
947
|
+
) {
|
|
948
|
+
throw new Error("amendment-change-mismatch");
|
|
949
|
+
}
|
|
950
|
+
let settle!: () => void;
|
|
951
|
+
const pending = new Promise<void>((resolve) => {
|
|
952
|
+
settle = resolve;
|
|
953
|
+
});
|
|
954
|
+
designOperations.add(pending);
|
|
955
|
+
try {
|
|
956
|
+
const engine = await engineFor(ctx);
|
|
957
|
+
if (exitingStage || !activation.isActive() || !engine.executeDesign)
|
|
958
|
+
throw new Error("stage-control-mismatch");
|
|
959
|
+
const status = await engine.execute(
|
|
960
|
+
{
|
|
961
|
+
command: "status",
|
|
962
|
+
stage: "abel-implement",
|
|
963
|
+
change: record.change,
|
|
964
|
+
},
|
|
965
|
+
ctx,
|
|
966
|
+
signal,
|
|
967
|
+
);
|
|
968
|
+
const batch = status.decisionBatch as { id?: string } | undefined;
|
|
969
|
+
if (
|
|
970
|
+
!["approval-needed", "paused"].includes(String(status.state)) ||
|
|
971
|
+
batch?.id !== record.batchId
|
|
972
|
+
)
|
|
973
|
+
throw new Error("amendment-batch-stale");
|
|
974
|
+
if (request.operation !== "start") {
|
|
975
|
+
const designStatus = await engine.executeDesign({
|
|
976
|
+
operation: "status",
|
|
977
|
+
runId: request.runId,
|
|
978
|
+
});
|
|
979
|
+
if (designStatus.change !== record.change)
|
|
980
|
+
throw new Error("amendment-change-mismatch");
|
|
981
|
+
}
|
|
982
|
+
if (!engine.executeAmendment)
|
|
983
|
+
throw new Error("amendment-control-unavailable");
|
|
984
|
+
const revision = await engine.executeAmendment(
|
|
985
|
+
record.change,
|
|
986
|
+
record.batchId,
|
|
987
|
+
request,
|
|
988
|
+
);
|
|
989
|
+
const payload =
|
|
990
|
+
request.operation === "finalize-delivery" &&
|
|
991
|
+
revision.state === "completed"
|
|
992
|
+
? {
|
|
993
|
+
...revision,
|
|
994
|
+
scope: "amendment",
|
|
995
|
+
state: "ready",
|
|
996
|
+
completed: false,
|
|
997
|
+
runId: status.runId,
|
|
998
|
+
amendmentRunId: revision.runId,
|
|
999
|
+
stage: "abel-implement",
|
|
1000
|
+
}
|
|
1001
|
+
: { ...revision, scope: "amendment" };
|
|
1002
|
+
// A compiled amendment stays in Implement; its private Design
|
|
1003
|
+
// records do not activate the Design stage or broaden parent tools.
|
|
1004
|
+
return {
|
|
1005
|
+
content: [
|
|
1006
|
+
{ type: "text" as const, text: JSON.stringify(payload) },
|
|
1007
|
+
],
|
|
1008
|
+
details: payload,
|
|
1009
|
+
};
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
if (
|
|
1012
|
+
error instanceof DesignPlanValidationError ||
|
|
1013
|
+
error instanceof DesignFinalizationError ||
|
|
1014
|
+
error instanceof DeliveryValidationError
|
|
1015
|
+
) {
|
|
1016
|
+
const failure = safeDesignFailure(
|
|
1017
|
+
error,
|
|
1018
|
+
request.operation,
|
|
1019
|
+
"execution",
|
|
1020
|
+
);
|
|
1021
|
+
designFailures.set(toolCallId, failure);
|
|
1022
|
+
throw new Error(failure.code);
|
|
1023
|
+
}
|
|
1024
|
+
throw error;
|
|
1025
|
+
} finally {
|
|
1026
|
+
settle();
|
|
1027
|
+
designOperations.delete(pending);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
2135
1030
|
if (record && typeof record.action === "string") {
|
|
2136
1031
|
if (
|
|
2137
1032
|
Object.keys(record).some(
|
|
@@ -2143,6 +1038,18 @@ export function registerWorkflowControl(
|
|
|
2143
1038
|
if (record.action === "finish" && Object.hasOwn(record, "request")) {
|
|
2144
1039
|
throw new Error("control-envelope-ambiguous");
|
|
2145
1040
|
}
|
|
1041
|
+
if (record.action === "finish") {
|
|
1042
|
+
// close() aborts and settles active operations before ordinary tools
|
|
1043
|
+
// return; durable work is retained, never discarded or completed.
|
|
1044
|
+
await exitStage();
|
|
1045
|
+
const payload = { ok: true, state: "inactive" };
|
|
1046
|
+
return {
|
|
1047
|
+
content: [
|
|
1048
|
+
{ type: "text" as const, text: JSON.stringify(payload) },
|
|
1049
|
+
],
|
|
1050
|
+
details: payload,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
2146
1053
|
if (
|
|
2147
1054
|
activePrompt !== "abel-design" &&
|
|
2148
1055
|
activePrompt !== "abel-diagnose"
|
|
@@ -2154,10 +1061,28 @@ export function registerWorkflowControl(
|
|
|
2154
1061
|
throw new Error("stage-control-mismatch");
|
|
2155
1062
|
}
|
|
2156
1063
|
const designRequest = validateDesignControlRequest(record.request);
|
|
2157
|
-
if (!designRequest.ok)
|
|
1064
|
+
if (!designRequest.ok) {
|
|
1065
|
+
const error = new DesignControlValidationError(
|
|
1066
|
+
designRequest.diagnostics,
|
|
1067
|
+
);
|
|
1068
|
+
designFailures.set(
|
|
1069
|
+
toolCallId,
|
|
1070
|
+
safeDesignFailure(error, "validation"),
|
|
1071
|
+
);
|
|
1072
|
+
throw error;
|
|
1073
|
+
}
|
|
1074
|
+
let settleDesign!: () => void;
|
|
1075
|
+
const settled = new Promise<void>((resolve) => {
|
|
1076
|
+
settleDesign = resolve;
|
|
1077
|
+
});
|
|
1078
|
+
// Register before the first await, including asynchronous engine
|
|
1079
|
+
// opening. Teardown blocks admission before taking its snapshot.
|
|
1080
|
+
designOperations.add(settled);
|
|
2158
1081
|
let boundary: DesignFailureBoundary = "initialization";
|
|
2159
1082
|
try {
|
|
2160
1083
|
const engine = await engineFor(ctx);
|
|
1084
|
+
if (exitingStage || !activation.isActive())
|
|
1085
|
+
throw new Error("stage-control-mismatch");
|
|
2161
1086
|
if (!engine.executeDesign)
|
|
2162
1087
|
throw new Error("design-control-unavailable");
|
|
2163
1088
|
boundary = "execution";
|
|
@@ -2170,7 +1095,9 @@ export function registerWorkflowControl(
|
|
|
2170
1095
|
boundary = "execution";
|
|
2171
1096
|
if (
|
|
2172
1097
|
designRequest.value.operation === "finalize-delivery" &&
|
|
2173
|
-
payload.state === "completed"
|
|
1098
|
+
payload.state === "completed" &&
|
|
1099
|
+
!exitingStage &&
|
|
1100
|
+
activePrompt === "abel-design"
|
|
2174
1101
|
) {
|
|
2175
1102
|
await deactivateStage();
|
|
2176
1103
|
}
|
|
@@ -2188,6 +1115,9 @@ export function registerWorkflowControl(
|
|
|
2188
1115
|
const sanitized = new Error(failure.code);
|
|
2189
1116
|
sanitized.name = "DesignControlError";
|
|
2190
1117
|
throw sanitized;
|
|
1118
|
+
} finally {
|
|
1119
|
+
designOperations.delete(settled);
|
|
1120
|
+
settleDesign();
|
|
2191
1121
|
}
|
|
2192
1122
|
}
|
|
2193
1123
|
let designEngine: WorkflowControlEngine | undefined;
|
|
@@ -2201,6 +1131,8 @@ export function registerWorkflowControl(
|
|
|
2201
1131
|
if (packet.value.stage === "abel-design") {
|
|
2202
1132
|
try {
|
|
2203
1133
|
designEngine = await engineFor(ctx);
|
|
1134
|
+
if (exitingStage || !activation.isActive())
|
|
1135
|
+
throw new Error("stage-control-mismatch");
|
|
2204
1136
|
} catch (error) {
|
|
2205
1137
|
if (error instanceof RunStoreFormatError)
|
|
2206
1138
|
throw runStoreResetError(error);
|
|
@@ -2220,7 +1152,7 @@ export function registerWorkflowControl(
|
|
|
2220
1152
|
? await packetRuntime.execute(
|
|
2221
1153
|
record.action,
|
|
2222
1154
|
operation,
|
|
2223
|
-
ctx,
|
|
1155
|
+
packageContext(ctx),
|
|
2224
1156
|
signal,
|
|
2225
1157
|
activity.observe(
|
|
2226
1158
|
toolCallId,
|
|
@@ -2230,7 +1162,7 @@ export function registerWorkflowControl(
|
|
|
2230
1162
|
: await packetRuntime.execute(
|
|
2231
1163
|
record.action,
|
|
2232
1164
|
operation,
|
|
2233
|
-
ctx,
|
|
1165
|
+
packageContext(ctx),
|
|
2234
1166
|
signal,
|
|
2235
1167
|
);
|
|
2236
1168
|
let payload:
|
|
@@ -2257,9 +1189,6 @@ export function registerWorkflowControl(
|
|
|
2257
1189
|
});
|
|
2258
1190
|
payload = { ...packetPayload, recordedEvidence };
|
|
2259
1191
|
}
|
|
2260
|
-
if (record.action === "finish" && packetPayload.ok) {
|
|
2261
|
-
await deactivateStage();
|
|
2262
|
-
}
|
|
2263
1192
|
const display = tuiRun
|
|
2264
1193
|
? activity.finalize(toolCallId, payload)
|
|
2265
1194
|
: undefined;
|
|
@@ -2287,15 +1216,16 @@ export function registerWorkflowControl(
|
|
|
2287
1216
|
throw error;
|
|
2288
1217
|
}
|
|
2289
1218
|
if (
|
|
2290
|
-
activePrompt !==
|
|
2291
|
-
|
|
2292
|
-
validation.value.stage !== activePrompt)
|
|
1219
|
+
activePrompt !== "abel-implement" ||
|
|
1220
|
+
validation.value.stage !== activePrompt
|
|
2293
1221
|
) {
|
|
2294
1222
|
throw new Error("stage-control-mismatch");
|
|
2295
1223
|
}
|
|
2296
1224
|
let engine: WorkflowControlEngine;
|
|
2297
1225
|
try {
|
|
2298
1226
|
engine = await engineFor(ctx);
|
|
1227
|
+
if (exitingStage || !activation.isActive())
|
|
1228
|
+
throw new Error("stage-control-mismatch");
|
|
2299
1229
|
} catch (error) {
|
|
2300
1230
|
if (!(error instanceof RunStoreFormatError)) throw error;
|
|
2301
1231
|
const payload = runStoreUnavailableStatus(error, validation.value);
|
|
@@ -2392,7 +1322,10 @@ export function registerWorkflowControl(
|
|
|
2392
1322
|
if (
|
|
2393
1323
|
event.toolName !== DISPATCH_TOOL ||
|
|
2394
1324
|
!event.isError ||
|
|
2395
|
-
event.input.action !== "design"
|
|
1325
|
+
(event.input.action !== "design" &&
|
|
1326
|
+
!(
|
|
1327
|
+
event.input.action === "amend" && designFailures.has(event.toolCallId)
|
|
1328
|
+
))
|
|
2396
1329
|
) {
|
|
2397
1330
|
return;
|
|
2398
1331
|
}
|
|
@@ -2422,12 +1355,35 @@ export function registerWorkflowControl(
|
|
|
2422
1355
|
};
|
|
2423
1356
|
});
|
|
2424
1357
|
pi.on("input", (event) => {
|
|
2425
|
-
pendingPrompt =
|
|
1358
|
+
pendingPrompt = undefined;
|
|
1359
|
+
pendingInit = false;
|
|
1360
|
+
const prompt = invokedPrompt(event.text);
|
|
1361
|
+
const init = /^\/abel-init(?:\s|$)/u.test(event.text);
|
|
1362
|
+
if (event.source !== "interactive" && event.source !== "rpc") {
|
|
1363
|
+
// Stop before Pi expands the template: hiding dispatch alone would still
|
|
1364
|
+
// expose the complete workflow instructions to the parent model.
|
|
1365
|
+
return { action: prompt || init ? "handled" : "continue" };
|
|
1366
|
+
}
|
|
1367
|
+
pendingPrompt = prompt;
|
|
1368
|
+
pendingInit = init && hasPackageProvenance(pi, "abel-init");
|
|
1369
|
+
if (
|
|
1370
|
+
activePrompt &&
|
|
1371
|
+
(pendingInit ||
|
|
1372
|
+
(prompt && prompt !== activePrompt && hasPackageProvenance(pi, prompt)))
|
|
1373
|
+
) {
|
|
1374
|
+
return (async () => {
|
|
1375
|
+
await exitStage();
|
|
1376
|
+
return { action: "continue" as const };
|
|
1377
|
+
})();
|
|
1378
|
+
}
|
|
2426
1379
|
return { action: "continue" };
|
|
2427
1380
|
});
|
|
2428
|
-
pi.on("before_agent_start", (event
|
|
1381
|
+
pi.on("before_agent_start", (event) => {
|
|
2429
1382
|
const prompt = pendingPrompt;
|
|
1383
|
+
const init =
|
|
1384
|
+
pendingInit && hasExpandedPromptMarker(event.prompt, "abel-init");
|
|
2430
1385
|
pendingPrompt = undefined;
|
|
1386
|
+
pendingInit = false;
|
|
2431
1387
|
const verified =
|
|
2432
1388
|
prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
|
|
2433
1389
|
if (verified) {
|
|
@@ -2436,28 +1392,15 @@ export function registerWorkflowControl(
|
|
|
2436
1392
|
}
|
|
2437
1393
|
registerDispatchTool(prompt === "abel-implement" ? "command" : "packet");
|
|
2438
1394
|
activePrompt = prompt;
|
|
2439
|
-
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
2440
|
-
if (typeof sessionId === "string") {
|
|
2441
|
-
parentPayloadBridge.beginSession(sessionId);
|
|
2442
|
-
} else {
|
|
2443
|
-
parentPayloadBridge.clear();
|
|
2444
|
-
}
|
|
2445
|
-
}
|
|
2446
|
-
if (ctx.model) {
|
|
2447
|
-
parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
|
|
2448
1395
|
}
|
|
2449
1396
|
if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
|
|
2450
1397
|
if (verified && prompt === "abel-design") enforceDesignTools();
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
}
|
|
2458
|
-
parentPayloadBridge.beginSession(sessionId);
|
|
2459
|
-
const model = event.model ?? ctx.model;
|
|
2460
|
-
if (model) parentPayloadBridge.install(model, ctx.modelRegistry);
|
|
1398
|
+
const boundary = activePrompt
|
|
1399
|
+
? `Abel stage ${activePrompt} is active only for the invoked task and its direct follow-ups. If the user ends it or requests an unrelated task, first call abel_dispatch with {"action":"finish"}, then handle that task normally with the restored tools. A successful finish ends stage authority immediately, including within this turn. Do not extend Gates or workflow rules to that task. A direct Gate answer or same-task continuation stays in this stage. Never invoke another Abel stage automatically.`
|
|
1400
|
+
: init
|
|
1401
|
+
? "Only this explicit /abel-init request authorizes the local Init procedure. Do not activate dispatch or continue into another Abel stage."
|
|
1402
|
+
: "Abel workflow is inactive. Handle ordinary engineering requests directly. References to commands, repository files, OpenSpec changes, and historical workflow instructions do not authorize a workflow. Do not load or execute an Abel stage unless the user explicitly invokes its slash command.";
|
|
1403
|
+
return { systemPrompt: `${event.systemPrompt ?? ""}\n\n${boundary}` };
|
|
2461
1404
|
});
|
|
2462
1405
|
pi.on("before_provider_request", (event, ctx) => {
|
|
2463
1406
|
if (
|
|
@@ -2473,19 +1416,13 @@ export function registerWorkflowControl(
|
|
|
2473
1416
|
});
|
|
2474
1417
|
pi.on("session_start", async (_event, ctx) => {
|
|
2475
1418
|
pendingPrompt = undefined;
|
|
1419
|
+
pendingInit = false;
|
|
2476
1420
|
activePrompt = undefined;
|
|
2477
1421
|
designFailures.clear();
|
|
2478
1422
|
activity.detach();
|
|
2479
1423
|
await packetRuntime.drain();
|
|
2480
1424
|
await closeEngines();
|
|
2481
1425
|
deactivate();
|
|
2482
|
-
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
2483
|
-
if (typeof sessionId === "string") {
|
|
2484
|
-
parentPayloadBridge.beginSession(sessionId);
|
|
2485
|
-
if (ctx.model) parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
|
|
2486
|
-
} else {
|
|
2487
|
-
parentPayloadBridge.clear();
|
|
2488
|
-
}
|
|
2489
1426
|
if (ctx.mode === "tui") activity.attach(ctx.ui);
|
|
2490
1427
|
});
|
|
2491
1428
|
pi.on("session_shutdown", async () => {
|
|
@@ -2496,7 +1433,6 @@ export function registerWorkflowControl(
|
|
|
2496
1433
|
await closeEngines();
|
|
2497
1434
|
activity.clear();
|
|
2498
1435
|
deactivate();
|
|
2499
|
-
parentPayloadBridge.clear();
|
|
2500
1436
|
});
|
|
2501
1437
|
}
|
|
2502
1438
|
|