@abelxiaoxing/cadence 1.2.4 → 1.4.0
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 +155 -10
- package/package.json +5 -3
- package/prompts/abel-design.md +71 -17
- package/prompts/abel-diagnose.md +10 -1
- package/prompts/abel-implement.md +118 -20
- package/prompts/abel-init.md +9 -2
- package/src/apply-transaction.ts +16 -14
- package/src/artifact-store.ts +17 -0
- package/src/candidate-patch.ts +2 -1
- package/src/canonical.ts +16 -0
- package/src/change-contract.ts +268 -0
- package/src/child-session.ts +4 -3
- package/src/contracts.ts +141 -33
- package/src/control-contracts.ts +51 -1
- package/src/delivery-compiler.ts +148 -31
- package/src/design-control.ts +83 -15
- package/src/design-journal.ts +216 -72
- package/src/durable-workflow.ts +4385 -0
- package/src/implement-graph.ts +11 -16
- package/src/index.ts +399 -1079
- package/src/isolation-backend.ts +84 -23
- package/src/openspec-cli.ts +494 -0
- package/src/package-delivery.ts +486 -0
- package/src/package-verification.ts +509 -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 +14 -1
- package/src/submit-tool.ts +20 -2
- package/src/task-ledger.ts +14 -55
- 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 +97 -34
- package/src/workflow-engine.ts +28 -8285
- package/src/workflow-policy.ts +1427 -0
- package/src/workflow-state-machine.ts +4478 -0
- package/src/workspace-io-entry.mjs +58 -0
- package/src/workspace-io-worker.mjs +984 -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/index.ts
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
|
+
import { compareCanonicalStrings } from "./canonical.ts";
|
|
2
|
+
import { packageDeliverySource } from "./package-delivery.ts";
|
|
3
|
+
import {
|
|
4
|
+
changeVerificationResult,
|
|
5
|
+
executePackageVerification,
|
|
6
|
+
phaseVerificationResult,
|
|
7
|
+
} from "./package-verification.ts";
|
|
8
|
+
import { captureVerificationEnvironmentIdentity } from "./verification-environment.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
22
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
-
import { execFile } from "node:child_process";
|
|
8
23
|
import { createHash } from "node:crypto";
|
|
9
|
-
import {
|
|
10
|
-
lstatSync,
|
|
11
|
-
mkdirSync,
|
|
12
|
-
mkdtempSync,
|
|
13
|
-
readFileSync,
|
|
14
|
-
realpathSync,
|
|
15
|
-
rmSync,
|
|
16
|
-
} from "node:fs";
|
|
17
24
|
import { homedir } from "node:os";
|
|
18
25
|
import path, { dirname, join } from "node:path";
|
|
19
26
|
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { promisify } from "node:util";
|
|
21
27
|
import type {
|
|
22
28
|
AgentToolResult,
|
|
23
29
|
AgentToolUpdateCallback,
|
|
@@ -30,12 +36,9 @@ import { type Activation, activateTool, deactivateTool } from "./activation.ts";
|
|
|
30
36
|
import { loadAgentDefinitions } from "./agent-registry.ts";
|
|
31
37
|
import { runChildSession } from "./child-session.ts";
|
|
32
38
|
import {
|
|
33
|
-
type AtomicVerificationContract,
|
|
34
39
|
type DesignEvidenceResult,
|
|
35
|
-
isValidRelativePath,
|
|
36
40
|
LIMITS,
|
|
37
41
|
type StructuredVerificationContract,
|
|
38
|
-
verificationSteps,
|
|
39
42
|
} from "./contracts.ts";
|
|
40
43
|
import {
|
|
41
44
|
CONTROL_COMMAND_PARAMETERS,
|
|
@@ -43,22 +46,16 @@ import {
|
|
|
43
46
|
validateControlCommand,
|
|
44
47
|
} from "./control-contracts.ts";
|
|
45
48
|
import {
|
|
46
|
-
assessDeliveryTraceability,
|
|
47
|
-
compileImplementPlan,
|
|
48
49
|
DeliveryValidationError,
|
|
49
50
|
DesignPlanValidationError,
|
|
50
|
-
type GateApprovalProof,
|
|
51
|
-
parseGateAReceipt,
|
|
52
|
-
parseImplementPlan,
|
|
53
|
-
parseReadyReceipt,
|
|
54
51
|
} from "./delivery-compiler.ts";
|
|
55
52
|
import {
|
|
56
53
|
DesignController,
|
|
57
54
|
DesignFinalizationError,
|
|
58
55
|
validateDesignControlRequest,
|
|
59
56
|
} from "./design-control.ts";
|
|
60
|
-
import { canonicalJson
|
|
61
|
-
import {
|
|
57
|
+
import { canonicalJson } from "./implement-graph.ts";
|
|
58
|
+
import { inspectOpenSpecDelivery } from "./openspec-cli.ts";
|
|
62
59
|
import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
|
|
63
60
|
import { ParentPayloadBridge } from "./parent-payload-bridge.ts";
|
|
64
61
|
import { runtimeForWorkerRoute } from "./parent-provider.ts";
|
|
@@ -70,7 +67,7 @@ import {
|
|
|
70
67
|
type WorkerRoutePolicy,
|
|
71
68
|
} from "./route-policy.ts";
|
|
72
69
|
import { RunStoreFormatError, RunStoreMigrationError } from "./run-store.ts";
|
|
73
|
-
import {
|
|
70
|
+
import { isSafeRegularFile } from "./safe-path.ts";
|
|
74
71
|
import { resolveStateRoot, StateRootError } from "./state-root.ts";
|
|
75
72
|
import {
|
|
76
73
|
ACTIVITY_DETAILS_KEY,
|
|
@@ -79,17 +76,16 @@ import {
|
|
|
79
76
|
renderActivityResult,
|
|
80
77
|
type WorkflowActivityUpdate,
|
|
81
78
|
} from "./subagent-activity.ts";
|
|
82
|
-
import { classifyCandidateContextRequest } from "./submit-tool.ts";
|
|
83
|
-
import {
|
|
84
|
-
bindCurrentVerificationCapability,
|
|
85
|
-
isVerificationCapabilityCurrent,
|
|
86
|
-
type VerificationRunnerBinding,
|
|
87
|
-
} from "./verification-capability.ts";
|
|
88
79
|
import {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
} from "./workflow-engine.ts";
|
|
80
|
+
classifyCandidateContextRequest,
|
|
81
|
+
permitsContextRead,
|
|
82
|
+
} from "./submit-tool.ts";
|
|
83
|
+
import { openDurableWorkflowEngine } from "./workflow-engine.ts";
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
inspectOpenSpecDelivery,
|
|
87
|
+
type OpenSpecDeliveryInspection,
|
|
88
|
+
} from "./openspec-cli.ts";
|
|
93
89
|
|
|
94
90
|
export const DISPATCH_TOOL = "abel_dispatch";
|
|
95
91
|
const DESIGN_PARENT_READ_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
@@ -286,9 +282,9 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
|
|
|
286
282
|
operationId: { type: "string" },
|
|
287
283
|
gate: { type: "string", enum: ["gate-a"] },
|
|
288
284
|
contract: {
|
|
289
|
-
type: "string",
|
|
285
|
+
anyOf: [{ type: "string" }, { type: "object" }],
|
|
290
286
|
description:
|
|
291
|
-
"
|
|
287
|
+
"Structured ChangeContract with goal, acceptance, constraints and policy. Legacy prose remains readable; code hashes authority.",
|
|
292
288
|
},
|
|
293
289
|
},
|
|
294
290
|
required: ["operation", "runId", "operationId", "gate", "contract"],
|
|
@@ -356,11 +352,13 @@ function invokedPrompt(text: string): EligiblePrompt | undefined {
|
|
|
356
352
|
return ELIGIBLE_PROMPTS.find((candidate) => candidate === name);
|
|
357
353
|
}
|
|
358
354
|
|
|
359
|
-
|
|
355
|
+
type WorkflowPrompt = EligiblePrompt | "abel-init";
|
|
356
|
+
|
|
357
|
+
function promptMarker(name: WorkflowPrompt): string {
|
|
360
358
|
return `<!-- ABEL:PROMPT:${name} -->`;
|
|
361
359
|
}
|
|
362
360
|
|
|
363
|
-
function hasPackageProvenance(pi: ExtensionAPI, name:
|
|
361
|
+
function hasPackageProvenance(pi: ExtensionAPI, name: WorkflowPrompt): boolean {
|
|
364
362
|
const commands = pi
|
|
365
363
|
.getCommands()
|
|
366
364
|
.filter((candidate) => candidate.name === name);
|
|
@@ -375,7 +373,7 @@ function hasPackageProvenance(pi: ExtensionAPI, name: EligiblePrompt): boolean {
|
|
|
375
373
|
|
|
376
374
|
function hasExpandedPromptMarker(
|
|
377
375
|
prompt: string,
|
|
378
|
-
name:
|
|
376
|
+
name: WorkflowPrompt,
|
|
379
377
|
): boolean {
|
|
380
378
|
const requestEnd = prompt.lastIndexOf("</abel-request>");
|
|
381
379
|
if (requestEnd < 0) return false;
|
|
@@ -589,7 +587,16 @@ function safeDesignFailure(
|
|
|
589
587
|
error instanceof DesignFinalizationError ||
|
|
590
588
|
error instanceof DesignPlanValidationError ||
|
|
591
589
|
error instanceof DeliveryValidationError
|
|
592
|
-
? error
|
|
590
|
+
? error instanceof DesignFinalizationError
|
|
591
|
+
? [
|
|
592
|
+
...error.diagnostics.filter(
|
|
593
|
+
(code) =>
|
|
594
|
+
!error.openSpecDiagnostic ||
|
|
595
|
+
code !== "design-openspec-unavailable",
|
|
596
|
+
),
|
|
597
|
+
...(error.openSpecDiagnostic ? [error.openSpecDiagnostic] : []),
|
|
598
|
+
]
|
|
599
|
+
: error.diagnostics
|
|
593
600
|
: [];
|
|
594
601
|
const diagnostics = rawDiagnostics
|
|
595
602
|
.flatMap((candidate) => {
|
|
@@ -625,17 +632,21 @@ function safeDesignFailure(
|
|
|
625
632
|
"dependencyTaskId",
|
|
626
633
|
"producerTaskId",
|
|
627
634
|
"producerPhase",
|
|
635
|
+
"command",
|
|
636
|
+
"reason",
|
|
637
|
+
"systemCode",
|
|
638
|
+
"exitCode",
|
|
628
639
|
].includes(entry[0]) &&
|
|
629
640
|
typeof entry[1] === "string" &&
|
|
630
641
|
SAFE_DESIGN_ERROR_CODE.test(entry[1])
|
|
631
642
|
);
|
|
632
643
|
})
|
|
633
|
-
.sort(([left], [right]) => left
|
|
644
|
+
.sort(([left], [right]) => compareCanonicalStrings(left, right)),
|
|
634
645
|
);
|
|
635
646
|
return typeof admitted.code === "string" ? [admitted] : [];
|
|
636
647
|
})
|
|
637
648
|
.sort((left, right) =>
|
|
638
|
-
canonicalJson(left)
|
|
649
|
+
compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
|
|
639
650
|
)
|
|
640
651
|
.filter(
|
|
641
652
|
(candidate, index, values) =>
|
|
@@ -667,6 +678,11 @@ export interface WorkflowControlEngine {
|
|
|
667
678
|
onActivity?: (event: WorkflowActivityUpdate) => void,
|
|
668
679
|
): Promise<Record<string, unknown>>;
|
|
669
680
|
executeDesign?(request: unknown): Promise<Record<string, unknown>>;
|
|
681
|
+
executeAmendment?(
|
|
682
|
+
change: string,
|
|
683
|
+
batchId: string,
|
|
684
|
+
request: unknown,
|
|
685
|
+
): Promise<Record<string, unknown>>;
|
|
670
686
|
assertDesignRun?(runId: string): void;
|
|
671
687
|
recordDesignEvidence?(input: {
|
|
672
688
|
runId: string;
|
|
@@ -680,998 +696,10 @@ export type WorkflowControlEngineFactory = (
|
|
|
680
696
|
parentPayloadBridge: ParentPayloadBridge,
|
|
681
697
|
) => WorkflowControlEngine | Promise<WorkflowControlEngine>;
|
|
682
698
|
|
|
683
|
-
const PACKAGE_DELIVERY_MAX_BYTES = 16 * 1024 * 1024;
|
|
684
|
-
const execFileAsync = promisify(execFile);
|
|
685
|
-
|
|
686
|
-
export interface OpenSpecDeliveryInspection {
|
|
687
|
-
change: string;
|
|
688
|
-
schema: string;
|
|
689
|
-
planningComplete: boolean;
|
|
690
|
-
strictValid: boolean;
|
|
691
|
-
artifactPaths: string[];
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
export interface PackageDeliverySourceOptions {
|
|
695
|
-
inspectOpenSpec?: (
|
|
696
|
-
consumerRoot: string,
|
|
697
|
-
change: string,
|
|
698
|
-
) => Promise<OpenSpecDeliveryInspection>;
|
|
699
|
-
verifyGateProof?: (input: {
|
|
700
|
-
change: string;
|
|
701
|
-
gate: "gate-a" | "gate-b";
|
|
702
|
-
proof: GateApprovalProof;
|
|
703
|
-
}) => boolean;
|
|
704
|
-
verifyFinalizedDelivery?: (input: {
|
|
705
|
-
change: string;
|
|
706
|
-
deliveryRevision: number;
|
|
707
|
-
receiptHash: string;
|
|
708
|
-
gateA: GateApprovalProof;
|
|
709
|
-
gateB: GateApprovalProof;
|
|
710
|
-
planCanonicalHash: string;
|
|
711
|
-
}) => boolean;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
export interface PackageWorkflowDeliverySource extends WorkflowDeliverySource {
|
|
715
|
-
discoverLatest(input: {
|
|
716
|
-
stage: "abel-implement";
|
|
717
|
-
change: string;
|
|
718
|
-
}): Promise<WorkflowAvailableDelivery | undefined>;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
699
|
function sha256(value: Uint8Array | string): string {
|
|
722
700
|
return createHash("sha256").update(value).digest("hex");
|
|
723
701
|
}
|
|
724
702
|
|
|
725
|
-
function proofVerified(verify: () => boolean): boolean {
|
|
726
|
-
try {
|
|
727
|
-
return verify() === true;
|
|
728
|
-
} catch {
|
|
729
|
-
return false;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function normalizedTrackingArtifact(
|
|
734
|
-
bytes: Uint8Array,
|
|
735
|
-
taskIds: readonly string[],
|
|
736
|
-
): Buffer | undefined {
|
|
737
|
-
let text: string;
|
|
738
|
-
try {
|
|
739
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
740
|
-
} catch {
|
|
741
|
-
return undefined;
|
|
742
|
-
}
|
|
743
|
-
const identities = taskIds.map((taskId) => ({
|
|
744
|
-
taskId,
|
|
745
|
-
pattern: new RegExp(
|
|
746
|
-
`(?:^|[^A-Za-z0-9._:-])${taskId.replace(
|
|
747
|
-
/[.*+?^${}()|[\]\\]/gu,
|
|
748
|
-
"\\$&",
|
|
749
|
-
)}(?![A-Za-z0-9._:-])`,
|
|
750
|
-
"u",
|
|
751
|
-
),
|
|
752
|
-
matches: 0,
|
|
753
|
-
}));
|
|
754
|
-
const normalized = text
|
|
755
|
-
.split(/(?<=\n)/u)
|
|
756
|
-
.map((segment) => {
|
|
757
|
-
const line = segment.endsWith("\n") ? segment.slice(0, -1) : segment;
|
|
758
|
-
if (!/^\s*-\s+\[[ xX]\]/u.test(line)) return segment;
|
|
759
|
-
const matching = identities.filter(({ pattern }) => pattern.test(line));
|
|
760
|
-
if (matching.length !== 1) return segment;
|
|
761
|
-
matching[0].matches += 1;
|
|
762
|
-
return segment.replace(/^(\s*-\s+)\[[xX]\]/u, "$1[ ]");
|
|
763
|
-
})
|
|
764
|
-
.join("");
|
|
765
|
-
if (identities.some(({ matches }) => matches !== 1)) return undefined;
|
|
766
|
-
return Buffer.from(normalized, "utf8");
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function readPackageDeliveryFile(
|
|
770
|
-
root: string,
|
|
771
|
-
relative: string,
|
|
772
|
-
maximumBytes = PACKAGE_DELIVERY_MAX_BYTES,
|
|
773
|
-
): Buffer {
|
|
774
|
-
const observation = observeSafePath(root, relative);
|
|
775
|
-
if (observation.kind !== "file") {
|
|
776
|
-
throw new Error("delivery-file-unavailable");
|
|
777
|
-
}
|
|
778
|
-
const target = path.join(root, ...relative.split("/"));
|
|
779
|
-
const stat = lstatSync(target);
|
|
780
|
-
if (stat.size < 1 || stat.size > maximumBytes) {
|
|
781
|
-
throw new Error("delivery-file-size-invalid");
|
|
782
|
-
}
|
|
783
|
-
return readFileSync(target);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function deliveryRecord(value: unknown): Record<string, unknown> | undefined {
|
|
787
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
788
|
-
? (value as Record<string, unknown>)
|
|
789
|
-
: undefined;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function parseCommandJson(
|
|
793
|
-
value: string,
|
|
794
|
-
code: string,
|
|
795
|
-
): Record<string, unknown> {
|
|
796
|
-
let parsed: unknown;
|
|
797
|
-
try {
|
|
798
|
-
parsed = JSON.parse(value);
|
|
799
|
-
} catch {
|
|
800
|
-
throw new Error(code);
|
|
801
|
-
}
|
|
802
|
-
const record = deliveryRecord(parsed);
|
|
803
|
-
if (!record) throw new Error(code);
|
|
804
|
-
return record;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
export async function inspectOpenSpecDelivery(
|
|
808
|
-
consumerRoot: string,
|
|
809
|
-
change: string,
|
|
810
|
-
): Promise<OpenSpecDeliveryInspection> {
|
|
811
|
-
const [statusExecution, validationExecution] = await Promise.all([
|
|
812
|
-
execFileAsync("openspec", ["status", "--change", change, "--json"], {
|
|
813
|
-
cwd: consumerRoot,
|
|
814
|
-
encoding: "utf8",
|
|
815
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
816
|
-
}),
|
|
817
|
-
execFileAsync(
|
|
818
|
-
"openspec",
|
|
819
|
-
["validate", change, "--strict", "--json", "--no-interactive"],
|
|
820
|
-
{
|
|
821
|
-
cwd: consumerRoot,
|
|
822
|
-
encoding: "utf8",
|
|
823
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
824
|
-
},
|
|
825
|
-
),
|
|
826
|
-
]);
|
|
827
|
-
const status = parseCommandJson(
|
|
828
|
-
statusExecution.stdout,
|
|
829
|
-
"delivery-openspec-status-invalid",
|
|
830
|
-
);
|
|
831
|
-
const validation = parseCommandJson(
|
|
832
|
-
validationExecution.stdout,
|
|
833
|
-
"delivery-openspec-validation-invalid",
|
|
834
|
-
);
|
|
835
|
-
const changeRoot = path.resolve(consumerRoot, "openspec", "changes", change);
|
|
836
|
-
const artifactPathsRecord = deliveryRecord(status.artifactPaths);
|
|
837
|
-
const artifactPaths = artifactPathsRecord
|
|
838
|
-
? Object.values(artifactPathsRecord).flatMap((entry) => {
|
|
839
|
-
const record = deliveryRecord(entry);
|
|
840
|
-
if (!record || !Array.isArray(record.existingOutputPaths)) return [];
|
|
841
|
-
return record.existingOutputPaths.flatMap((candidate) => {
|
|
842
|
-
if (typeof candidate !== "string") return [];
|
|
843
|
-
const resolved = path.resolve(candidate);
|
|
844
|
-
const relative = path.relative(changeRoot, resolved);
|
|
845
|
-
return relative &&
|
|
846
|
-
!relative.startsWith(`..${path.sep}`) &&
|
|
847
|
-
relative !== ".." &&
|
|
848
|
-
!path.isAbsolute(relative)
|
|
849
|
-
? [relative.split(path.sep).join("/")]
|
|
850
|
-
: [];
|
|
851
|
-
});
|
|
852
|
-
})
|
|
853
|
-
: [];
|
|
854
|
-
const items = Array.isArray(validation.items) ? validation.items : [];
|
|
855
|
-
const strictValid =
|
|
856
|
-
items.length === 1 &&
|
|
857
|
-
deliveryRecord(items[0])?.id === change &&
|
|
858
|
-
deliveryRecord(items[0])?.valid === true;
|
|
859
|
-
if (
|
|
860
|
-
status.changeName !== change ||
|
|
861
|
-
typeof status.schemaName !== "string" ||
|
|
862
|
-
artifactPaths.length === 0
|
|
863
|
-
) {
|
|
864
|
-
throw new Error("delivery-openspec-status-invalid");
|
|
865
|
-
}
|
|
866
|
-
return {
|
|
867
|
-
change,
|
|
868
|
-
schema: status.schemaName,
|
|
869
|
-
planningComplete:
|
|
870
|
-
status.isPlanningComplete === true && status.isComplete === true,
|
|
871
|
-
strictValid,
|
|
872
|
-
artifactPaths: [...new Set(artifactPaths)].sort(),
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
export function packageDeliverySource(
|
|
877
|
-
consumerRoot: string,
|
|
878
|
-
options: PackageDeliverySourceOptions = {},
|
|
879
|
-
): PackageWorkflowDeliverySource {
|
|
880
|
-
const inspect = options.inspectOpenSpec ?? inspectOpenSpecDelivery;
|
|
881
|
-
const verifyGateProof = options.verifyGateProof;
|
|
882
|
-
const verifyFinalizedDelivery = options.verifyFinalizedDelivery;
|
|
883
|
-
return {
|
|
884
|
-
async discoverLatest(input) {
|
|
885
|
-
if (
|
|
886
|
-
input.stage !== "abel-implement" ||
|
|
887
|
-
!verifyGateProof ||
|
|
888
|
-
!verifyFinalizedDelivery
|
|
889
|
-
) {
|
|
890
|
-
return undefined;
|
|
891
|
-
}
|
|
892
|
-
try {
|
|
893
|
-
const changeRoot = `openspec/changes/${input.change}`;
|
|
894
|
-
const receiptBytes = readPackageDeliveryFile(
|
|
895
|
-
consumerRoot,
|
|
896
|
-
`${changeRoot}/ready.yaml`,
|
|
897
|
-
4 * 1024 * 1024,
|
|
898
|
-
);
|
|
899
|
-
const receipt = parseReadyReceipt(receiptBytes);
|
|
900
|
-
if (receipt.change !== input.change) return undefined;
|
|
901
|
-
const gateABytes = readPackageDeliveryFile(
|
|
902
|
-
consumerRoot,
|
|
903
|
-
`${changeRoot}/${receipt.approvals.gateA.path}`,
|
|
904
|
-
4 * 1024 * 1024,
|
|
905
|
-
);
|
|
906
|
-
if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
|
|
907
|
-
return undefined;
|
|
908
|
-
}
|
|
909
|
-
const gateA = parseGateAReceipt(gateABytes);
|
|
910
|
-
if (
|
|
911
|
-
gateA.change !== receipt.change ||
|
|
912
|
-
gateA.schema !== receipt.schema ||
|
|
913
|
-
!proofVerified(() =>
|
|
914
|
-
verifyGateProof({
|
|
915
|
-
change: input.change,
|
|
916
|
-
gate: "gate-a",
|
|
917
|
-
proof: gateA.approval,
|
|
918
|
-
}),
|
|
919
|
-
) ||
|
|
920
|
-
!proofVerified(() =>
|
|
921
|
-
verifyGateProof({
|
|
922
|
-
change: input.change,
|
|
923
|
-
gate: "gate-b",
|
|
924
|
-
proof: receipt.approvals.gateB,
|
|
925
|
-
}),
|
|
926
|
-
)
|
|
927
|
-
) {
|
|
928
|
-
return undefined;
|
|
929
|
-
}
|
|
930
|
-
const receiptHash = sha256(receiptBytes);
|
|
931
|
-
if (
|
|
932
|
-
!proofVerified(() =>
|
|
933
|
-
verifyFinalizedDelivery({
|
|
934
|
-
change: input.change,
|
|
935
|
-
deliveryRevision: receipt.deliveryRevision,
|
|
936
|
-
receiptHash,
|
|
937
|
-
gateA: gateA.approval,
|
|
938
|
-
gateB: receipt.approvals.gateB,
|
|
939
|
-
planCanonicalHash: receipt.plan.canonicalHash,
|
|
940
|
-
}),
|
|
941
|
-
)
|
|
942
|
-
) {
|
|
943
|
-
return undefined;
|
|
944
|
-
}
|
|
945
|
-
return {
|
|
946
|
-
deliveryRevision: receipt.deliveryRevision,
|
|
947
|
-
receiptHash,
|
|
948
|
-
};
|
|
949
|
-
} catch {
|
|
950
|
-
return undefined;
|
|
951
|
-
}
|
|
952
|
-
},
|
|
953
|
-
async load(input) {
|
|
954
|
-
if (input.stage !== "abel-implement") {
|
|
955
|
-
throw new DeliveryValidationError(["delivery-stage-invalid"]);
|
|
956
|
-
}
|
|
957
|
-
const changeRoot = `openspec/changes/${input.change}`;
|
|
958
|
-
const receiptRelative = `${changeRoot}/ready.yaml`;
|
|
959
|
-
const planRelative = `${changeRoot}/implement-plan.json`;
|
|
960
|
-
let receiptBytes: Buffer;
|
|
961
|
-
try {
|
|
962
|
-
receiptBytes = readPackageDeliveryFile(
|
|
963
|
-
consumerRoot,
|
|
964
|
-
receiptRelative,
|
|
965
|
-
4 * 1024 * 1024,
|
|
966
|
-
);
|
|
967
|
-
} catch {
|
|
968
|
-
throw new DeliveryValidationError(["delivery-receipt-unavailable"]);
|
|
969
|
-
}
|
|
970
|
-
let receipt: ReturnType<typeof parseReadyReceipt>;
|
|
971
|
-
try {
|
|
972
|
-
receipt = parseReadyReceipt(receiptBytes);
|
|
973
|
-
} catch {
|
|
974
|
-
throw new DeliveryValidationError([
|
|
975
|
-
"delivery-receipt-invalid",
|
|
976
|
-
"delivery-recompile-required",
|
|
977
|
-
]);
|
|
978
|
-
}
|
|
979
|
-
const diagnostics = new Set<string>();
|
|
980
|
-
if (receipt.change !== input.change) {
|
|
981
|
-
diagnostics.add("delivery-change-mismatch");
|
|
982
|
-
}
|
|
983
|
-
const receiptHash = sha256(receiptBytes);
|
|
984
|
-
if (
|
|
985
|
-
(input.deliveryRevision !== undefined &&
|
|
986
|
-
input.deliveryRevision !== receipt.deliveryRevision) ||
|
|
987
|
-
(input.receiptHash !== undefined && input.receiptHash !== receiptHash)
|
|
988
|
-
) {
|
|
989
|
-
diagnostics.add("delivery-revision-mismatch");
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
let inspection: OpenSpecDeliveryInspection | undefined;
|
|
993
|
-
try {
|
|
994
|
-
inspection = await inspect(consumerRoot, input.change);
|
|
995
|
-
} catch {
|
|
996
|
-
diagnostics.add("delivery-openspec-unavailable");
|
|
997
|
-
}
|
|
998
|
-
if (inspection) {
|
|
999
|
-
if (!inspection.strictValid) {
|
|
1000
|
-
diagnostics.add("delivery-openspec-strict-invalid");
|
|
1001
|
-
}
|
|
1002
|
-
if (!inspection.planningComplete) {
|
|
1003
|
-
diagnostics.add("delivery-openspec-incomplete");
|
|
1004
|
-
}
|
|
1005
|
-
if (inspection.schema !== receipt.schema) {
|
|
1006
|
-
diagnostics.add("delivery-schema-mismatch");
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
let gateABytes: Buffer | undefined;
|
|
1011
|
-
let gateA: ReturnType<typeof parseGateAReceipt> | undefined;
|
|
1012
|
-
try {
|
|
1013
|
-
gateABytes = readPackageDeliveryFile(
|
|
1014
|
-
consumerRoot,
|
|
1015
|
-
`${changeRoot}/${receipt.approvals.gateA.path}`,
|
|
1016
|
-
4 * 1024 * 1024,
|
|
1017
|
-
);
|
|
1018
|
-
if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
|
|
1019
|
-
diagnostics.add("delivery-gate-a-hash-mismatch");
|
|
1020
|
-
}
|
|
1021
|
-
gateA = parseGateAReceipt(gateABytes);
|
|
1022
|
-
if (
|
|
1023
|
-
gateA.change !== receipt.change ||
|
|
1024
|
-
gateA.schema !== receipt.schema
|
|
1025
|
-
) {
|
|
1026
|
-
diagnostics.add("delivery-gate-a-binding-mismatch");
|
|
1027
|
-
}
|
|
1028
|
-
} catch {
|
|
1029
|
-
diagnostics.add("delivery-gate-a-invalid");
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
const artifactBytes = new Map<string, Buffer>();
|
|
1033
|
-
const artifactHashes = new Map(
|
|
1034
|
-
receipt.artifacts.map((artifact) => [
|
|
1035
|
-
artifact.path,
|
|
1036
|
-
artifact.rawSha256,
|
|
1037
|
-
]),
|
|
1038
|
-
);
|
|
1039
|
-
for (const artifact of receipt.artifacts) {
|
|
1040
|
-
try {
|
|
1041
|
-
const bytes = readPackageDeliveryFile(
|
|
1042
|
-
consumerRoot,
|
|
1043
|
-
`${changeRoot}/${artifact.path}`,
|
|
1044
|
-
);
|
|
1045
|
-
artifactBytes.set(artifact.path, bytes);
|
|
1046
|
-
if (
|
|
1047
|
-
artifact.path !== receipt.traceability.taskPath &&
|
|
1048
|
-
sha256(bytes) !== artifact.rawSha256
|
|
1049
|
-
) {
|
|
1050
|
-
diagnostics.add(`delivery-artifact-hash-mismatch:${artifact.path}`);
|
|
1051
|
-
}
|
|
1052
|
-
} catch {
|
|
1053
|
-
diagnostics.add(`delivery-artifact-unavailable:${artifact.path}`);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
if (inspection) {
|
|
1057
|
-
const covered = new Set(receipt.artifacts.map((entry) => entry.path));
|
|
1058
|
-
const expected = new Set(inspection.artifactPaths);
|
|
1059
|
-
for (const relative of expected) {
|
|
1060
|
-
if (!isValidRelativePath(relative) || !covered.has(relative)) {
|
|
1061
|
-
diagnostics.add(`delivery-artifact-unbound:${relative}`);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
for (const relative of covered) {
|
|
1065
|
-
if (!expected.has(relative)) {
|
|
1066
|
-
diagnostics.add(`delivery-artifact-not-in-openspec:${relative}`);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
if (gateA) {
|
|
1071
|
-
if (!verifyGateProof) {
|
|
1072
|
-
diagnostics.add("delivery-gate-proof-verifier-unavailable");
|
|
1073
|
-
} else if (
|
|
1074
|
-
!proofVerified(() =>
|
|
1075
|
-
verifyGateProof({
|
|
1076
|
-
change: input.change,
|
|
1077
|
-
gate: "gate-a",
|
|
1078
|
-
proof: gateA.approval,
|
|
1079
|
-
}),
|
|
1080
|
-
)
|
|
1081
|
-
) {
|
|
1082
|
-
diagnostics.add("delivery-gate-a-proof-invalid");
|
|
1083
|
-
}
|
|
1084
|
-
for (const artifact of gateA.artifacts) {
|
|
1085
|
-
if (artifactHashes.get(artifact.path) !== artifact.rawSha256) {
|
|
1086
|
-
diagnostics.add(
|
|
1087
|
-
`delivery-gate-a-artifact-mismatch:${artifact.path}`,
|
|
1088
|
-
);
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
if (
|
|
1093
|
-
verifyGateProof &&
|
|
1094
|
-
!proofVerified(() =>
|
|
1095
|
-
verifyGateProof({
|
|
1096
|
-
change: input.change,
|
|
1097
|
-
gate: "gate-b",
|
|
1098
|
-
proof: receipt.approvals.gateB,
|
|
1099
|
-
}),
|
|
1100
|
-
)
|
|
1101
|
-
) {
|
|
1102
|
-
diagnostics.add("delivery-gate-b-proof-invalid");
|
|
1103
|
-
}
|
|
1104
|
-
if (gateA) {
|
|
1105
|
-
if (!verifyFinalizedDelivery) {
|
|
1106
|
-
diagnostics.add("delivery-finalization-verifier-unavailable");
|
|
1107
|
-
} else if (
|
|
1108
|
-
!proofVerified(() =>
|
|
1109
|
-
verifyFinalizedDelivery({
|
|
1110
|
-
change: input.change,
|
|
1111
|
-
deliveryRevision: receipt.deliveryRevision,
|
|
1112
|
-
receiptHash,
|
|
1113
|
-
gateA: gateA.approval,
|
|
1114
|
-
gateB: receipt.approvals.gateB,
|
|
1115
|
-
planCanonicalHash: receipt.plan.canonicalHash,
|
|
1116
|
-
}),
|
|
1117
|
-
)
|
|
1118
|
-
) {
|
|
1119
|
-
diagnostics.add("delivery-finalization-proof-invalid");
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
let plan: ReturnType<typeof parseImplementPlan> | undefined;
|
|
1124
|
-
let planBytes: Buffer | undefined;
|
|
1125
|
-
try {
|
|
1126
|
-
planBytes = readPackageDeliveryFile(consumerRoot, planRelative);
|
|
1127
|
-
plan = parseImplementPlan(planBytes);
|
|
1128
|
-
} catch {
|
|
1129
|
-
diagnostics.add("delivery-plan-invalid");
|
|
1130
|
-
}
|
|
1131
|
-
if (plan && planBytes) {
|
|
1132
|
-
if (plan.changeId !== input.change) {
|
|
1133
|
-
diagnostics.add("delivery-plan-change-mismatch");
|
|
1134
|
-
}
|
|
1135
|
-
let compiled: ReturnType<typeof compileImplementPlan> | undefined;
|
|
1136
|
-
try {
|
|
1137
|
-
compiled = compileImplementPlan(plan, { consumerRoot });
|
|
1138
|
-
} catch {
|
|
1139
|
-
diagnostics.add("delivery-verification-closure-invalid");
|
|
1140
|
-
}
|
|
1141
|
-
if (
|
|
1142
|
-
receipt.plan.path !== "implement-plan.json" ||
|
|
1143
|
-
receipt.plan.rawSha256 !== sha256(planBytes) ||
|
|
1144
|
-
receipt.plan.canonicalHash !== hashCanonicalValue(plan)
|
|
1145
|
-
) {
|
|
1146
|
-
diagnostics.add("delivery-plan-binding-invalid");
|
|
1147
|
-
}
|
|
1148
|
-
if (
|
|
1149
|
-
compiled &&
|
|
1150
|
-
canonicalJson(compiled.closure) !==
|
|
1151
|
-
canonicalJson(receipt.verificationClosure)
|
|
1152
|
-
) {
|
|
1153
|
-
diagnostics.add("delivery-verification-closure-mismatch");
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
if (plan) {
|
|
1158
|
-
const tasksBytes = artifactBytes.get(receipt.traceability.taskPath);
|
|
1159
|
-
const expectedTasksHash = artifactHashes.get(
|
|
1160
|
-
receipt.traceability.taskPath,
|
|
1161
|
-
);
|
|
1162
|
-
if (
|
|
1163
|
-
tasksBytes &&
|
|
1164
|
-
expectedTasksHash &&
|
|
1165
|
-
sha256(tasksBytes) !== expectedTasksHash
|
|
1166
|
-
) {
|
|
1167
|
-
const normalized = normalizedTrackingArtifact(
|
|
1168
|
-
tasksBytes,
|
|
1169
|
-
plan.tracking.taskIds,
|
|
1170
|
-
);
|
|
1171
|
-
if (!normalized || sha256(normalized) !== expectedTasksHash) {
|
|
1172
|
-
diagnostics.add(
|
|
1173
|
-
`delivery-artifact-hash-mismatch:${receipt.traceability.taskPath}`,
|
|
1174
|
-
);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
const specs = [...artifactBytes]
|
|
1178
|
-
.filter(
|
|
1179
|
-
([relative]) =>
|
|
1180
|
-
relative.startsWith("specs/") && relative.endsWith("/spec.md"),
|
|
1181
|
-
)
|
|
1182
|
-
.flatMap(([relative, bytes]) => {
|
|
1183
|
-
try {
|
|
1184
|
-
return [
|
|
1185
|
-
{
|
|
1186
|
-
path: relative,
|
|
1187
|
-
text: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
1188
|
-
},
|
|
1189
|
-
];
|
|
1190
|
-
} catch {
|
|
1191
|
-
diagnostics.add(`delivery-artifact-encoding-invalid:${relative}`);
|
|
1192
|
-
return [];
|
|
1193
|
-
}
|
|
1194
|
-
});
|
|
1195
|
-
if (!tasksBytes || specs.length === 0) {
|
|
1196
|
-
diagnostics.add("delivery-traceability-input-unavailable");
|
|
1197
|
-
} else {
|
|
1198
|
-
try {
|
|
1199
|
-
const traceability = assessDeliveryTraceability({
|
|
1200
|
-
tasksMarkdown: new TextDecoder("utf-8", { fatal: true }).decode(
|
|
1201
|
-
tasksBytes,
|
|
1202
|
-
),
|
|
1203
|
-
specs,
|
|
1204
|
-
plan,
|
|
1205
|
-
});
|
|
1206
|
-
if (!traceability.ok) {
|
|
1207
|
-
for (const diagnostic of traceability.diagnostics) {
|
|
1208
|
-
diagnostics.add(diagnostic);
|
|
1209
|
-
}
|
|
1210
|
-
} else if (
|
|
1211
|
-
canonicalJson(traceability.value) !==
|
|
1212
|
-
canonicalJson(receipt.traceability)
|
|
1213
|
-
) {
|
|
1214
|
-
diagnostics.add("delivery-traceability-binding-mismatch");
|
|
1215
|
-
}
|
|
1216
|
-
} catch {
|
|
1217
|
-
diagnostics.add("delivery-traceability-invalid");
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
if (!plan || !gateA || diagnostics.size > 0) {
|
|
1222
|
-
throw new DeliveryValidationError([...diagnostics]);
|
|
1223
|
-
}
|
|
1224
|
-
return {
|
|
1225
|
-
gate: "gate-b",
|
|
1226
|
-
revision: receipt.deliveryRevision,
|
|
1227
|
-
receiptHash,
|
|
1228
|
-
plan,
|
|
1229
|
-
approvalProofs: {
|
|
1230
|
-
gateA: structuredClone(gateA.approval),
|
|
1231
|
-
gateB: structuredClone(receipt.approvals.gateB),
|
|
1232
|
-
},
|
|
1233
|
-
};
|
|
1234
|
-
},
|
|
1235
|
-
};
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
function packageScriptInvocation(
|
|
1239
|
-
packageManager: string,
|
|
1240
|
-
script: string,
|
|
1241
|
-
args: string[],
|
|
1242
|
-
): string[] {
|
|
1243
|
-
return packageManager === "npm" || packageManager === "pnpm"
|
|
1244
|
-
? ["run", script, "--", ...args]
|
|
1245
|
-
: ["run", script, ...args];
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
function bindingFor(
|
|
1249
|
-
bindings: readonly VerificationRunnerBinding[],
|
|
1250
|
-
command: string,
|
|
1251
|
-
): VerificationRunnerBinding | undefined {
|
|
1252
|
-
return bindings.find((binding) => binding.command === command);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
function prepareRunnerBindings(
|
|
1256
|
-
root: string,
|
|
1257
|
-
bindings: readonly VerificationRunnerBinding[],
|
|
1258
|
-
): {
|
|
1259
|
-
bindings: VerificationRunnerBinding[];
|
|
1260
|
-
mounts: Array<{ source: string; target: string }>;
|
|
1261
|
-
cleanupRoot?: string;
|
|
1262
|
-
} {
|
|
1263
|
-
if (!bindings.some((binding) => binding.mountSource)) {
|
|
1264
|
-
return {
|
|
1265
|
-
bindings: bindings.map((binding) => ({
|
|
1266
|
-
...binding,
|
|
1267
|
-
...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
|
|
1268
|
-
})),
|
|
1269
|
-
mounts: [],
|
|
1270
|
-
};
|
|
1271
|
-
}
|
|
1272
|
-
const cleanupRoot = mkdtempSync(path.join(root, ".cadence-runners-"));
|
|
1273
|
-
try {
|
|
1274
|
-
const mounted = new Map<
|
|
1275
|
-
string,
|
|
1276
|
-
{ source: string; target: string; sandboxTarget: string }
|
|
1277
|
-
>();
|
|
1278
|
-
const prepared = bindings.map((binding) => {
|
|
1279
|
-
if (!binding.mountSource) return { ...binding };
|
|
1280
|
-
const source = path.resolve(binding.mountSource);
|
|
1281
|
-
const sourceStat = lstatSync(source);
|
|
1282
|
-
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
|
|
1283
|
-
throw new Error("runner-mount-unavailable");
|
|
1284
|
-
}
|
|
1285
|
-
let mount = mounted.get(source);
|
|
1286
|
-
if (!mount) {
|
|
1287
|
-
const target = path.join(cleanupRoot, String(mounted.size));
|
|
1288
|
-
mkdirSync(target, { mode: 0o700 });
|
|
1289
|
-
const sandboxRelative = path
|
|
1290
|
-
.relative(root, target)
|
|
1291
|
-
.split(path.sep)
|
|
1292
|
-
.join("/");
|
|
1293
|
-
mount = {
|
|
1294
|
-
source,
|
|
1295
|
-
target,
|
|
1296
|
-
sandboxTarget: `/workspace/${sandboxRelative}`,
|
|
1297
|
-
};
|
|
1298
|
-
mounted.set(source, mount);
|
|
1299
|
-
}
|
|
1300
|
-
const executable = realpathSync(binding.executablePath);
|
|
1301
|
-
const relative = path.relative(source, executable);
|
|
1302
|
-
if (
|
|
1303
|
-
relative === "" ||
|
|
1304
|
-
relative.startsWith("..") ||
|
|
1305
|
-
path.isAbsolute(relative)
|
|
1306
|
-
) {
|
|
1307
|
-
throw new Error("runner-mount-unavailable");
|
|
1308
|
-
}
|
|
1309
|
-
return {
|
|
1310
|
-
command: binding.command,
|
|
1311
|
-
executablePath: path.posix.join(
|
|
1312
|
-
mount.sandboxTarget,
|
|
1313
|
-
...relative.split(path.sep),
|
|
1314
|
-
),
|
|
1315
|
-
...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
|
|
1316
|
-
};
|
|
1317
|
-
});
|
|
1318
|
-
return {
|
|
1319
|
-
bindings: prepared,
|
|
1320
|
-
mounts: [...mounted.values()].map(({ source, sandboxTarget }) => ({
|
|
1321
|
-
source,
|
|
1322
|
-
target: sandboxTarget,
|
|
1323
|
-
})),
|
|
1324
|
-
cleanupRoot,
|
|
1325
|
-
};
|
|
1326
|
-
} catch (error) {
|
|
1327
|
-
rmSync(cleanupRoot, { recursive: true, force: true });
|
|
1328
|
-
throw error;
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
function verificationInvocation(
|
|
1333
|
-
step: AtomicVerificationContract,
|
|
1334
|
-
bindings: readonly VerificationRunnerBinding[],
|
|
1335
|
-
): { executable: string; args: string[] } | undefined {
|
|
1336
|
-
const reporter = step.kind === "vitest" ? ["--reporter=json"] : [];
|
|
1337
|
-
if (step.kind === "package-script") {
|
|
1338
|
-
const binding = bindingFor(bindings, step.packageManager);
|
|
1339
|
-
return binding
|
|
1340
|
-
? {
|
|
1341
|
-
executable: binding.executablePath,
|
|
1342
|
-
args: [
|
|
1343
|
-
...(binding.fixedArgs ?? []),
|
|
1344
|
-
...packageScriptInvocation(
|
|
1345
|
-
step.packageManager,
|
|
1346
|
-
step.script,
|
|
1347
|
-
step.args,
|
|
1348
|
-
),
|
|
1349
|
-
],
|
|
1350
|
-
}
|
|
1351
|
-
: undefined;
|
|
1352
|
-
}
|
|
1353
|
-
const runner = step.runner;
|
|
1354
|
-
if (runner.kind === "package-script") {
|
|
1355
|
-
const binding = bindingFor(bindings, runner.packageManager);
|
|
1356
|
-
const args =
|
|
1357
|
-
step.kind === "vitest"
|
|
1358
|
-
? [...step.testFiles, ...step.args, ...reporter]
|
|
1359
|
-
: step.args;
|
|
1360
|
-
return binding
|
|
1361
|
-
? {
|
|
1362
|
-
executable: binding.executablePath,
|
|
1363
|
-
args: [
|
|
1364
|
-
...(binding.fixedArgs ?? []),
|
|
1365
|
-
...packageScriptInvocation(
|
|
1366
|
-
runner.packageManager,
|
|
1367
|
-
runner.script,
|
|
1368
|
-
args,
|
|
1369
|
-
),
|
|
1370
|
-
],
|
|
1371
|
-
}
|
|
1372
|
-
: undefined;
|
|
1373
|
-
}
|
|
1374
|
-
if (runner.kind === "local-binary") {
|
|
1375
|
-
const binding = bindingFor(bindings, runner.executable);
|
|
1376
|
-
return {
|
|
1377
|
-
executable:
|
|
1378
|
-
binding?.executablePath ??
|
|
1379
|
-
`/workspace/node_modules/.bin/${runner.executable}`,
|
|
1380
|
-
args: [
|
|
1381
|
-
...(binding?.fixedArgs ?? []),
|
|
1382
|
-
...(step.kind === "vitest"
|
|
1383
|
-
? [...step.testFiles, ...step.args, ...reporter]
|
|
1384
|
-
: step.args),
|
|
1385
|
-
],
|
|
1386
|
-
};
|
|
1387
|
-
}
|
|
1388
|
-
const command = runner.kind === "node" ? "node" : "npx";
|
|
1389
|
-
const binding = bindingFor(bindings, command);
|
|
1390
|
-
if (!binding) return undefined;
|
|
1391
|
-
return {
|
|
1392
|
-
executable: binding.executablePath,
|
|
1393
|
-
args:
|
|
1394
|
-
runner.kind === "node"
|
|
1395
|
-
? [...(binding.fixedArgs ?? []), runner.script, ...step.args]
|
|
1396
|
-
: [
|
|
1397
|
-
...(binding.fixedArgs ?? []),
|
|
1398
|
-
"--no-install",
|
|
1399
|
-
runner.executable,
|
|
1400
|
-
...(step.kind === "vitest" ? step.testFiles : []),
|
|
1401
|
-
...step.args,
|
|
1402
|
-
...reporter,
|
|
1403
|
-
],
|
|
1404
|
-
};
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
function vitestReport(stdout: string):
|
|
1408
|
-
| {
|
|
1409
|
-
total: number;
|
|
1410
|
-
failed: number;
|
|
1411
|
-
success: boolean;
|
|
1412
|
-
failures: string[];
|
|
1413
|
-
failedText: string[];
|
|
1414
|
-
}
|
|
1415
|
-
| undefined {
|
|
1416
|
-
let value: unknown;
|
|
1417
|
-
try {
|
|
1418
|
-
value = JSON.parse(stdout);
|
|
1419
|
-
} catch {
|
|
1420
|
-
return undefined;
|
|
1421
|
-
}
|
|
1422
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1423
|
-
return undefined;
|
|
1424
|
-
}
|
|
1425
|
-
const report = value as Record<string, unknown>;
|
|
1426
|
-
if (
|
|
1427
|
-
!Number.isSafeInteger(report.numTotalTests) ||
|
|
1428
|
-
!Number.isSafeInteger(report.numFailedTests) ||
|
|
1429
|
-
typeof report.success !== "boolean"
|
|
1430
|
-
) {
|
|
1431
|
-
return undefined;
|
|
1432
|
-
}
|
|
1433
|
-
const failedAssertions = (
|
|
1434
|
-
Array.isArray(report.testResults) ? report.testResults : []
|
|
1435
|
-
).flatMap((suite): Array<{ identity: string; text: string[] }> => {
|
|
1436
|
-
if (!suite || typeof suite !== "object" || Array.isArray(suite)) return [];
|
|
1437
|
-
const record = suite as Record<string, unknown>;
|
|
1438
|
-
const suiteName =
|
|
1439
|
-
typeof record.name === "string"
|
|
1440
|
-
? record.name.replace(/^\/workspace\//u, "")
|
|
1441
|
-
: "unknown-suite";
|
|
1442
|
-
if (!Array.isArray(record.assertionResults)) return [];
|
|
1443
|
-
return record.assertionResults.flatMap(
|
|
1444
|
-
(assertion): Array<{ identity: string; text: string[] }> => {
|
|
1445
|
-
if (
|
|
1446
|
-
!assertion ||
|
|
1447
|
-
typeof assertion !== "object" ||
|
|
1448
|
-
Array.isArray(assertion)
|
|
1449
|
-
) {
|
|
1450
|
-
return [];
|
|
1451
|
-
}
|
|
1452
|
-
const entry = assertion as Record<string, unknown>;
|
|
1453
|
-
if (entry.status !== "failed") return [];
|
|
1454
|
-
const title =
|
|
1455
|
-
typeof entry.fullName === "string"
|
|
1456
|
-
? entry.fullName
|
|
1457
|
-
: typeof entry.title === "string"
|
|
1458
|
-
? entry.title
|
|
1459
|
-
: "unknown-assertion";
|
|
1460
|
-
const diagnostics = Array.isArray(entry.failureMessages)
|
|
1461
|
-
? entry.failureMessages.filter(
|
|
1462
|
-
(message): message is string => typeof message === "string",
|
|
1463
|
-
)
|
|
1464
|
-
: [];
|
|
1465
|
-
return [
|
|
1466
|
-
{
|
|
1467
|
-
identity: `${suiteName}\0${title}`,
|
|
1468
|
-
text: [title, ...diagnostics],
|
|
1469
|
-
},
|
|
1470
|
-
];
|
|
1471
|
-
},
|
|
1472
|
-
);
|
|
1473
|
-
});
|
|
1474
|
-
return {
|
|
1475
|
-
total: report.numTotalTests as number,
|
|
1476
|
-
failed: report.numFailedTests as number,
|
|
1477
|
-
success: report.success,
|
|
1478
|
-
failures: failedAssertions.map((failure) => failure.identity),
|
|
1479
|
-
failedText: failedAssertions.flatMap((failure) => failure.text),
|
|
1480
|
-
};
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
function normalizedFailureIdentities(input: {
|
|
1484
|
-
step: AtomicVerificationContract;
|
|
1485
|
-
report?: ReturnType<typeof vitestReport>;
|
|
1486
|
-
output: string;
|
|
1487
|
-
exitCode: number;
|
|
1488
|
-
}): string[] {
|
|
1489
|
-
const vitestFailures = input.report?.failures ?? [];
|
|
1490
|
-
if (vitestFailures.length > 0) {
|
|
1491
|
-
return [...new Set(vitestFailures)]
|
|
1492
|
-
.sort()
|
|
1493
|
-
.map((failure) => sha256(`vitest-failure\0${failure}`));
|
|
1494
|
-
}
|
|
1495
|
-
const normalizedOutput = input.output
|
|
1496
|
-
.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu"), "")
|
|
1497
|
-
.replace(/\r\n?/gu, "\n")
|
|
1498
|
-
.replace(/\b\d+(?:\.\d+)?(?:ms|s)\b/gu, "<duration>")
|
|
1499
|
-
.slice(0, 64 * 1024);
|
|
1500
|
-
return [
|
|
1501
|
-
sha256(
|
|
1502
|
-
canonicalJson({
|
|
1503
|
-
verificationId: input.step.id,
|
|
1504
|
-
exitCode: input.exitCode,
|
|
1505
|
-
output: normalizedOutput,
|
|
1506
|
-
}),
|
|
1507
|
-
),
|
|
1508
|
-
];
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
export async function executePackageVerification(input: {
|
|
1512
|
-
root: string;
|
|
1513
|
-
dependencyOwner: string;
|
|
1514
|
-
verification: StructuredVerificationContract;
|
|
1515
|
-
signal: AbortSignal;
|
|
1516
|
-
}) {
|
|
1517
|
-
const capability = bindCurrentVerificationCapability(
|
|
1518
|
-
input.root,
|
|
1519
|
-
input.verification,
|
|
1520
|
-
{ dependencyOwner: input.dependencyOwner },
|
|
1521
|
-
);
|
|
1522
|
-
if (!capability.ok) {
|
|
1523
|
-
return {
|
|
1524
|
-
ok: false as const,
|
|
1525
|
-
kind: "paused" as const,
|
|
1526
|
-
code: capability.diagnostic.code,
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
|
-
let runners: ReturnType<typeof prepareRunnerBindings>;
|
|
1530
|
-
try {
|
|
1531
|
-
runners = prepareRunnerBindings(
|
|
1532
|
-
input.root,
|
|
1533
|
-
capability.value.runnerBindings,
|
|
1534
|
-
);
|
|
1535
|
-
} catch {
|
|
1536
|
-
return {
|
|
1537
|
-
ok: false as const,
|
|
1538
|
-
kind: "paused" as const,
|
|
1539
|
-
code: "runner-mount-unavailable",
|
|
1540
|
-
};
|
|
1541
|
-
}
|
|
1542
|
-
try {
|
|
1543
|
-
const nodeModules = path.join(input.dependencyOwner, "node_modules");
|
|
1544
|
-
const dependency = lstatSync(nodeModules, { throwIfNoEntry: false });
|
|
1545
|
-
const mounts = [] as Array<{
|
|
1546
|
-
source: string;
|
|
1547
|
-
target: string;
|
|
1548
|
-
writable?: boolean;
|
|
1549
|
-
}>;
|
|
1550
|
-
if (dependency) {
|
|
1551
|
-
if (!dependency.isDirectory() || dependency.isSymbolicLink()) {
|
|
1552
|
-
return {
|
|
1553
|
-
ok: false as const,
|
|
1554
|
-
kind: "paused" as const,
|
|
1555
|
-
code: "dependency-path-unsafe",
|
|
1556
|
-
};
|
|
1557
|
-
}
|
|
1558
|
-
const target = path.join(input.root, "node_modules");
|
|
1559
|
-
const targetStat = lstatSync(target, { throwIfNoEntry: false });
|
|
1560
|
-
if (
|
|
1561
|
-
targetStat &&
|
|
1562
|
-
(!targetStat.isDirectory() || targetStat.isSymbolicLink())
|
|
1563
|
-
) {
|
|
1564
|
-
return {
|
|
1565
|
-
ok: false as const,
|
|
1566
|
-
kind: "paused" as const,
|
|
1567
|
-
code: "dependency-path-unsafe",
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
|
-
if (!targetStat) mkdirSync(target, { mode: 0o700 });
|
|
1571
|
-
mounts.push({ source: nodeModules, target: "/workspace/node_modules" });
|
|
1572
|
-
}
|
|
1573
|
-
mounts.push(...runners.mounts);
|
|
1574
|
-
const isolation = new BubblewrapIsolationBackend();
|
|
1575
|
-
const steps = verificationSteps(input.verification);
|
|
1576
|
-
let final:
|
|
1577
|
-
| {
|
|
1578
|
-
exitCode: number;
|
|
1579
|
-
classification: AtomicVerificationContract["classification"];
|
|
1580
|
-
diagnostic: { kind: "assertion" | "compiler"; id: string };
|
|
1581
|
-
}
|
|
1582
|
-
| undefined;
|
|
1583
|
-
for (const step of steps) {
|
|
1584
|
-
const invocation = verificationInvocation(step, runners.bindings);
|
|
1585
|
-
if (!invocation) {
|
|
1586
|
-
return {
|
|
1587
|
-
ok: false as const,
|
|
1588
|
-
kind: "paused" as const,
|
|
1589
|
-
code: "runner-missing",
|
|
1590
|
-
};
|
|
1591
|
-
}
|
|
1592
|
-
const executed = await isolation.run({
|
|
1593
|
-
root: input.root,
|
|
1594
|
-
executable: invocation.executable,
|
|
1595
|
-
args: invocation.args,
|
|
1596
|
-
mounts,
|
|
1597
|
-
environment: { CI: "1" },
|
|
1598
|
-
signal: input.signal,
|
|
1599
|
-
});
|
|
1600
|
-
if (!executed.ok) {
|
|
1601
|
-
if (executed.state === "cancelled") throw input.signal.reason;
|
|
1602
|
-
return {
|
|
1603
|
-
ok: false as const,
|
|
1604
|
-
kind: "paused" as const,
|
|
1605
|
-
code: executed.code,
|
|
1606
|
-
};
|
|
1607
|
-
}
|
|
1608
|
-
const report =
|
|
1609
|
-
step.kind === "vitest" ? vitestReport(executed.stdout) : undefined;
|
|
1610
|
-
const output = `${executed.stdout}\n${executed.stderr}`;
|
|
1611
|
-
const identity =
|
|
1612
|
-
step.classification !== "expected-red" ||
|
|
1613
|
-
(step.kind === "vitest"
|
|
1614
|
-
? report?.failedText.some((text) =>
|
|
1615
|
-
text.includes(step.expectedFailure ?? ""),
|
|
1616
|
-
) === true
|
|
1617
|
-
: output.includes(step.expectedFailure ?? ""));
|
|
1618
|
-
const accepted =
|
|
1619
|
-
step.classification === "expected-red"
|
|
1620
|
-
? executed.exitCode !== 0 &&
|
|
1621
|
-
identity &&
|
|
1622
|
-
(step.kind !== "vitest" ||
|
|
1623
|
-
(report !== undefined &&
|
|
1624
|
-
report.failed > 0 &&
|
|
1625
|
-
report.total >= step.minTests))
|
|
1626
|
-
: executed.exitCode === 0 &&
|
|
1627
|
-
(step.kind !== "vitest" ||
|
|
1628
|
-
(report?.success === true && report.total >= step.minTests));
|
|
1629
|
-
if (!accepted) {
|
|
1630
|
-
return {
|
|
1631
|
-
ok: false as const,
|
|
1632
|
-
kind: "retryable" as const,
|
|
1633
|
-
code:
|
|
1634
|
-
step.classification === "expected-red" && executed.exitCode === 0
|
|
1635
|
-
? "red-not-witnessed"
|
|
1636
|
-
: "verification-rejected",
|
|
1637
|
-
failureIdentities: normalizedFailureIdentities({
|
|
1638
|
-
step,
|
|
1639
|
-
report,
|
|
1640
|
-
output,
|
|
1641
|
-
exitCode: executed.exitCode,
|
|
1642
|
-
}),
|
|
1643
|
-
};
|
|
1644
|
-
}
|
|
1645
|
-
if (!isVerificationCapabilityCurrent(input.root, capability.value)) {
|
|
1646
|
-
return {
|
|
1647
|
-
ok: false as const,
|
|
1648
|
-
kind: "retryable" as const,
|
|
1649
|
-
code: "verification-input-unavailable",
|
|
1650
|
-
};
|
|
1651
|
-
}
|
|
1652
|
-
final = {
|
|
1653
|
-
exitCode: executed.exitCode,
|
|
1654
|
-
classification: step.classification,
|
|
1655
|
-
diagnostic: {
|
|
1656
|
-
kind: step.kind === "vitest" ? "assertion" : "compiler",
|
|
1657
|
-
id: step.id,
|
|
1658
|
-
},
|
|
1659
|
-
};
|
|
1660
|
-
}
|
|
1661
|
-
return final
|
|
1662
|
-
? { ok: true as const, ...final }
|
|
1663
|
-
: {
|
|
1664
|
-
ok: false as const,
|
|
1665
|
-
kind: "paused" as const,
|
|
1666
|
-
code: "verification-contract-unsupported",
|
|
1667
|
-
};
|
|
1668
|
-
} finally {
|
|
1669
|
-
if (runners.cleanupRoot) {
|
|
1670
|
-
rmSync(runners.cleanupRoot, { recursive: true, force: true });
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
703
|
function affectedVerification(
|
|
1676
704
|
task: Parameters<
|
|
1677
705
|
Parameters<typeof openDurableWorkflowEngine>[0]["proposeCandidate"]
|
|
@@ -1732,7 +760,39 @@ export function openPackageWorkflowControlEngine(
|
|
|
1732
760
|
if (!implementationAgent) {
|
|
1733
761
|
throw new Error("implementation-worker-agent-unavailable");
|
|
1734
762
|
}
|
|
763
|
+
const hostLimit = (name: string, fallback: number): number => {
|
|
764
|
+
const value =
|
|
765
|
+
process.env[name] === undefined ? fallback : Number(process.env[name]);
|
|
766
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
767
|
+
throw new Error("verification-host-limit-invalid");
|
|
768
|
+
return value;
|
|
769
|
+
};
|
|
770
|
+
const maxReportBytes = hostLimit(
|
|
771
|
+
"ABEL_VERIFICATION_MAX_REPORT_BYTES",
|
|
772
|
+
64 * 1024 * 1024,
|
|
773
|
+
);
|
|
774
|
+
const workHardLimit = hostLimit("ABEL_WORK_MAX_UNITS", 512);
|
|
1735
775
|
const engine = openDurableWorkflowEngine({
|
|
776
|
+
workHardLimit,
|
|
777
|
+
verificationPolicy: "report-file-v4",
|
|
778
|
+
verificationEnvironment: (plan, signal) =>
|
|
779
|
+
captureVerificationEnvironmentIdentity(
|
|
780
|
+
consumerRoot,
|
|
781
|
+
[
|
|
782
|
+
...plan.tasks.flatMap((task) => [
|
|
783
|
+
...Object.values(task.phases).map((phase) => phase.verification),
|
|
784
|
+
task.affectedVerification,
|
|
785
|
+
task.repairVerification,
|
|
786
|
+
]),
|
|
787
|
+
plan.verification.baseline.fullSuite,
|
|
788
|
+
plan.verification.change.fullSuite,
|
|
789
|
+
plan.verification.change.postApply,
|
|
790
|
+
...(plan.verification.agentsCheckpoint.verification
|
|
791
|
+
? [plan.verification.agentsCheckpoint.verification]
|
|
792
|
+
: []),
|
|
793
|
+
],
|
|
794
|
+
signal,
|
|
795
|
+
),
|
|
1736
796
|
consumerRoot,
|
|
1737
797
|
stateRoot,
|
|
1738
798
|
deliverySource: packageDeliverySource(consumerRoot, {
|
|
@@ -1785,10 +845,28 @@ export function openPackageWorkflowControlEngine(
|
|
|
1785
845
|
].sort(),
|
|
1786
846
|
}
|
|
1787
847
|
: {
|
|
1788
|
-
read: [
|
|
848
|
+
read: [
|
|
849
|
+
...new Set(
|
|
850
|
+
taskPhases.flatMap((boundary) => [
|
|
851
|
+
...boundary.read,
|
|
852
|
+
...boundary.write,
|
|
853
|
+
...boundary.delete,
|
|
854
|
+
]),
|
|
855
|
+
),
|
|
856
|
+
].sort(),
|
|
1789
857
|
write: [...phase.write],
|
|
1790
858
|
delete: [...phase.delete],
|
|
1791
859
|
};
|
|
860
|
+
executionBoundary.read = [
|
|
861
|
+
...new Set([
|
|
862
|
+
...executionBoundary.read,
|
|
863
|
+
...(input.contextReadPaths ?? []).filter(
|
|
864
|
+
(relative) =>
|
|
865
|
+
permitsContextRead(relative, input.task.roots) &&
|
|
866
|
+
isSafeRegularFile(input.workspaceRoot, relative),
|
|
867
|
+
),
|
|
868
|
+
]),
|
|
869
|
+
].sort();
|
|
1792
870
|
const phaseContract = {
|
|
1793
871
|
candidateId: input.candidateArtifact.identity.candidateId,
|
|
1794
872
|
taskId: input.taskId,
|
|
@@ -1808,6 +886,9 @@ export function openPackageWorkflowControlEngine(
|
|
|
1808
886
|
...(input.contextRequest
|
|
1809
887
|
? { requestedContext: structuredClone(input.contextRequest) }
|
|
1810
888
|
: {}),
|
|
889
|
+
...(input.recoveryFeedback
|
|
890
|
+
? { recoveryFeedback: structuredClone(input.recoveryFeedback) }
|
|
891
|
+
: {}),
|
|
1811
892
|
...(input.repair
|
|
1812
893
|
? {
|
|
1813
894
|
repair: {
|
|
@@ -1829,6 +910,7 @@ export function openPackageWorkflowControlEngine(
|
|
|
1829
910
|
input.task.objective,
|
|
1830
911
|
input.task.context.agents,
|
|
1831
912
|
input.task.context.contract,
|
|
913
|
+
"Use recoveryFeedback to change the failing approach. For compact-patch, prefer exact replace operations and omit unchanged bodies; submit one complete atomic patch. Never repeat an unchanged failing submission or weaken verification to obtain a pass.",
|
|
1832
914
|
`<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
|
|
1833
915
|
].join("\n\n"),
|
|
1834
916
|
requestId: childRequestId(input.operationId, input.taskId, input.phase),
|
|
@@ -1864,7 +946,7 @@ export function openPackageWorkflowControlEngine(
|
|
|
1864
946
|
return { kind: "approval-needed", code: child.failure.code };
|
|
1865
947
|
}
|
|
1866
948
|
if (child.failure.kind === "result-limit") {
|
|
1867
|
-
return { kind: "
|
|
949
|
+
return { kind: "retryable", code: "needs-task-split" };
|
|
1868
950
|
}
|
|
1869
951
|
const attemptDiagnostic = {
|
|
1870
952
|
finalCategory: child.classification.finalCategory,
|
|
@@ -1883,6 +965,7 @@ export function openPackageWorkflowControlEngine(
|
|
|
1883
965
|
if (result.kind === "context-request") {
|
|
1884
966
|
return classifyCandidateContextRequest(result, {
|
|
1885
967
|
phase: input.phase,
|
|
968
|
+
contextReadRoots: input.task.roots,
|
|
1886
969
|
readPaths: phase.read,
|
|
1887
970
|
writePaths: phase.write,
|
|
1888
971
|
deletePaths: phase.delete,
|
|
@@ -1915,44 +998,37 @@ export function openPackageWorkflowControlEngine(
|
|
|
1915
998
|
paths: [...result.paths],
|
|
1916
999
|
};
|
|
1917
1000
|
},
|
|
1918
|
-
verifyPhase: (input) =>
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1001
|
+
verifyPhase: async (input) =>
|
|
1002
|
+
phaseVerificationResult(
|
|
1003
|
+
await executePackageVerification({
|
|
1004
|
+
maxReportBytes,
|
|
1005
|
+
executionWritePaths: input.executionWritePaths,
|
|
1006
|
+
root: input.root,
|
|
1007
|
+
dependencyOwner: consumerRoot,
|
|
1008
|
+
verification: input.verification,
|
|
1009
|
+
signal: input.signal,
|
|
1010
|
+
}),
|
|
1011
|
+
),
|
|
1925
1012
|
verifyChange: async (input) => {
|
|
1926
1013
|
const verifications = input.verification
|
|
1927
1014
|
? [input.verification]
|
|
1928
1015
|
: input.plan.tasks.map((task) => affectedVerification(task));
|
|
1929
1016
|
for (const verification of verifications) {
|
|
1930
1017
|
const result = await executePackageVerification({
|
|
1018
|
+
maxReportBytes,
|
|
1019
|
+
executionWritePaths: input.plan.tasks.flatMap((task) =>
|
|
1020
|
+
Object.values(task.phases).flatMap((phase) => [
|
|
1021
|
+
...phase.write,
|
|
1022
|
+
...phase.delete,
|
|
1023
|
+
]),
|
|
1024
|
+
),
|
|
1931
1025
|
root: input.root,
|
|
1932
1026
|
dependencyOwner: consumerRoot,
|
|
1933
1027
|
verification,
|
|
1934
1028
|
signal: input.signal,
|
|
1935
1029
|
});
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
"runner-missing",
|
|
1939
|
-
"script-missing",
|
|
1940
|
-
"script-command-mismatch",
|
|
1941
|
-
"local-executable-missing",
|
|
1942
|
-
]).has(result.code);
|
|
1943
|
-
return {
|
|
1944
|
-
ok: false,
|
|
1945
|
-
kind: adapter
|
|
1946
|
-
? ("verification-adapter" as const)
|
|
1947
|
-
: result.kind === "paused"
|
|
1948
|
-
? ("environment" as const)
|
|
1949
|
-
: ("verification" as const),
|
|
1950
|
-
code: result.code,
|
|
1951
|
-
...(result.failureIdentities
|
|
1952
|
-
? { failureIdentities: result.failureIdentities }
|
|
1953
|
-
: {}),
|
|
1954
|
-
};
|
|
1955
|
-
}
|
|
1030
|
+
const observed = changeVerificationResult(result);
|
|
1031
|
+
if (!observed.ok) return observed;
|
|
1956
1032
|
}
|
|
1957
1033
|
return {
|
|
1958
1034
|
ok: true,
|
|
@@ -2000,6 +1076,11 @@ export function openPackageWorkflowControlEngine(
|
|
|
2000
1076
|
executeDesign(request: unknown) {
|
|
2001
1077
|
return design.execute(request);
|
|
2002
1078
|
},
|
|
1079
|
+
executeAmendment(change: string, batchId: string, request: unknown) {
|
|
1080
|
+
return engine.amend(change, batchId, request, (assertAuthority) =>
|
|
1081
|
+
design.executeWithAuthority(request, assertAuthority),
|
|
1082
|
+
);
|
|
1083
|
+
},
|
|
2003
1084
|
assertDesignRun(runId: string) {
|
|
2004
1085
|
design.assertDesignRun(runId);
|
|
2005
1086
|
},
|
|
@@ -2066,8 +1147,11 @@ export function registerWorkflowControl(
|
|
|
2066
1147
|
const parentPayloadBridge = new ParentPayloadBridge();
|
|
2067
1148
|
const activity = new ActivityController();
|
|
2068
1149
|
const engines = new Map<string, Promise<WorkflowControlEngine>>();
|
|
1150
|
+
const designOperations = new Set<Promise<void>>();
|
|
2069
1151
|
const designFailures = new Map<string, SafeDesignFailure>();
|
|
2070
1152
|
let pendingPrompt: EligiblePrompt | undefined;
|
|
1153
|
+
let pendingInit = false;
|
|
1154
|
+
let exitingStage = false;
|
|
2071
1155
|
let activePrompt: EligiblePrompt | undefined;
|
|
2072
1156
|
let designToolSnapshot: string[] | undefined;
|
|
2073
1157
|
const activation = new (class implements Activation {
|
|
@@ -2109,16 +1193,19 @@ export function registerWorkflowControl(
|
|
|
2109
1193
|
return opened;
|
|
2110
1194
|
};
|
|
2111
1195
|
const closeEngines = async () => {
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
await Promise.
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
1196
|
+
// Design close() is synchronous and does not own an operation queue.
|
|
1197
|
+
// Keep its journal open through finalization's commit/lease-cleanup path
|
|
1198
|
+
// and the dispatch result handling before closing any engine storage.
|
|
1199
|
+
await Promise.all([...designOperations]);
|
|
1200
|
+
const closed = await Promise.allSettled(
|
|
1201
|
+
[...engines].map(async ([key, opened]) => {
|
|
1202
|
+
const engine = await opened;
|
|
1203
|
+
await engine.close();
|
|
1204
|
+
if (engines.get(key) === opened) engines.delete(key);
|
|
1205
|
+
}),
|
|
2121
1206
|
);
|
|
1207
|
+
const failed = closed.find((result) => result.status === "rejected");
|
|
1208
|
+
if (failed?.status === "rejected") throw failed.reason;
|
|
2122
1209
|
};
|
|
2123
1210
|
const restoreDesignTools = () => {
|
|
2124
1211
|
if (!designToolSnapshot) return;
|
|
@@ -2156,6 +1243,16 @@ export function registerWorkflowControl(
|
|
|
2156
1243
|
deactivate();
|
|
2157
1244
|
parentPayloadBridge.clear();
|
|
2158
1245
|
};
|
|
1246
|
+
const exitStage = async () => {
|
|
1247
|
+
if (exitingStage) throw new Error("stage-control-mismatch");
|
|
1248
|
+
exitingStage = true;
|
|
1249
|
+
try {
|
|
1250
|
+
await closeEngines();
|
|
1251
|
+
await deactivateStage();
|
|
1252
|
+
} finally {
|
|
1253
|
+
exitingStage = false;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
2159
1256
|
|
|
2160
1257
|
// Pi validates tool arguments before execute(), so expose only the schema
|
|
2161
1258
|
// legal for the active stage instead of one ambiguous command/packet union.
|
|
@@ -2181,6 +1278,61 @@ export function registerWorkflowControl(
|
|
|
2181
1278
|
} as const;
|
|
2182
1279
|
let registeredParameterKind: "command" | "packet" | undefined;
|
|
2183
1280
|
|
|
1281
|
+
// Stage exit is session control, not a durable Implement command. Keep the
|
|
1282
|
+
// six engine commands unchanged and admit only this exact extra envelope.
|
|
1283
|
+
const FINISH_PARAMETERS = {
|
|
1284
|
+
type: "object",
|
|
1285
|
+
properties: { action: { type: "string", enum: ["finish"] } },
|
|
1286
|
+
required: ["action"],
|
|
1287
|
+
additionalProperties: false,
|
|
1288
|
+
} as const;
|
|
1289
|
+
const AMEND_PARAMETERS = {
|
|
1290
|
+
type: "object",
|
|
1291
|
+
properties: {
|
|
1292
|
+
action: { type: "string", enum: ["amend"] },
|
|
1293
|
+
change: { type: "string" },
|
|
1294
|
+
batchId: { type: "string" },
|
|
1295
|
+
request: DESIGN_CONTROL_REQUEST_SCHEMA,
|
|
1296
|
+
},
|
|
1297
|
+
required: ["action", "change", "batchId", "request"],
|
|
1298
|
+
additionalProperties: false,
|
|
1299
|
+
} as const;
|
|
1300
|
+
const IMPLEMENT_PARAMETERS = {
|
|
1301
|
+
...CONTROL_COMMAND_PARAMETERS,
|
|
1302
|
+
properties: {
|
|
1303
|
+
...CONTROL_COMMAND_PARAMETERS.properties,
|
|
1304
|
+
...AMEND_PARAMETERS.properties,
|
|
1305
|
+
action: { type: "string", enum: ["finish", "amend"] },
|
|
1306
|
+
},
|
|
1307
|
+
required: [],
|
|
1308
|
+
anyOf: [
|
|
1309
|
+
...CONTROL_COMMAND_PARAMETERS.anyOf,
|
|
1310
|
+
FINISH_PARAMETERS,
|
|
1311
|
+
AMEND_PARAMETERS,
|
|
1312
|
+
],
|
|
1313
|
+
} as const;
|
|
1314
|
+
const prepareImplementArguments = (args: unknown): unknown => {
|
|
1315
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return args;
|
|
1316
|
+
const record = { ...(args as Record<string, unknown>) };
|
|
1317
|
+
// Strict providers can pad fields belonging to the other union branch.
|
|
1318
|
+
if (record.action !== "amend") {
|
|
1319
|
+
for (const key of ["batchId", "request"]) {
|
|
1320
|
+
if (record[key] === null || record[key] === undefined)
|
|
1321
|
+
delete record[key];
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (record.action === "finish" || record.action === "amend") {
|
|
1325
|
+
for (const key of Object.keys(CONTROL_COMMAND_PARAMETERS.properties)) {
|
|
1326
|
+
if (record[key] === null || record[key] === undefined)
|
|
1327
|
+
delete record[key];
|
|
1328
|
+
}
|
|
1329
|
+
return record;
|
|
1330
|
+
}
|
|
1331
|
+
if (record.action === null || record.action === undefined)
|
|
1332
|
+
delete record.action;
|
|
1333
|
+
return canonicalizeControlCommandToolInput(record);
|
|
1334
|
+
};
|
|
1335
|
+
|
|
2184
1336
|
const registerDispatchTool = (kind: "command" | "packet") => {
|
|
2185
1337
|
if (registeredParameterKind === kind) return;
|
|
2186
1338
|
registeredParameterKind = kind;
|
|
@@ -2189,17 +1341,16 @@ export function registerWorkflowControl(
|
|
|
2189
1341
|
label: "Abel Control",
|
|
2190
1342
|
description:
|
|
2191
1343
|
kind === "command"
|
|
2192
|
-
?
|
|
1344
|
+
? '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.'
|
|
2193
1345
|
: "Private stage-bound Abel packet control. Accepts bounded Design and Diagnose packet operations.",
|
|
2194
1346
|
executionMode: "parallel",
|
|
2195
1347
|
...(kind === "command"
|
|
2196
1348
|
? {
|
|
2197
1349
|
prepareArguments: (args: unknown) =>
|
|
2198
|
-
|
|
1350
|
+
prepareImplementArguments(args) as never,
|
|
2199
1351
|
}
|
|
2200
1352
|
: {}),
|
|
2201
|
-
parameters:
|
|
2202
|
-
kind === "command" ? CONTROL_COMMAND_PARAMETERS : PACKET_PARAMETERS,
|
|
1353
|
+
parameters: kind === "command" ? IMPLEMENT_PARAMETERS : PACKET_PARAMETERS,
|
|
2203
1354
|
async execute(
|
|
2204
1355
|
toolCallId: string,
|
|
2205
1356
|
params: unknown,
|
|
@@ -2207,10 +1358,120 @@ export function registerWorkflowControl(
|
|
|
2207
1358
|
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
2208
1359
|
ctx: ExtensionContext,
|
|
2209
1360
|
) {
|
|
1361
|
+
if (
|
|
1362
|
+
exitingStage ||
|
|
1363
|
+
!activation.isActive() ||
|
|
1364
|
+
activePrompt === undefined
|
|
1365
|
+
) {
|
|
1366
|
+
throw new Error("stage-control-mismatch");
|
|
1367
|
+
}
|
|
1368
|
+
if (kind === "command") params = prepareImplementArguments(params);
|
|
2210
1369
|
const record =
|
|
2211
1370
|
params && typeof params === "object" && !Array.isArray(params)
|
|
2212
1371
|
? (params as Record<string, unknown>)
|
|
2213
1372
|
: undefined;
|
|
1373
|
+
if (record?.action === "amend") {
|
|
1374
|
+
if (
|
|
1375
|
+
activePrompt !== "abel-implement" ||
|
|
1376
|
+
Object.keys(record).some(
|
|
1377
|
+
(key) =>
|
|
1378
|
+
!["action", "change", "batchId", "request"].includes(key),
|
|
1379
|
+
) ||
|
|
1380
|
+
typeof record.change !== "string" ||
|
|
1381
|
+
typeof record.batchId !== "string"
|
|
1382
|
+
) {
|
|
1383
|
+
throw new Error("stage-control-mismatch");
|
|
1384
|
+
}
|
|
1385
|
+
const parsed = validateDesignControlRequest(record.request);
|
|
1386
|
+
if (!parsed.ok) throw new Error(parsed.code);
|
|
1387
|
+
const request = parsed.value;
|
|
1388
|
+
if (
|
|
1389
|
+
request.operation === "bind-change" ||
|
|
1390
|
+
(request.operation === "start" &&
|
|
1391
|
+
(!("change" in request) || request.change !== record.change))
|
|
1392
|
+
) {
|
|
1393
|
+
throw new Error("amendment-change-mismatch");
|
|
1394
|
+
}
|
|
1395
|
+
let settle!: () => void;
|
|
1396
|
+
const pending = new Promise<void>((resolve) => {
|
|
1397
|
+
settle = resolve;
|
|
1398
|
+
});
|
|
1399
|
+
designOperations.add(pending);
|
|
1400
|
+
try {
|
|
1401
|
+
const engine = await engineFor(ctx);
|
|
1402
|
+
if (exitingStage || !activation.isActive() || !engine.executeDesign)
|
|
1403
|
+
throw new Error("stage-control-mismatch");
|
|
1404
|
+
const status = await engine.execute(
|
|
1405
|
+
{
|
|
1406
|
+
command: "status",
|
|
1407
|
+
stage: "abel-implement",
|
|
1408
|
+
change: record.change,
|
|
1409
|
+
},
|
|
1410
|
+
ctx,
|
|
1411
|
+
signal,
|
|
1412
|
+
);
|
|
1413
|
+
const batch = status.decisionBatch as { id?: string } | undefined;
|
|
1414
|
+
if (
|
|
1415
|
+
!["approval-needed", "paused"].includes(String(status.state)) ||
|
|
1416
|
+
batch?.id !== record.batchId
|
|
1417
|
+
)
|
|
1418
|
+
throw new Error("amendment-batch-stale");
|
|
1419
|
+
if (request.operation !== "start") {
|
|
1420
|
+
const designStatus = await engine.executeDesign({
|
|
1421
|
+
operation: "status",
|
|
1422
|
+
runId: request.runId,
|
|
1423
|
+
});
|
|
1424
|
+
if (designStatus.change !== record.change)
|
|
1425
|
+
throw new Error("amendment-change-mismatch");
|
|
1426
|
+
}
|
|
1427
|
+
if (!engine.executeAmendment)
|
|
1428
|
+
throw new Error("amendment-control-unavailable");
|
|
1429
|
+
const revision = await engine.executeAmendment(
|
|
1430
|
+
record.change,
|
|
1431
|
+
record.batchId,
|
|
1432
|
+
request,
|
|
1433
|
+
);
|
|
1434
|
+
const payload =
|
|
1435
|
+
request.operation === "finalize-delivery" &&
|
|
1436
|
+
revision.state === "completed"
|
|
1437
|
+
? {
|
|
1438
|
+
...revision,
|
|
1439
|
+
scope: "amendment",
|
|
1440
|
+
state: "ready",
|
|
1441
|
+
completed: false,
|
|
1442
|
+
runId: status.runId,
|
|
1443
|
+
amendmentRunId: revision.runId,
|
|
1444
|
+
stage: "abel-implement",
|
|
1445
|
+
}
|
|
1446
|
+
: { ...revision, scope: "amendment" };
|
|
1447
|
+
// A compiled amendment stays in Implement; its private Design
|
|
1448
|
+
// records do not activate the Design stage or broaden parent tools.
|
|
1449
|
+
return {
|
|
1450
|
+
content: [
|
|
1451
|
+
{ type: "text" as const, text: JSON.stringify(payload) },
|
|
1452
|
+
],
|
|
1453
|
+
details: payload,
|
|
1454
|
+
};
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
if (
|
|
1457
|
+
error instanceof DesignPlanValidationError ||
|
|
1458
|
+
error instanceof DesignFinalizationError ||
|
|
1459
|
+
error instanceof DeliveryValidationError
|
|
1460
|
+
) {
|
|
1461
|
+
const failure = safeDesignFailure(
|
|
1462
|
+
error,
|
|
1463
|
+
request.operation,
|
|
1464
|
+
"execution",
|
|
1465
|
+
);
|
|
1466
|
+
designFailures.set(toolCallId, failure);
|
|
1467
|
+
throw new Error(failure.code);
|
|
1468
|
+
}
|
|
1469
|
+
throw error;
|
|
1470
|
+
} finally {
|
|
1471
|
+
settle();
|
|
1472
|
+
designOperations.delete(pending);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
2214
1475
|
if (record && typeof record.action === "string") {
|
|
2215
1476
|
if (
|
|
2216
1477
|
Object.keys(record).some(
|
|
@@ -2222,6 +1483,18 @@ export function registerWorkflowControl(
|
|
|
2222
1483
|
if (record.action === "finish" && Object.hasOwn(record, "request")) {
|
|
2223
1484
|
throw new Error("control-envelope-ambiguous");
|
|
2224
1485
|
}
|
|
1486
|
+
if (record.action === "finish") {
|
|
1487
|
+
// close() aborts and settles active operations before ordinary tools
|
|
1488
|
+
// return; durable work is retained, never discarded or completed.
|
|
1489
|
+
await exitStage();
|
|
1490
|
+
const payload = { ok: true, state: "inactive" };
|
|
1491
|
+
return {
|
|
1492
|
+
content: [
|
|
1493
|
+
{ type: "text" as const, text: JSON.stringify(payload) },
|
|
1494
|
+
],
|
|
1495
|
+
details: payload,
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
2225
1498
|
if (
|
|
2226
1499
|
activePrompt !== "abel-design" &&
|
|
2227
1500
|
activePrompt !== "abel-diagnose"
|
|
@@ -2234,9 +1507,18 @@ export function registerWorkflowControl(
|
|
|
2234
1507
|
}
|
|
2235
1508
|
const designRequest = validateDesignControlRequest(record.request);
|
|
2236
1509
|
if (!designRequest.ok) throw new Error(designRequest.code);
|
|
1510
|
+
let settleDesign!: () => void;
|
|
1511
|
+
const settled = new Promise<void>((resolve) => {
|
|
1512
|
+
settleDesign = resolve;
|
|
1513
|
+
});
|
|
1514
|
+
// Register before the first await, including asynchronous engine
|
|
1515
|
+
// opening. Teardown blocks admission before taking its snapshot.
|
|
1516
|
+
designOperations.add(settled);
|
|
2237
1517
|
let boundary: DesignFailureBoundary = "initialization";
|
|
2238
1518
|
try {
|
|
2239
1519
|
const engine = await engineFor(ctx);
|
|
1520
|
+
if (exitingStage || !activation.isActive())
|
|
1521
|
+
throw new Error("stage-control-mismatch");
|
|
2240
1522
|
if (!engine.executeDesign)
|
|
2241
1523
|
throw new Error("design-control-unavailable");
|
|
2242
1524
|
boundary = "execution";
|
|
@@ -2249,7 +1531,9 @@ export function registerWorkflowControl(
|
|
|
2249
1531
|
boundary = "execution";
|
|
2250
1532
|
if (
|
|
2251
1533
|
designRequest.value.operation === "finalize-delivery" &&
|
|
2252
|
-
payload.state === "completed"
|
|
1534
|
+
payload.state === "completed" &&
|
|
1535
|
+
!exitingStage &&
|
|
1536
|
+
activePrompt === "abel-design"
|
|
2253
1537
|
) {
|
|
2254
1538
|
await deactivateStage();
|
|
2255
1539
|
}
|
|
@@ -2267,6 +1551,9 @@ export function registerWorkflowControl(
|
|
|
2267
1551
|
const sanitized = new Error(failure.code);
|
|
2268
1552
|
sanitized.name = "DesignControlError";
|
|
2269
1553
|
throw sanitized;
|
|
1554
|
+
} finally {
|
|
1555
|
+
designOperations.delete(settled);
|
|
1556
|
+
settleDesign();
|
|
2270
1557
|
}
|
|
2271
1558
|
}
|
|
2272
1559
|
let designEngine: WorkflowControlEngine | undefined;
|
|
@@ -2280,6 +1567,8 @@ export function registerWorkflowControl(
|
|
|
2280
1567
|
if (packet.value.stage === "abel-design") {
|
|
2281
1568
|
try {
|
|
2282
1569
|
designEngine = await engineFor(ctx);
|
|
1570
|
+
if (exitingStage || !activation.isActive())
|
|
1571
|
+
throw new Error("stage-control-mismatch");
|
|
2283
1572
|
} catch (error) {
|
|
2284
1573
|
if (error instanceof RunStoreFormatError)
|
|
2285
1574
|
throw runStoreResetError(error);
|
|
@@ -2336,9 +1625,6 @@ export function registerWorkflowControl(
|
|
|
2336
1625
|
});
|
|
2337
1626
|
payload = { ...packetPayload, recordedEvidence };
|
|
2338
1627
|
}
|
|
2339
|
-
if (record.action === "finish" && packetPayload.ok) {
|
|
2340
|
-
await deactivateStage();
|
|
2341
|
-
}
|
|
2342
1628
|
const display = tuiRun
|
|
2343
1629
|
? activity.finalize(toolCallId, payload)
|
|
2344
1630
|
: undefined;
|
|
@@ -2366,15 +1652,16 @@ export function registerWorkflowControl(
|
|
|
2366
1652
|
throw error;
|
|
2367
1653
|
}
|
|
2368
1654
|
if (
|
|
2369
|
-
activePrompt !==
|
|
2370
|
-
|
|
2371
|
-
validation.value.stage !== activePrompt)
|
|
1655
|
+
activePrompt !== "abel-implement" ||
|
|
1656
|
+
validation.value.stage !== activePrompt
|
|
2372
1657
|
) {
|
|
2373
1658
|
throw new Error("stage-control-mismatch");
|
|
2374
1659
|
}
|
|
2375
1660
|
let engine: WorkflowControlEngine;
|
|
2376
1661
|
try {
|
|
2377
1662
|
engine = await engineFor(ctx);
|
|
1663
|
+
if (exitingStage || !activation.isActive())
|
|
1664
|
+
throw new Error("stage-control-mismatch");
|
|
2378
1665
|
} catch (error) {
|
|
2379
1666
|
if (!(error instanceof RunStoreFormatError)) throw error;
|
|
2380
1667
|
const payload = runStoreUnavailableStatus(error, validation.value);
|
|
@@ -2471,7 +1758,10 @@ export function registerWorkflowControl(
|
|
|
2471
1758
|
if (
|
|
2472
1759
|
event.toolName !== DISPATCH_TOOL ||
|
|
2473
1760
|
!event.isError ||
|
|
2474
|
-
event.input.action !== "design"
|
|
1761
|
+
(event.input.action !== "design" &&
|
|
1762
|
+
!(
|
|
1763
|
+
event.input.action === "amend" && designFailures.has(event.toolCallId)
|
|
1764
|
+
))
|
|
2475
1765
|
) {
|
|
2476
1766
|
return;
|
|
2477
1767
|
}
|
|
@@ -2501,12 +1791,35 @@ export function registerWorkflowControl(
|
|
|
2501
1791
|
};
|
|
2502
1792
|
});
|
|
2503
1793
|
pi.on("input", (event) => {
|
|
2504
|
-
pendingPrompt =
|
|
1794
|
+
pendingPrompt = undefined;
|
|
1795
|
+
pendingInit = false;
|
|
1796
|
+
const prompt = invokedPrompt(event.text);
|
|
1797
|
+
const init = /^\/abel-init(?:\s|$)/u.test(event.text);
|
|
1798
|
+
if (event.source !== "interactive" && event.source !== "rpc") {
|
|
1799
|
+
// Stop before Pi expands the template: hiding dispatch alone would still
|
|
1800
|
+
// expose the complete workflow instructions to the parent model.
|
|
1801
|
+
return { action: prompt || init ? "handled" : "continue" };
|
|
1802
|
+
}
|
|
1803
|
+
pendingPrompt = prompt;
|
|
1804
|
+
pendingInit = init && hasPackageProvenance(pi, "abel-init");
|
|
1805
|
+
if (
|
|
1806
|
+
activePrompt &&
|
|
1807
|
+
(pendingInit ||
|
|
1808
|
+
(prompt && prompt !== activePrompt && hasPackageProvenance(pi, prompt)))
|
|
1809
|
+
) {
|
|
1810
|
+
return (async () => {
|
|
1811
|
+
await exitStage();
|
|
1812
|
+
return { action: "continue" as const };
|
|
1813
|
+
})();
|
|
1814
|
+
}
|
|
2505
1815
|
return { action: "continue" };
|
|
2506
1816
|
});
|
|
2507
1817
|
pi.on("before_agent_start", (event, ctx) => {
|
|
2508
1818
|
const prompt = pendingPrompt;
|
|
1819
|
+
const init =
|
|
1820
|
+
pendingInit && hasExpandedPromptMarker(event.prompt, "abel-init");
|
|
2509
1821
|
pendingPrompt = undefined;
|
|
1822
|
+
pendingInit = false;
|
|
2510
1823
|
const verified =
|
|
2511
1824
|
prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
|
|
2512
1825
|
if (verified) {
|
|
@@ -2527,6 +1840,12 @@ export function registerWorkflowControl(
|
|
|
2527
1840
|
}
|
|
2528
1841
|
if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
|
|
2529
1842
|
if (verified && prompt === "abel-design") enforceDesignTools();
|
|
1843
|
+
const boundary = activePrompt
|
|
1844
|
+
? `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.`
|
|
1845
|
+
: init
|
|
1846
|
+
? "Only this explicit /abel-init request authorizes the local Init procedure. Do not activate dispatch or continue into another Abel stage."
|
|
1847
|
+
: "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.";
|
|
1848
|
+
return { systemPrompt: `${event.systemPrompt ?? ""}\n\n${boundary}` };
|
|
2530
1849
|
});
|
|
2531
1850
|
pi.on("model_select", (event, ctx) => {
|
|
2532
1851
|
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
@@ -2552,6 +1871,7 @@ export function registerWorkflowControl(
|
|
|
2552
1871
|
});
|
|
2553
1872
|
pi.on("session_start", async (_event, ctx) => {
|
|
2554
1873
|
pendingPrompt = undefined;
|
|
1874
|
+
pendingInit = false;
|
|
2555
1875
|
activePrompt = undefined;
|
|
2556
1876
|
designFailures.clear();
|
|
2557
1877
|
activity.detach();
|