@abelxiaoxing/cadence 1.0.14 → 1.1.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 +39 -13
- package/agents/contract-reviewer.md +2 -2
- package/agents/design-explorer.md +1 -1
- package/agents/diagnosis-worker.md +6 -3
- package/agents/implementation-worker.md +14 -9
- package/config/.env.example +5 -17
- package/config/routes.example.json +42 -0
- package/package.json +3 -3
- package/prompts/abel-design.md +206 -228
- package/prompts/abel-diagnose.md +39 -18
- package/prompts/abel-implement.md +122 -178
- package/prompts/abel-init.md +36 -33
- package/skills/abel-workflow/SKILL.md +190 -168
- package/src/apply-transaction.ts +1427 -0
- package/src/artifact-store.ts +204 -0
- package/src/child-session.ts +136 -17
- package/src/contracts.ts +121 -696
- package/src/control-contracts.ts +246 -0
- package/src/delivery-compiler.ts +1118 -0
- package/src/design-control.ts +846 -0
- package/src/design-journal.ts +1569 -0
- package/src/file-snapshot.ts +5 -0
- package/src/implement-graph.ts +11 -4
- package/src/index.ts +1833 -118
- package/src/isolation-backend.ts +269 -0
- package/src/packet-runtime.ts +533 -0
- package/src/parent-provider.ts +23 -4
- package/src/route-policy.ts +483 -0
- package/src/run-state.ts +430 -0
- package/src/run-store.ts +990 -0
- package/src/safe-path.ts +11 -0
- package/src/scoped-tools.ts +36 -0
- package/src/state-root.ts +215 -0
- package/src/subagent-activity.ts +526 -27
- package/src/submit-tool.ts +274 -0
- package/src/task-ledger.ts +1380 -0
- package/src/verification-capability.ts +560 -45
- package/src/worker-broker.ts +703 -0
- package/src/workflow-engine.ts +7706 -0
- package/src/workspace-store.ts +544 -0
- package/src/candidate-preflight.ts +0 -1618
- package/src/drain.ts +0 -20
- package/src/patch.ts +0 -505
- package/src/result-store.ts +0 -312
- package/src/runtime.ts +0 -2436
- package/src/scheduler.ts +0 -384
- package/src/subagent-endpoint.ts +0 -158
- package/src/worker.ts +0 -204
package/src/index.ts
CHANGED
|
@@ -3,8 +3,21 @@
|
|
|
3
3
|
// the active set at session start. Eligible-stage activation is wired by the
|
|
4
4
|
// workflow routing (abel-design/implement/diagnose provenance) in the prompts
|
|
5
5
|
// integration; abel-init and ordinary prompts never activate dispatch.
|
|
6
|
-
import {
|
|
6
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import {
|
|
10
|
+
lstatSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
mkdtempSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import path, { dirname, join } from "node:path";
|
|
7
19
|
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { promisify } from "node:util";
|
|
8
21
|
import type {
|
|
9
22
|
AgentToolResult,
|
|
10
23
|
AgentToolUpdateCallback,
|
|
@@ -14,17 +27,66 @@ import type {
|
|
|
14
27
|
ToolRenderResultOptions,
|
|
15
28
|
} from "@earendil-works/pi-coding-agent";
|
|
16
29
|
import { type Activation, activateTool, deactivateTool } from "./activation.ts";
|
|
17
|
-
import {
|
|
30
|
+
import { loadAgentDefinitions } from "./agent-registry.ts";
|
|
31
|
+
import { runChildSession } from "./child-session.ts";
|
|
32
|
+
import {
|
|
33
|
+
type AtomicVerificationContract,
|
|
34
|
+
type DesignEvidenceResult,
|
|
35
|
+
isValidRelativePath,
|
|
36
|
+
LIMITS,
|
|
37
|
+
type StructuredVerificationContract,
|
|
38
|
+
verificationSteps,
|
|
39
|
+
} from "./contracts.ts";
|
|
40
|
+
import { validateControlCommand } from "./control-contracts.ts";
|
|
41
|
+
import {
|
|
42
|
+
assessDeliveryTraceability,
|
|
43
|
+
compileImplementPlan,
|
|
44
|
+
DeliveryValidationError,
|
|
45
|
+
type GateApprovalProof,
|
|
46
|
+
IMPLEMENT_PLAN_SCHEMA_VERSION,
|
|
47
|
+
parseGateAReceipt,
|
|
48
|
+
parseImplementPlan,
|
|
49
|
+
parseReadyReceipt,
|
|
50
|
+
} from "./delivery-compiler.ts";
|
|
51
|
+
import {
|
|
52
|
+
DesignController,
|
|
53
|
+
validateDesignControlRequest,
|
|
54
|
+
} from "./design-control.ts";
|
|
55
|
+
import { canonicalJson, hashCanonicalValue } from "./implement-graph.ts";
|
|
56
|
+
import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
|
|
57
|
+
import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
|
|
18
58
|
import { ParentPayloadBridge } from "./parent-payload-bridge.ts";
|
|
19
|
-
import {
|
|
59
|
+
import { runtimeForWorkerRoute } from "./parent-provider.ts";
|
|
60
|
+
import {
|
|
61
|
+
inspectRoutePolicy,
|
|
62
|
+
loadRoutePolicy,
|
|
63
|
+
type RoutePolicyResolution,
|
|
64
|
+
unavailableRoutePolicy,
|
|
65
|
+
type WorkerRoutePolicy,
|
|
66
|
+
} from "./route-policy.ts";
|
|
67
|
+
import { observeSafePath } from "./safe-path.ts";
|
|
68
|
+
import { resolveStateRoot } from "./state-root.ts";
|
|
20
69
|
import {
|
|
21
70
|
ACTIVITY_DETAILS_KEY,
|
|
22
71
|
ActivityController,
|
|
23
72
|
renderActivityCall,
|
|
24
73
|
renderActivityResult,
|
|
74
|
+
type WorkflowActivityUpdate,
|
|
25
75
|
} from "./subagent-activity.ts";
|
|
76
|
+
import { classifyCandidateContextRequest } from "./submit-tool.ts";
|
|
77
|
+
import {
|
|
78
|
+
bindCurrentVerificationCapability,
|
|
79
|
+
isVerificationCapabilityCurrent,
|
|
80
|
+
type VerificationRunnerBinding,
|
|
81
|
+
} from "./verification-capability.ts";
|
|
82
|
+
import {
|
|
83
|
+
openDurableWorkflowEngine,
|
|
84
|
+
type WorkflowAvailableDelivery,
|
|
85
|
+
type WorkflowDeliverySource,
|
|
86
|
+
} from "./workflow-engine.ts";
|
|
26
87
|
|
|
27
88
|
export const DISPATCH_TOOL = "abel_dispatch";
|
|
89
|
+
const DESIGN_PARENT_READ_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
28
90
|
|
|
29
91
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
30
92
|
const ELIGIBLE_PROMPTS = [
|
|
@@ -55,6 +117,10 @@ const DESIGN_REQUEST_SCHEMA = {
|
|
|
55
117
|
enum: ["design-explorer"],
|
|
56
118
|
description: "Exact package-owned read-only Agent role.",
|
|
57
119
|
},
|
|
120
|
+
runId: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "Durable Design run identity returned by Design start.",
|
|
123
|
+
},
|
|
58
124
|
id: {
|
|
59
125
|
type: "string",
|
|
60
126
|
description:
|
|
@@ -128,6 +194,7 @@ const DESIGN_REQUEST_SCHEMA = {
|
|
|
128
194
|
required: [
|
|
129
195
|
"stage",
|
|
130
196
|
"role",
|
|
197
|
+
"runId",
|
|
131
198
|
"id",
|
|
132
199
|
"phase",
|
|
133
200
|
"objective",
|
|
@@ -139,11 +206,89 @@ const DESIGN_REQUEST_SCHEMA = {
|
|
|
139
206
|
additionalProperties: false,
|
|
140
207
|
} as const;
|
|
141
208
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
209
|
+
const DESIGN_CONTROL_REQUEST_SCHEMA = {
|
|
210
|
+
description:
|
|
211
|
+
"One closed durable Design control operation. The operation body is strict and idempotent by operationId.",
|
|
212
|
+
oneOf: [
|
|
213
|
+
{
|
|
214
|
+
type: "object",
|
|
215
|
+
properties: {
|
|
216
|
+
operation: { type: "string", enum: ["record-decision"] },
|
|
217
|
+
runId: { type: "string" },
|
|
218
|
+
operationId: { type: "string" },
|
|
219
|
+
decisionId: { type: "string" },
|
|
220
|
+
category: { type: "string", enum: ["behavior", "technical"] },
|
|
221
|
+
contractHash: { type: "string" },
|
|
222
|
+
refs: { type: "array", items: { type: "string" } },
|
|
223
|
+
},
|
|
224
|
+
required: [
|
|
225
|
+
"operation",
|
|
226
|
+
"runId",
|
|
227
|
+
"operationId",
|
|
228
|
+
"decisionId",
|
|
229
|
+
"category",
|
|
230
|
+
"contractHash",
|
|
231
|
+
"refs",
|
|
232
|
+
],
|
|
233
|
+
additionalProperties: false,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
type: "object",
|
|
237
|
+
properties: {
|
|
238
|
+
operation: { type: "string", enum: ["approve-gate"] },
|
|
239
|
+
runId: { type: "string" },
|
|
240
|
+
operationId: { type: "string" },
|
|
241
|
+
gate: { type: "string", enum: ["gate-a", "gate-b"] },
|
|
242
|
+
contractHash: { type: "string" },
|
|
243
|
+
},
|
|
244
|
+
required: ["operation", "runId", "operationId", "gate", "contractHash"],
|
|
245
|
+
additionalProperties: false,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
operation: { type: "string", enum: ["write-artifact"] },
|
|
251
|
+
runId: { type: "string" },
|
|
252
|
+
operationId: { type: "string" },
|
|
253
|
+
path: {
|
|
254
|
+
type: "string",
|
|
255
|
+
description: "Allowed path relative to this Design change root.",
|
|
256
|
+
},
|
|
257
|
+
content: {
|
|
258
|
+
type: "string",
|
|
259
|
+
maxLength: 16 * 1024 * 1024,
|
|
260
|
+
description: "Exact bounded UTF-8 artifact content.",
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
required: ["operation", "runId", "operationId", "path", "content"],
|
|
264
|
+
additionalProperties: false,
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
type: "object",
|
|
268
|
+
properties: {
|
|
269
|
+
operation: { type: "string", enum: ["delete-artifact"] },
|
|
270
|
+
runId: { type: "string" },
|
|
271
|
+
operationId: { type: "string" },
|
|
272
|
+
path: {
|
|
273
|
+
type: "string",
|
|
274
|
+
description: "Allowed path relative to this Design change root.",
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
required: ["operation", "runId", "operationId", "path"],
|
|
278
|
+
additionalProperties: false,
|
|
279
|
+
},
|
|
280
|
+
...(["compile-plan", "finalize-delivery"] as const).map((operation) => ({
|
|
281
|
+
type: "object" as const,
|
|
282
|
+
properties: {
|
|
283
|
+
operation: { type: "string" as const, enum: [operation] },
|
|
284
|
+
runId: { type: "string" as const },
|
|
285
|
+
operationId: { type: "string" as const },
|
|
286
|
+
},
|
|
287
|
+
required: ["operation", "runId", "operationId"],
|
|
288
|
+
additionalProperties: false,
|
|
289
|
+
})),
|
|
290
|
+
],
|
|
291
|
+
} as const;
|
|
147
292
|
|
|
148
293
|
function invokedPrompt(text: string): EligiblePrompt | undefined {
|
|
149
294
|
const name = text.match(/^\/([^\s]+)(?:\s|$)/)?.[1];
|
|
@@ -218,91 +363,1672 @@ function splitUsage(result: unknown): { payload: unknown; usage?: unknown } {
|
|
|
218
363
|
return { payload, usage };
|
|
219
364
|
}
|
|
220
365
|
|
|
221
|
-
export
|
|
366
|
+
export interface WorkflowControlEngine {
|
|
367
|
+
execute(
|
|
368
|
+
command: unknown,
|
|
369
|
+
context?: ExtensionContext,
|
|
370
|
+
signal?: AbortSignal,
|
|
371
|
+
onActivity?: (event: WorkflowActivityUpdate) => void,
|
|
372
|
+
): Promise<Record<string, unknown>>;
|
|
373
|
+
executeDesign?(request: unknown): Promise<Record<string, unknown>>;
|
|
374
|
+
assertDesignRun?(runId: string): void;
|
|
375
|
+
recordDesignEvidence?(input: {
|
|
376
|
+
runId: string;
|
|
377
|
+
evidence: DesignEvidenceResult;
|
|
378
|
+
}): unknown;
|
|
379
|
+
close(): void | Promise<void>;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export type WorkflowControlEngineFactory = (
|
|
383
|
+
ctx: ExtensionContext,
|
|
384
|
+
parentPayloadBridge: ParentPayloadBridge,
|
|
385
|
+
) => WorkflowControlEngine | Promise<WorkflowControlEngine>;
|
|
386
|
+
|
|
387
|
+
const PACKAGE_DELIVERY_MAX_BYTES = 16 * 1024 * 1024;
|
|
388
|
+
const execFileAsync = promisify(execFile);
|
|
389
|
+
|
|
390
|
+
export interface OpenSpecDeliveryInspection {
|
|
391
|
+
change: string;
|
|
392
|
+
schema: string;
|
|
393
|
+
planningComplete: boolean;
|
|
394
|
+
strictValid: boolean;
|
|
395
|
+
artifactPaths: string[];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export interface PackageDeliverySourceOptions {
|
|
399
|
+
inspectOpenSpec?: (
|
|
400
|
+
consumerRoot: string,
|
|
401
|
+
change: string,
|
|
402
|
+
) => Promise<OpenSpecDeliveryInspection>;
|
|
403
|
+
verifyGateProof?: (input: {
|
|
404
|
+
change: string;
|
|
405
|
+
gate: "gate-a" | "gate-b";
|
|
406
|
+
proof: GateApprovalProof;
|
|
407
|
+
}) => boolean;
|
|
408
|
+
verifyFinalizedDelivery?: (input: {
|
|
409
|
+
change: string;
|
|
410
|
+
deliveryRevision: number;
|
|
411
|
+
receiptHash: string;
|
|
412
|
+
gateA: GateApprovalProof;
|
|
413
|
+
gateB: GateApprovalProof;
|
|
414
|
+
planCanonicalHash: string;
|
|
415
|
+
}) => boolean;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export interface PackageWorkflowDeliverySource extends WorkflowDeliverySource {
|
|
419
|
+
discoverLatest(input: {
|
|
420
|
+
stage: "abel-design" | "abel-implement";
|
|
421
|
+
change: string;
|
|
422
|
+
}): Promise<WorkflowAvailableDelivery | undefined>;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function sha256(value: Uint8Array | string): string {
|
|
426
|
+
return createHash("sha256").update(value).digest("hex");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function proofVerified(verify: () => boolean): boolean {
|
|
430
|
+
try {
|
|
431
|
+
return verify() === true;
|
|
432
|
+
} catch {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function normalizedTrackingArtifact(
|
|
438
|
+
bytes: Uint8Array,
|
|
439
|
+
taskIds: readonly string[],
|
|
440
|
+
): Buffer | undefined {
|
|
441
|
+
let text: string;
|
|
442
|
+
try {
|
|
443
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
444
|
+
} catch {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
const identities = taskIds.map((taskId) => ({
|
|
448
|
+
taskId,
|
|
449
|
+
pattern: new RegExp(
|
|
450
|
+
`(?:^|[^A-Za-z0-9._:-])${taskId.replace(
|
|
451
|
+
/[.*+?^${}()|[\]\\]/gu,
|
|
452
|
+
"\\$&",
|
|
453
|
+
)}(?![A-Za-z0-9._:-])`,
|
|
454
|
+
"u",
|
|
455
|
+
),
|
|
456
|
+
matches: 0,
|
|
457
|
+
}));
|
|
458
|
+
const normalized = text
|
|
459
|
+
.split(/(?<=\n)/u)
|
|
460
|
+
.map((segment) => {
|
|
461
|
+
const line = segment.endsWith("\n") ? segment.slice(0, -1) : segment;
|
|
462
|
+
if (!/^\s*-\s+\[[ xX]\]/u.test(line)) return segment;
|
|
463
|
+
const matching = identities.filter(({ pattern }) => pattern.test(line));
|
|
464
|
+
if (matching.length !== 1) return segment;
|
|
465
|
+
matching[0].matches += 1;
|
|
466
|
+
return segment.replace(/^(\s*-\s+)\[[xX]\]/u, "$1[ ]");
|
|
467
|
+
})
|
|
468
|
+
.join("");
|
|
469
|
+
if (identities.some(({ matches }) => matches !== 1)) return undefined;
|
|
470
|
+
return Buffer.from(normalized, "utf8");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function readPackageDeliveryFile(
|
|
474
|
+
root: string,
|
|
475
|
+
relative: string,
|
|
476
|
+
maximumBytes = PACKAGE_DELIVERY_MAX_BYTES,
|
|
477
|
+
): Buffer {
|
|
478
|
+
const observation = observeSafePath(root, relative);
|
|
479
|
+
if (observation.kind !== "file") {
|
|
480
|
+
throw new Error("delivery-file-unavailable");
|
|
481
|
+
}
|
|
482
|
+
const target = path.join(root, ...relative.split("/"));
|
|
483
|
+
const stat = lstatSync(target);
|
|
484
|
+
if (stat.size < 1 || stat.size > maximumBytes) {
|
|
485
|
+
throw new Error("delivery-file-size-invalid");
|
|
486
|
+
}
|
|
487
|
+
return readFileSync(target);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function deliveryRecord(value: unknown): Record<string, unknown> | undefined {
|
|
491
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
492
|
+
? (value as Record<string, unknown>)
|
|
493
|
+
: undefined;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function parseCommandJson(
|
|
497
|
+
value: string,
|
|
498
|
+
code: string,
|
|
499
|
+
): Record<string, unknown> {
|
|
500
|
+
let parsed: unknown;
|
|
501
|
+
try {
|
|
502
|
+
parsed = JSON.parse(value);
|
|
503
|
+
} catch {
|
|
504
|
+
throw new Error(code);
|
|
505
|
+
}
|
|
506
|
+
const record = deliveryRecord(parsed);
|
|
507
|
+
if (!record) throw new Error(code);
|
|
508
|
+
return record;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export async function inspectOpenSpecDelivery(
|
|
512
|
+
consumerRoot: string,
|
|
513
|
+
change: string,
|
|
514
|
+
): Promise<OpenSpecDeliveryInspection> {
|
|
515
|
+
const [statusExecution, validationExecution] = await Promise.all([
|
|
516
|
+
execFileAsync("openspec", ["status", "--change", change, "--json"], {
|
|
517
|
+
cwd: consumerRoot,
|
|
518
|
+
encoding: "utf8",
|
|
519
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
520
|
+
}),
|
|
521
|
+
execFileAsync(
|
|
522
|
+
"openspec",
|
|
523
|
+
["validate", change, "--strict", "--json", "--no-interactive"],
|
|
524
|
+
{
|
|
525
|
+
cwd: consumerRoot,
|
|
526
|
+
encoding: "utf8",
|
|
527
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
528
|
+
},
|
|
529
|
+
),
|
|
530
|
+
]);
|
|
531
|
+
const status = parseCommandJson(
|
|
532
|
+
statusExecution.stdout,
|
|
533
|
+
"delivery-openspec-status-invalid",
|
|
534
|
+
);
|
|
535
|
+
const validation = parseCommandJson(
|
|
536
|
+
validationExecution.stdout,
|
|
537
|
+
"delivery-openspec-validation-invalid",
|
|
538
|
+
);
|
|
539
|
+
const changeRoot = path.resolve(consumerRoot, "openspec", "changes", change);
|
|
540
|
+
const artifactPathsRecord = deliveryRecord(status.artifactPaths);
|
|
541
|
+
const artifactPaths = artifactPathsRecord
|
|
542
|
+
? Object.values(artifactPathsRecord).flatMap((entry) => {
|
|
543
|
+
const record = deliveryRecord(entry);
|
|
544
|
+
if (!record || !Array.isArray(record.existingOutputPaths)) return [];
|
|
545
|
+
return record.existingOutputPaths.flatMap((candidate) => {
|
|
546
|
+
if (typeof candidate !== "string") return [];
|
|
547
|
+
const resolved = path.resolve(candidate);
|
|
548
|
+
const relative = path.relative(changeRoot, resolved);
|
|
549
|
+
return relative &&
|
|
550
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
551
|
+
relative !== ".." &&
|
|
552
|
+
!path.isAbsolute(relative)
|
|
553
|
+
? [relative.split(path.sep).join("/")]
|
|
554
|
+
: [];
|
|
555
|
+
});
|
|
556
|
+
})
|
|
557
|
+
: [];
|
|
558
|
+
const items = Array.isArray(validation.items) ? validation.items : [];
|
|
559
|
+
const strictValid =
|
|
560
|
+
items.length === 1 &&
|
|
561
|
+
deliveryRecord(items[0])?.id === change &&
|
|
562
|
+
deliveryRecord(items[0])?.valid === true;
|
|
563
|
+
if (
|
|
564
|
+
status.changeName !== change ||
|
|
565
|
+
typeof status.schemaName !== "string" ||
|
|
566
|
+
artifactPaths.length === 0
|
|
567
|
+
) {
|
|
568
|
+
throw new Error("delivery-openspec-status-invalid");
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
change,
|
|
572
|
+
schema: status.schemaName,
|
|
573
|
+
planningComplete:
|
|
574
|
+
status.isPlanningComplete === true && status.isComplete === true,
|
|
575
|
+
strictValid,
|
|
576
|
+
artifactPaths: [...new Set(artifactPaths)].sort(),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function packageDeliverySource(
|
|
581
|
+
consumerRoot: string,
|
|
582
|
+
options: PackageDeliverySourceOptions = {},
|
|
583
|
+
): PackageWorkflowDeliverySource {
|
|
584
|
+
const inspect = options.inspectOpenSpec ?? inspectOpenSpecDelivery;
|
|
585
|
+
const verifyGateProof = options.verifyGateProof;
|
|
586
|
+
const verifyFinalizedDelivery = options.verifyFinalizedDelivery;
|
|
587
|
+
return {
|
|
588
|
+
async discoverLatest(input) {
|
|
589
|
+
if (
|
|
590
|
+
input.stage !== "abel-implement" ||
|
|
591
|
+
!verifyGateProof ||
|
|
592
|
+
!verifyFinalizedDelivery
|
|
593
|
+
) {
|
|
594
|
+
return undefined;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const changeRoot = `openspec/changes/${input.change}`;
|
|
598
|
+
const receiptBytes = readPackageDeliveryFile(
|
|
599
|
+
consumerRoot,
|
|
600
|
+
`${changeRoot}/ready.yaml`,
|
|
601
|
+
4 * 1024 * 1024,
|
|
602
|
+
);
|
|
603
|
+
const receipt = parseReadyReceipt(receiptBytes);
|
|
604
|
+
if (receipt.change !== input.change) return undefined;
|
|
605
|
+
const gateABytes = readPackageDeliveryFile(
|
|
606
|
+
consumerRoot,
|
|
607
|
+
`${changeRoot}/${receipt.approvals.gateA.path}`,
|
|
608
|
+
4 * 1024 * 1024,
|
|
609
|
+
);
|
|
610
|
+
if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
const gateA = parseGateAReceipt(gateABytes);
|
|
614
|
+
if (
|
|
615
|
+
gateA.change !== receipt.change ||
|
|
616
|
+
gateA.schema !== receipt.schema ||
|
|
617
|
+
!proofVerified(() =>
|
|
618
|
+
verifyGateProof({
|
|
619
|
+
change: input.change,
|
|
620
|
+
gate: "gate-a",
|
|
621
|
+
proof: gateA.approval,
|
|
622
|
+
}),
|
|
623
|
+
) ||
|
|
624
|
+
!proofVerified(() =>
|
|
625
|
+
verifyGateProof({
|
|
626
|
+
change: input.change,
|
|
627
|
+
gate: "gate-b",
|
|
628
|
+
proof: receipt.approvals.gateB,
|
|
629
|
+
}),
|
|
630
|
+
)
|
|
631
|
+
) {
|
|
632
|
+
return undefined;
|
|
633
|
+
}
|
|
634
|
+
const receiptHash = sha256(receiptBytes);
|
|
635
|
+
if (
|
|
636
|
+
!proofVerified(() =>
|
|
637
|
+
verifyFinalizedDelivery({
|
|
638
|
+
change: input.change,
|
|
639
|
+
deliveryRevision: receipt.deliveryRevision,
|
|
640
|
+
receiptHash,
|
|
641
|
+
gateA: gateA.approval,
|
|
642
|
+
gateB: receipt.approvals.gateB,
|
|
643
|
+
planCanonicalHash: receipt.plan.canonicalHash,
|
|
644
|
+
}),
|
|
645
|
+
)
|
|
646
|
+
) {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
deliveryRevision: receipt.deliveryRevision,
|
|
651
|
+
receiptHash,
|
|
652
|
+
};
|
|
653
|
+
} catch {
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
async load(input) {
|
|
658
|
+
if (input.stage !== "abel-implement") {
|
|
659
|
+
throw new DeliveryValidationError(["delivery-stage-invalid"]);
|
|
660
|
+
}
|
|
661
|
+
const changeRoot = `openspec/changes/${input.change}`;
|
|
662
|
+
const receiptRelative = `${changeRoot}/ready.yaml`;
|
|
663
|
+
const planRelative = `${changeRoot}/implement-plan.json`;
|
|
664
|
+
let receiptBytes: Buffer;
|
|
665
|
+
try {
|
|
666
|
+
receiptBytes = readPackageDeliveryFile(
|
|
667
|
+
consumerRoot,
|
|
668
|
+
receiptRelative,
|
|
669
|
+
4 * 1024 * 1024,
|
|
670
|
+
);
|
|
671
|
+
} catch {
|
|
672
|
+
throw new DeliveryValidationError(["delivery-receipt-unavailable"]);
|
|
673
|
+
}
|
|
674
|
+
let receipt: ReturnType<typeof parseReadyReceipt>;
|
|
675
|
+
try {
|
|
676
|
+
receipt = parseReadyReceipt(receiptBytes);
|
|
677
|
+
} catch {
|
|
678
|
+
throw new DeliveryValidationError(["delivery-receipt-invalid"]);
|
|
679
|
+
}
|
|
680
|
+
const diagnostics = new Set<string>();
|
|
681
|
+
if (receipt.change !== input.change) {
|
|
682
|
+
diagnostics.add("delivery-change-mismatch");
|
|
683
|
+
}
|
|
684
|
+
const receiptHash = sha256(receiptBytes);
|
|
685
|
+
if (
|
|
686
|
+
(input.deliveryRevision !== undefined &&
|
|
687
|
+
input.deliveryRevision !== receipt.deliveryRevision) ||
|
|
688
|
+
(input.receiptHash !== undefined && input.receiptHash !== receiptHash)
|
|
689
|
+
) {
|
|
690
|
+
diagnostics.add("delivery-revision-mismatch");
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
let inspection: OpenSpecDeliveryInspection | undefined;
|
|
694
|
+
try {
|
|
695
|
+
inspection = await inspect(consumerRoot, input.change);
|
|
696
|
+
} catch {
|
|
697
|
+
diagnostics.add("delivery-openspec-unavailable");
|
|
698
|
+
}
|
|
699
|
+
if (inspection) {
|
|
700
|
+
if (!inspection.strictValid) {
|
|
701
|
+
diagnostics.add("delivery-openspec-strict-invalid");
|
|
702
|
+
}
|
|
703
|
+
if (!inspection.planningComplete) {
|
|
704
|
+
diagnostics.add("delivery-openspec-incomplete");
|
|
705
|
+
}
|
|
706
|
+
if (inspection.schema !== receipt.schema) {
|
|
707
|
+
diagnostics.add("delivery-schema-mismatch");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
let gateABytes: Buffer | undefined;
|
|
712
|
+
let gateA: ReturnType<typeof parseGateAReceipt> | undefined;
|
|
713
|
+
try {
|
|
714
|
+
gateABytes = readPackageDeliveryFile(
|
|
715
|
+
consumerRoot,
|
|
716
|
+
`${changeRoot}/${receipt.approvals.gateA.path}`,
|
|
717
|
+
4 * 1024 * 1024,
|
|
718
|
+
);
|
|
719
|
+
if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
|
|
720
|
+
diagnostics.add("delivery-gate-a-hash-mismatch");
|
|
721
|
+
}
|
|
722
|
+
gateA = parseGateAReceipt(gateABytes);
|
|
723
|
+
if (
|
|
724
|
+
gateA.change !== receipt.change ||
|
|
725
|
+
gateA.schema !== receipt.schema
|
|
726
|
+
) {
|
|
727
|
+
diagnostics.add("delivery-gate-a-binding-mismatch");
|
|
728
|
+
}
|
|
729
|
+
} catch {
|
|
730
|
+
diagnostics.add("delivery-gate-a-invalid");
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const artifactBytes = new Map<string, Buffer>();
|
|
734
|
+
const artifactHashes = new Map(
|
|
735
|
+
receipt.artifacts.map((artifact) => [
|
|
736
|
+
artifact.path,
|
|
737
|
+
artifact.rawSha256,
|
|
738
|
+
]),
|
|
739
|
+
);
|
|
740
|
+
for (const artifact of receipt.artifacts) {
|
|
741
|
+
try {
|
|
742
|
+
const bytes = readPackageDeliveryFile(
|
|
743
|
+
consumerRoot,
|
|
744
|
+
`${changeRoot}/${artifact.path}`,
|
|
745
|
+
);
|
|
746
|
+
artifactBytes.set(artifact.path, bytes);
|
|
747
|
+
if (
|
|
748
|
+
artifact.path !== receipt.traceability.taskPath &&
|
|
749
|
+
sha256(bytes) !== artifact.rawSha256
|
|
750
|
+
) {
|
|
751
|
+
diagnostics.add(`delivery-artifact-hash-mismatch:${artifact.path}`);
|
|
752
|
+
}
|
|
753
|
+
} catch {
|
|
754
|
+
diagnostics.add(`delivery-artifact-unavailable:${artifact.path}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (inspection) {
|
|
758
|
+
const covered = new Set(receipt.artifacts.map((entry) => entry.path));
|
|
759
|
+
const expected = new Set(inspection.artifactPaths);
|
|
760
|
+
for (const relative of expected) {
|
|
761
|
+
if (!isValidRelativePath(relative) || !covered.has(relative)) {
|
|
762
|
+
diagnostics.add(`delivery-artifact-unbound:${relative}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
for (const relative of covered) {
|
|
766
|
+
if (!expected.has(relative)) {
|
|
767
|
+
diagnostics.add(`delivery-artifact-not-in-openspec:${relative}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (gateA) {
|
|
772
|
+
if (!verifyGateProof) {
|
|
773
|
+
diagnostics.add("delivery-gate-proof-verifier-unavailable");
|
|
774
|
+
} else if (
|
|
775
|
+
!proofVerified(() =>
|
|
776
|
+
verifyGateProof({
|
|
777
|
+
change: input.change,
|
|
778
|
+
gate: "gate-a",
|
|
779
|
+
proof: gateA.approval,
|
|
780
|
+
}),
|
|
781
|
+
)
|
|
782
|
+
) {
|
|
783
|
+
diagnostics.add("delivery-gate-a-proof-invalid");
|
|
784
|
+
}
|
|
785
|
+
for (const artifact of gateA.artifacts) {
|
|
786
|
+
if (artifactHashes.get(artifact.path) !== artifact.rawSha256) {
|
|
787
|
+
diagnostics.add(
|
|
788
|
+
`delivery-gate-a-artifact-mismatch:${artifact.path}`,
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (
|
|
794
|
+
verifyGateProof &&
|
|
795
|
+
!proofVerified(() =>
|
|
796
|
+
verifyGateProof({
|
|
797
|
+
change: input.change,
|
|
798
|
+
gate: "gate-b",
|
|
799
|
+
proof: receipt.approvals.gateB,
|
|
800
|
+
}),
|
|
801
|
+
)
|
|
802
|
+
) {
|
|
803
|
+
diagnostics.add("delivery-gate-b-proof-invalid");
|
|
804
|
+
}
|
|
805
|
+
if (gateA) {
|
|
806
|
+
if (!verifyFinalizedDelivery) {
|
|
807
|
+
diagnostics.add("delivery-finalization-verifier-unavailable");
|
|
808
|
+
} else if (
|
|
809
|
+
!proofVerified(() =>
|
|
810
|
+
verifyFinalizedDelivery({
|
|
811
|
+
change: input.change,
|
|
812
|
+
deliveryRevision: receipt.deliveryRevision,
|
|
813
|
+
receiptHash,
|
|
814
|
+
gateA: gateA.approval,
|
|
815
|
+
gateB: receipt.approvals.gateB,
|
|
816
|
+
planCanonicalHash: receipt.plan.canonicalHash,
|
|
817
|
+
}),
|
|
818
|
+
)
|
|
819
|
+
) {
|
|
820
|
+
diagnostics.add("delivery-finalization-proof-invalid");
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
let plan: ReturnType<typeof parseImplementPlan> | undefined;
|
|
825
|
+
let planBytes: Buffer | undefined;
|
|
826
|
+
try {
|
|
827
|
+
planBytes = readPackageDeliveryFile(consumerRoot, planRelative);
|
|
828
|
+
plan = parseImplementPlan(planBytes);
|
|
829
|
+
} catch {
|
|
830
|
+
diagnostics.add("delivery-plan-invalid");
|
|
831
|
+
}
|
|
832
|
+
if (plan && planBytes) {
|
|
833
|
+
if (plan.changeId !== input.change) {
|
|
834
|
+
diagnostics.add("delivery-plan-change-mismatch");
|
|
835
|
+
}
|
|
836
|
+
let compiled: ReturnType<typeof compileImplementPlan> | undefined;
|
|
837
|
+
try {
|
|
838
|
+
compiled = compileImplementPlan(plan, { consumerRoot });
|
|
839
|
+
} catch {
|
|
840
|
+
diagnostics.add("delivery-verification-closure-invalid");
|
|
841
|
+
}
|
|
842
|
+
if (
|
|
843
|
+
receipt.plan.path !== "implement-plan.json" ||
|
|
844
|
+
receipt.plan.schemaVersion !== IMPLEMENT_PLAN_SCHEMA_VERSION ||
|
|
845
|
+
receipt.plan.rawSha256 !== sha256(planBytes) ||
|
|
846
|
+
receipt.plan.canonicalHash !== hashCanonicalValue(plan)
|
|
847
|
+
) {
|
|
848
|
+
diagnostics.add("delivery-plan-binding-invalid");
|
|
849
|
+
}
|
|
850
|
+
if (
|
|
851
|
+
compiled &&
|
|
852
|
+
canonicalJson(compiled.closure) !==
|
|
853
|
+
canonicalJson(receipt.verificationClosure)
|
|
854
|
+
) {
|
|
855
|
+
diagnostics.add("delivery-verification-closure-mismatch");
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (plan) {
|
|
860
|
+
const tasksBytes = artifactBytes.get(receipt.traceability.taskPath);
|
|
861
|
+
const expectedTasksHash = artifactHashes.get(
|
|
862
|
+
receipt.traceability.taskPath,
|
|
863
|
+
);
|
|
864
|
+
if (
|
|
865
|
+
tasksBytes &&
|
|
866
|
+
expectedTasksHash &&
|
|
867
|
+
sha256(tasksBytes) !== expectedTasksHash
|
|
868
|
+
) {
|
|
869
|
+
const normalized = normalizedTrackingArtifact(
|
|
870
|
+
tasksBytes,
|
|
871
|
+
plan.tracking.taskIds,
|
|
872
|
+
);
|
|
873
|
+
if (!normalized || sha256(normalized) !== expectedTasksHash) {
|
|
874
|
+
diagnostics.add(
|
|
875
|
+
`delivery-artifact-hash-mismatch:${receipt.traceability.taskPath}`,
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const specs = [...artifactBytes]
|
|
880
|
+
.filter(
|
|
881
|
+
([relative]) =>
|
|
882
|
+
relative.startsWith("specs/") && relative.endsWith("/spec.md"),
|
|
883
|
+
)
|
|
884
|
+
.flatMap(([relative, bytes]) => {
|
|
885
|
+
try {
|
|
886
|
+
return [
|
|
887
|
+
{
|
|
888
|
+
path: relative,
|
|
889
|
+
text: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
890
|
+
},
|
|
891
|
+
];
|
|
892
|
+
} catch {
|
|
893
|
+
diagnostics.add(`delivery-artifact-encoding-invalid:${relative}`);
|
|
894
|
+
return [];
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
if (!tasksBytes || specs.length === 0) {
|
|
898
|
+
diagnostics.add("delivery-traceability-input-unavailable");
|
|
899
|
+
} else {
|
|
900
|
+
try {
|
|
901
|
+
const traceability = assessDeliveryTraceability({
|
|
902
|
+
tasksMarkdown: new TextDecoder("utf-8", { fatal: true }).decode(
|
|
903
|
+
tasksBytes,
|
|
904
|
+
),
|
|
905
|
+
specs,
|
|
906
|
+
plan,
|
|
907
|
+
});
|
|
908
|
+
if (!traceability.ok) {
|
|
909
|
+
for (const diagnostic of traceability.diagnostics) {
|
|
910
|
+
diagnostics.add(diagnostic);
|
|
911
|
+
}
|
|
912
|
+
} else if (
|
|
913
|
+
canonicalJson(traceability.value) !==
|
|
914
|
+
canonicalJson(receipt.traceability)
|
|
915
|
+
) {
|
|
916
|
+
diagnostics.add("delivery-traceability-binding-mismatch");
|
|
917
|
+
}
|
|
918
|
+
} catch {
|
|
919
|
+
diagnostics.add("delivery-traceability-invalid");
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (!plan || !gateA || diagnostics.size > 0) {
|
|
924
|
+
throw new DeliveryValidationError([...diagnostics]);
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
version: 2,
|
|
928
|
+
gate: "gate-b",
|
|
929
|
+
revision: receipt.deliveryRevision,
|
|
930
|
+
receiptHash,
|
|
931
|
+
plan,
|
|
932
|
+
approvalProofs: {
|
|
933
|
+
gateA: structuredClone(gateA.approval),
|
|
934
|
+
gateB: structuredClone(receipt.approvals.gateB),
|
|
935
|
+
},
|
|
936
|
+
};
|
|
937
|
+
},
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function packageScriptInvocation(
|
|
942
|
+
packageManager: string,
|
|
943
|
+
script: string,
|
|
944
|
+
args: string[],
|
|
945
|
+
): string[] {
|
|
946
|
+
return packageManager === "npm" || packageManager === "pnpm"
|
|
947
|
+
? ["run", script, "--", ...args]
|
|
948
|
+
: ["run", script, ...args];
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function bindingFor(
|
|
952
|
+
bindings: readonly VerificationRunnerBinding[],
|
|
953
|
+
command: string,
|
|
954
|
+
): VerificationRunnerBinding | undefined {
|
|
955
|
+
return bindings.find((binding) => binding.command === command);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function prepareRunnerBindings(
|
|
959
|
+
root: string,
|
|
960
|
+
bindings: readonly VerificationRunnerBinding[],
|
|
961
|
+
): {
|
|
962
|
+
bindings: VerificationRunnerBinding[];
|
|
963
|
+
mounts: Array<{ source: string; target: string }>;
|
|
964
|
+
cleanupRoot?: string;
|
|
965
|
+
} {
|
|
966
|
+
if (!bindings.some((binding) => binding.mountSource)) {
|
|
967
|
+
return {
|
|
968
|
+
bindings: bindings.map((binding) => ({
|
|
969
|
+
...binding,
|
|
970
|
+
...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
|
|
971
|
+
})),
|
|
972
|
+
mounts: [],
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
const cleanupRoot = mkdtempSync(path.join(root, ".cadence-runners-"));
|
|
976
|
+
try {
|
|
977
|
+
const mounted = new Map<
|
|
978
|
+
string,
|
|
979
|
+
{ source: string; target: string; sandboxTarget: string }
|
|
980
|
+
>();
|
|
981
|
+
const prepared = bindings.map((binding) => {
|
|
982
|
+
if (!binding.mountSource) return { ...binding };
|
|
983
|
+
const source = path.resolve(binding.mountSource);
|
|
984
|
+
const sourceStat = lstatSync(source);
|
|
985
|
+
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
|
|
986
|
+
throw new Error("runner-mount-unavailable");
|
|
987
|
+
}
|
|
988
|
+
let mount = mounted.get(source);
|
|
989
|
+
if (!mount) {
|
|
990
|
+
const target = path.join(cleanupRoot, String(mounted.size));
|
|
991
|
+
mkdirSync(target, { mode: 0o700 });
|
|
992
|
+
const sandboxRelative = path
|
|
993
|
+
.relative(root, target)
|
|
994
|
+
.split(path.sep)
|
|
995
|
+
.join("/");
|
|
996
|
+
mount = {
|
|
997
|
+
source,
|
|
998
|
+
target,
|
|
999
|
+
sandboxTarget: `/workspace/${sandboxRelative}`,
|
|
1000
|
+
};
|
|
1001
|
+
mounted.set(source, mount);
|
|
1002
|
+
}
|
|
1003
|
+
const executable = realpathSync(binding.executablePath);
|
|
1004
|
+
const relative = path.relative(source, executable);
|
|
1005
|
+
if (
|
|
1006
|
+
relative === "" ||
|
|
1007
|
+
relative.startsWith("..") ||
|
|
1008
|
+
path.isAbsolute(relative)
|
|
1009
|
+
) {
|
|
1010
|
+
throw new Error("runner-mount-unavailable");
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
command: binding.command,
|
|
1014
|
+
executablePath: path.posix.join(
|
|
1015
|
+
mount.sandboxTarget,
|
|
1016
|
+
...relative.split(path.sep),
|
|
1017
|
+
),
|
|
1018
|
+
...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
|
|
1019
|
+
};
|
|
1020
|
+
});
|
|
1021
|
+
return {
|
|
1022
|
+
bindings: prepared,
|
|
1023
|
+
mounts: [...mounted.values()].map(({ source, sandboxTarget }) => ({
|
|
1024
|
+
source,
|
|
1025
|
+
target: sandboxTarget,
|
|
1026
|
+
})),
|
|
1027
|
+
cleanupRoot,
|
|
1028
|
+
};
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
rmSync(cleanupRoot, { recursive: true, force: true });
|
|
1031
|
+
throw error;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function verificationInvocation(
|
|
1036
|
+
step: AtomicVerificationContract,
|
|
1037
|
+
bindings: readonly VerificationRunnerBinding[],
|
|
1038
|
+
): { executable: string; args: string[] } | undefined {
|
|
1039
|
+
const reporter = step.kind === "vitest" ? ["--reporter=json"] : [];
|
|
1040
|
+
if (step.kind === "package-script") {
|
|
1041
|
+
const binding = bindingFor(bindings, step.packageManager);
|
|
1042
|
+
return binding
|
|
1043
|
+
? {
|
|
1044
|
+
executable: binding.executablePath,
|
|
1045
|
+
args: [
|
|
1046
|
+
...(binding.fixedArgs ?? []),
|
|
1047
|
+
...packageScriptInvocation(
|
|
1048
|
+
step.packageManager,
|
|
1049
|
+
step.script,
|
|
1050
|
+
step.args,
|
|
1051
|
+
),
|
|
1052
|
+
],
|
|
1053
|
+
}
|
|
1054
|
+
: undefined;
|
|
1055
|
+
}
|
|
1056
|
+
const runner = step.runner;
|
|
1057
|
+
if (runner.kind === "package-script") {
|
|
1058
|
+
const binding = bindingFor(bindings, runner.packageManager);
|
|
1059
|
+
const args =
|
|
1060
|
+
step.kind === "vitest"
|
|
1061
|
+
? [...step.testFiles, ...step.args, ...reporter]
|
|
1062
|
+
: step.args;
|
|
1063
|
+
return binding
|
|
1064
|
+
? {
|
|
1065
|
+
executable: binding.executablePath,
|
|
1066
|
+
args: [
|
|
1067
|
+
...(binding.fixedArgs ?? []),
|
|
1068
|
+
...packageScriptInvocation(
|
|
1069
|
+
runner.packageManager,
|
|
1070
|
+
runner.script,
|
|
1071
|
+
args,
|
|
1072
|
+
),
|
|
1073
|
+
],
|
|
1074
|
+
}
|
|
1075
|
+
: undefined;
|
|
1076
|
+
}
|
|
1077
|
+
if (runner.kind === "local-binary") {
|
|
1078
|
+
const binding = bindingFor(bindings, runner.executable);
|
|
1079
|
+
return {
|
|
1080
|
+
executable:
|
|
1081
|
+
binding?.executablePath ??
|
|
1082
|
+
`/workspace/node_modules/.bin/${runner.executable}`,
|
|
1083
|
+
args: [
|
|
1084
|
+
...(binding?.fixedArgs ?? []),
|
|
1085
|
+
...(step.kind === "vitest"
|
|
1086
|
+
? [...step.testFiles, ...step.args, ...reporter]
|
|
1087
|
+
: step.args),
|
|
1088
|
+
],
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
const command = runner.kind === "node" ? "node" : "npx";
|
|
1092
|
+
const binding = bindingFor(bindings, command);
|
|
1093
|
+
if (!binding) return undefined;
|
|
1094
|
+
return {
|
|
1095
|
+
executable: binding.executablePath,
|
|
1096
|
+
args:
|
|
1097
|
+
runner.kind === "node"
|
|
1098
|
+
? [...(binding.fixedArgs ?? []), runner.script, ...step.args]
|
|
1099
|
+
: [
|
|
1100
|
+
...(binding.fixedArgs ?? []),
|
|
1101
|
+
"--no-install",
|
|
1102
|
+
runner.executable,
|
|
1103
|
+
...(step.kind === "vitest" ? step.testFiles : []),
|
|
1104
|
+
...step.args,
|
|
1105
|
+
...reporter,
|
|
1106
|
+
],
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function vitestReport(stdout: string):
|
|
1111
|
+
| {
|
|
1112
|
+
total: number;
|
|
1113
|
+
failed: number;
|
|
1114
|
+
success: boolean;
|
|
1115
|
+
failures: string[];
|
|
1116
|
+
failedText: string[];
|
|
1117
|
+
}
|
|
1118
|
+
| undefined {
|
|
1119
|
+
let value: unknown;
|
|
1120
|
+
try {
|
|
1121
|
+
value = JSON.parse(stdout);
|
|
1122
|
+
} catch {
|
|
1123
|
+
return undefined;
|
|
1124
|
+
}
|
|
1125
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1126
|
+
return undefined;
|
|
1127
|
+
}
|
|
1128
|
+
const report = value as Record<string, unknown>;
|
|
1129
|
+
if (
|
|
1130
|
+
!Number.isSafeInteger(report.numTotalTests) ||
|
|
1131
|
+
!Number.isSafeInteger(report.numFailedTests) ||
|
|
1132
|
+
typeof report.success !== "boolean"
|
|
1133
|
+
) {
|
|
1134
|
+
return undefined;
|
|
1135
|
+
}
|
|
1136
|
+
const failedAssertions = (
|
|
1137
|
+
Array.isArray(report.testResults) ? report.testResults : []
|
|
1138
|
+
).flatMap((suite): Array<{ identity: string; text: string[] }> => {
|
|
1139
|
+
if (!suite || typeof suite !== "object" || Array.isArray(suite)) return [];
|
|
1140
|
+
const record = suite as Record<string, unknown>;
|
|
1141
|
+
const suiteName =
|
|
1142
|
+
typeof record.name === "string"
|
|
1143
|
+
? record.name.replace(/^\/workspace\//u, "")
|
|
1144
|
+
: "unknown-suite";
|
|
1145
|
+
if (!Array.isArray(record.assertionResults)) return [];
|
|
1146
|
+
return record.assertionResults.flatMap(
|
|
1147
|
+
(assertion): Array<{ identity: string; text: string[] }> => {
|
|
1148
|
+
if (
|
|
1149
|
+
!assertion ||
|
|
1150
|
+
typeof assertion !== "object" ||
|
|
1151
|
+
Array.isArray(assertion)
|
|
1152
|
+
) {
|
|
1153
|
+
return [];
|
|
1154
|
+
}
|
|
1155
|
+
const entry = assertion as Record<string, unknown>;
|
|
1156
|
+
if (entry.status !== "failed") return [];
|
|
1157
|
+
const title =
|
|
1158
|
+
typeof entry.fullName === "string"
|
|
1159
|
+
? entry.fullName
|
|
1160
|
+
: typeof entry.title === "string"
|
|
1161
|
+
? entry.title
|
|
1162
|
+
: "unknown-assertion";
|
|
1163
|
+
const diagnostics = Array.isArray(entry.failureMessages)
|
|
1164
|
+
? entry.failureMessages.filter(
|
|
1165
|
+
(message): message is string => typeof message === "string",
|
|
1166
|
+
)
|
|
1167
|
+
: [];
|
|
1168
|
+
return [
|
|
1169
|
+
{
|
|
1170
|
+
identity: `${suiteName}\0${title}`,
|
|
1171
|
+
text: [title, ...diagnostics],
|
|
1172
|
+
},
|
|
1173
|
+
];
|
|
1174
|
+
},
|
|
1175
|
+
);
|
|
1176
|
+
});
|
|
1177
|
+
return {
|
|
1178
|
+
total: report.numTotalTests as number,
|
|
1179
|
+
failed: report.numFailedTests as number,
|
|
1180
|
+
success: report.success,
|
|
1181
|
+
failures: failedAssertions.map((failure) => failure.identity),
|
|
1182
|
+
failedText: failedAssertions.flatMap((failure) => failure.text),
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function normalizedFailureIdentities(input: {
|
|
1187
|
+
step: AtomicVerificationContract;
|
|
1188
|
+
report?: ReturnType<typeof vitestReport>;
|
|
1189
|
+
output: string;
|
|
1190
|
+
exitCode: number;
|
|
1191
|
+
}): string[] {
|
|
1192
|
+
const vitestFailures = input.report?.failures ?? [];
|
|
1193
|
+
if (vitestFailures.length > 0) {
|
|
1194
|
+
return [...new Set(vitestFailures)]
|
|
1195
|
+
.sort()
|
|
1196
|
+
.map((failure) => sha256(`vitest-failure-v1\0${failure}`));
|
|
1197
|
+
}
|
|
1198
|
+
const normalizedOutput = input.output
|
|
1199
|
+
.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu"), "")
|
|
1200
|
+
.replace(/\r\n?/gu, "\n")
|
|
1201
|
+
.replace(/\b\d+(?:\.\d+)?(?:ms|s)\b/gu, "<duration>")
|
|
1202
|
+
.slice(0, 64 * 1024);
|
|
1203
|
+
return [
|
|
1204
|
+
sha256(
|
|
1205
|
+
canonicalJson({
|
|
1206
|
+
version: 1,
|
|
1207
|
+
verificationId: input.step.id,
|
|
1208
|
+
exitCode: input.exitCode,
|
|
1209
|
+
output: normalizedOutput,
|
|
1210
|
+
}),
|
|
1211
|
+
),
|
|
1212
|
+
];
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
export async function executePackageVerification(input: {
|
|
1216
|
+
root: string;
|
|
1217
|
+
dependencyOwner: string;
|
|
1218
|
+
verification: StructuredVerificationContract;
|
|
1219
|
+
signal: AbortSignal;
|
|
1220
|
+
}) {
|
|
1221
|
+
const capability = bindCurrentVerificationCapability(
|
|
1222
|
+
input.root,
|
|
1223
|
+
input.verification,
|
|
1224
|
+
{ dependencyOwner: input.dependencyOwner },
|
|
1225
|
+
);
|
|
1226
|
+
if (!capability.ok) {
|
|
1227
|
+
return {
|
|
1228
|
+
ok: false as const,
|
|
1229
|
+
kind: "paused" as const,
|
|
1230
|
+
code: capability.diagnostic.code,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
let runners: ReturnType<typeof prepareRunnerBindings>;
|
|
1234
|
+
try {
|
|
1235
|
+
runners = prepareRunnerBindings(
|
|
1236
|
+
input.root,
|
|
1237
|
+
capability.value.runnerBindings,
|
|
1238
|
+
);
|
|
1239
|
+
} catch {
|
|
1240
|
+
return {
|
|
1241
|
+
ok: false as const,
|
|
1242
|
+
kind: "paused" as const,
|
|
1243
|
+
code: "runner-mount-unavailable",
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
const nodeModules = path.join(input.dependencyOwner, "node_modules");
|
|
1248
|
+
const dependency = lstatSync(nodeModules, { throwIfNoEntry: false });
|
|
1249
|
+
const mounts = [] as Array<{
|
|
1250
|
+
source: string;
|
|
1251
|
+
target: string;
|
|
1252
|
+
writable?: boolean;
|
|
1253
|
+
}>;
|
|
1254
|
+
if (dependency) {
|
|
1255
|
+
if (!dependency.isDirectory() || dependency.isSymbolicLink()) {
|
|
1256
|
+
return {
|
|
1257
|
+
ok: false as const,
|
|
1258
|
+
kind: "paused" as const,
|
|
1259
|
+
code: "dependency-path-unsafe",
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
const target = path.join(input.root, "node_modules");
|
|
1263
|
+
const targetStat = lstatSync(target, { throwIfNoEntry: false });
|
|
1264
|
+
if (
|
|
1265
|
+
targetStat &&
|
|
1266
|
+
(!targetStat.isDirectory() || targetStat.isSymbolicLink())
|
|
1267
|
+
) {
|
|
1268
|
+
return {
|
|
1269
|
+
ok: false as const,
|
|
1270
|
+
kind: "paused" as const,
|
|
1271
|
+
code: "dependency-path-unsafe",
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
if (!targetStat) mkdirSync(target, { mode: 0o700 });
|
|
1275
|
+
mounts.push({ source: nodeModules, target: "/workspace/node_modules" });
|
|
1276
|
+
}
|
|
1277
|
+
mounts.push(...runners.mounts);
|
|
1278
|
+
const isolation = new BubblewrapIsolationBackend();
|
|
1279
|
+
const steps = verificationSteps(input.verification);
|
|
1280
|
+
let final:
|
|
1281
|
+
| {
|
|
1282
|
+
exitCode: number;
|
|
1283
|
+
classification: AtomicVerificationContract["classification"];
|
|
1284
|
+
diagnostic: { kind: "assertion" | "compiler"; id: string };
|
|
1285
|
+
}
|
|
1286
|
+
| undefined;
|
|
1287
|
+
for (const step of steps) {
|
|
1288
|
+
const invocation = verificationInvocation(step, runners.bindings);
|
|
1289
|
+
if (!invocation) {
|
|
1290
|
+
return {
|
|
1291
|
+
ok: false as const,
|
|
1292
|
+
kind: "paused" as const,
|
|
1293
|
+
code: "runner-missing",
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
const executed = await isolation.run({
|
|
1297
|
+
root: input.root,
|
|
1298
|
+
executable: invocation.executable,
|
|
1299
|
+
args: invocation.args,
|
|
1300
|
+
mounts,
|
|
1301
|
+
environment: { CI: "1" },
|
|
1302
|
+
signal: input.signal,
|
|
1303
|
+
});
|
|
1304
|
+
if (!executed.ok) {
|
|
1305
|
+
if (executed.state === "cancelled") throw input.signal.reason;
|
|
1306
|
+
return {
|
|
1307
|
+
ok: false as const,
|
|
1308
|
+
kind: "paused" as const,
|
|
1309
|
+
code: executed.code,
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
const report =
|
|
1313
|
+
step.kind === "vitest" ? vitestReport(executed.stdout) : undefined;
|
|
1314
|
+
const output = `${executed.stdout}\n${executed.stderr}`;
|
|
1315
|
+
const identity =
|
|
1316
|
+
step.classification !== "expected-red" ||
|
|
1317
|
+
(step.kind === "vitest"
|
|
1318
|
+
? report?.failedText.some((text) =>
|
|
1319
|
+
text.includes(step.expectedFailure ?? ""),
|
|
1320
|
+
) === true
|
|
1321
|
+
: output.includes(step.expectedFailure ?? ""));
|
|
1322
|
+
const accepted =
|
|
1323
|
+
step.classification === "expected-red"
|
|
1324
|
+
? executed.exitCode !== 0 &&
|
|
1325
|
+
identity &&
|
|
1326
|
+
(step.kind !== "vitest" ||
|
|
1327
|
+
(report !== undefined &&
|
|
1328
|
+
report.failed > 0 &&
|
|
1329
|
+
report.total >= step.minTests))
|
|
1330
|
+
: executed.exitCode === 0 &&
|
|
1331
|
+
(step.kind !== "vitest" ||
|
|
1332
|
+
(report?.success === true && report.total >= step.minTests));
|
|
1333
|
+
if (!accepted) {
|
|
1334
|
+
return {
|
|
1335
|
+
ok: false as const,
|
|
1336
|
+
kind: "retryable" as const,
|
|
1337
|
+
code:
|
|
1338
|
+
step.classification === "expected-red" && executed.exitCode === 0
|
|
1339
|
+
? "red-not-witnessed"
|
|
1340
|
+
: "verification-rejected",
|
|
1341
|
+
failureIdentities: normalizedFailureIdentities({
|
|
1342
|
+
step,
|
|
1343
|
+
report,
|
|
1344
|
+
output,
|
|
1345
|
+
exitCode: executed.exitCode,
|
|
1346
|
+
}),
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
if (!isVerificationCapabilityCurrent(input.root, capability.value)) {
|
|
1350
|
+
return {
|
|
1351
|
+
ok: false as const,
|
|
1352
|
+
kind: "retryable" as const,
|
|
1353
|
+
code: "verification-input-unavailable",
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
final = {
|
|
1357
|
+
exitCode: executed.exitCode,
|
|
1358
|
+
classification: step.classification,
|
|
1359
|
+
diagnostic: {
|
|
1360
|
+
kind: step.kind === "vitest" ? "assertion" : "compiler",
|
|
1361
|
+
id: step.id,
|
|
1362
|
+
},
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
return final
|
|
1366
|
+
? { ok: true as const, ...final }
|
|
1367
|
+
: {
|
|
1368
|
+
ok: false as const,
|
|
1369
|
+
kind: "paused" as const,
|
|
1370
|
+
code: "verification-contract-unsupported",
|
|
1371
|
+
};
|
|
1372
|
+
} finally {
|
|
1373
|
+
if (runners.cleanupRoot) {
|
|
1374
|
+
rmSync(runners.cleanupRoot, { recursive: true, force: true });
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function affectedVerification(
|
|
1380
|
+
task: Parameters<
|
|
1381
|
+
Parameters<typeof openDurableWorkflowEngine>[0]["proposeCandidate"]
|
|
1382
|
+
>[0]["task"],
|
|
1383
|
+
): StructuredVerificationContract {
|
|
1384
|
+
if (task.affectedVerification) {
|
|
1385
|
+
return structuredClone(task.affectedVerification);
|
|
1386
|
+
}
|
|
1387
|
+
const finalPhase = task.phases.refactor ?? task.phases.green;
|
|
1388
|
+
const verification = structuredClone(finalPhase.verification);
|
|
1389
|
+
const affected = [...new Set(task.impactClosure.affectedSuite)].sort();
|
|
1390
|
+
if (affected.length === 0) return verification;
|
|
1391
|
+
if (verification.kind === "vitest") {
|
|
1392
|
+
return { ...verification, testFiles: affected };
|
|
1393
|
+
}
|
|
1394
|
+
if (verification.kind === "steps") {
|
|
1395
|
+
return {
|
|
1396
|
+
...verification,
|
|
1397
|
+
steps: verification.steps.map((step) =>
|
|
1398
|
+
step.kind === "vitest" ? { ...step, testFiles: affected } : step,
|
|
1399
|
+
),
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
return verification;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function childRequestId(
|
|
1406
|
+
operationId: string,
|
|
1407
|
+
taskId: string,
|
|
1408
|
+
phase: string,
|
|
1409
|
+
): string {
|
|
1410
|
+
return `child-${sha256(`${operationId}\0${taskId}\0${phase}`).slice(0, 40)}`;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
export function openPackageWorkflowControlEngine(
|
|
1414
|
+
initialContext: ExtensionContext,
|
|
1415
|
+
parentPayloadBridge: ParentPayloadBridge,
|
|
1416
|
+
): WorkflowControlEngine {
|
|
1417
|
+
const consumerRoot = path.resolve(initialContext.cwd);
|
|
1418
|
+
const routeResolution = loadRoutePolicy({
|
|
1419
|
+
cwd: consumerRoot,
|
|
1420
|
+
home: homedir(),
|
|
1421
|
+
});
|
|
1422
|
+
const stateRoot = resolveStateRoot({
|
|
1423
|
+
consumerRoot,
|
|
1424
|
+
xdgStateHome: process.env.XDG_STATE_HOME,
|
|
1425
|
+
});
|
|
1426
|
+
const contexts = new AsyncLocalStorage<ExtensionContext>();
|
|
1427
|
+
const design = DesignController.open({
|
|
1428
|
+
consumerRoot,
|
|
1429
|
+
stateRoot,
|
|
1430
|
+
inspectOpenSpec: inspectOpenSpecDelivery,
|
|
1431
|
+
});
|
|
1432
|
+
const implementationAgent = loadAgentDefinitions().find(
|
|
1433
|
+
(agent) => agent.role === "implementation-worker",
|
|
1434
|
+
);
|
|
1435
|
+
if (!implementationAgent) {
|
|
1436
|
+
throw new Error("implementation-worker-agent-unavailable");
|
|
1437
|
+
}
|
|
1438
|
+
const engine = openDurableWorkflowEngine({
|
|
1439
|
+
consumerRoot,
|
|
1440
|
+
stateRoot,
|
|
1441
|
+
deliverySource: packageDeliverySource(consumerRoot, {
|
|
1442
|
+
verifyGateProof: (input) => design.verifyGateProof(input),
|
|
1443
|
+
verifyFinalizedDelivery: (input) => design.verifyFinalizedDelivery(input),
|
|
1444
|
+
}),
|
|
1445
|
+
routePolicy: routeResolution.ok
|
|
1446
|
+
? routeResolution.policy
|
|
1447
|
+
: unavailableRoutePolicy(),
|
|
1448
|
+
proposeCandidate: async (input) => {
|
|
1449
|
+
const context = contexts.getStore();
|
|
1450
|
+
if (!context) {
|
|
1451
|
+
return { kind: "paused", code: "parent-context-unavailable" };
|
|
1452
|
+
}
|
|
1453
|
+
const phaseRuntime = await runtimeForWorkerRoute(
|
|
1454
|
+
input.route as WorkerRoutePolicy,
|
|
1455
|
+
context,
|
|
1456
|
+
parentPayloadBridge,
|
|
1457
|
+
input.signal,
|
|
1458
|
+
);
|
|
1459
|
+
if (!phaseRuntime.ok) {
|
|
1460
|
+
if (phaseRuntime.failure.kind === "cancelled") {
|
|
1461
|
+
return { kind: "operation-cancelled", code: "cancelled" };
|
|
1462
|
+
}
|
|
1463
|
+
throw new Error(phaseRuntime.failure.code);
|
|
1464
|
+
}
|
|
1465
|
+
const phase = input.task.phases[input.phase];
|
|
1466
|
+
if (!phase) {
|
|
1467
|
+
return { kind: "paused", code: "task-phase-unavailable" };
|
|
1468
|
+
}
|
|
1469
|
+
const phaseContract = {
|
|
1470
|
+
candidateId: input.candidateArtifact.identity.candidateId,
|
|
1471
|
+
taskId: input.taskId,
|
|
1472
|
+
phase: input.phase,
|
|
1473
|
+
readSet: [...phase.read],
|
|
1474
|
+
writeSet: [...phase.write],
|
|
1475
|
+
deleteSet: [...phase.delete],
|
|
1476
|
+
verification: structuredClone(phase.verification),
|
|
1477
|
+
agentsImpact: input.task.agents.impact,
|
|
1478
|
+
agentsTarget: input.task.agents.target ?? null,
|
|
1479
|
+
agentsManagedOnly: true,
|
|
1480
|
+
agentsWriteAllowed: false,
|
|
1481
|
+
impactClosure: structuredClone(input.task.impactClosure),
|
|
1482
|
+
...(input.repair
|
|
1483
|
+
? {
|
|
1484
|
+
repair: {
|
|
1485
|
+
attempt: input.repair.attempt,
|
|
1486
|
+
attribution: input.repair.attribution,
|
|
1487
|
+
failureIdentities: [...input.repair.failureIdentities],
|
|
1488
|
+
verification: structuredClone(input.task.repairVerification),
|
|
1489
|
+
inBoundaryOnly: true,
|
|
1490
|
+
},
|
|
1491
|
+
}
|
|
1492
|
+
: {}),
|
|
1493
|
+
};
|
|
1494
|
+
const child = await runChildSession({
|
|
1495
|
+
cwd: input.workspaceRoot,
|
|
1496
|
+
modelRuntime: phaseRuntime.modelRuntime,
|
|
1497
|
+
model: phaseRuntime.model,
|
|
1498
|
+
systemPrompt: [
|
|
1499
|
+
implementationAgent.content,
|
|
1500
|
+
input.task.objective,
|
|
1501
|
+
input.task.context.agents,
|
|
1502
|
+
input.task.context.contract,
|
|
1503
|
+
`<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
|
|
1504
|
+
].join("\n\n"),
|
|
1505
|
+
requestId: childRequestId(input.operationId, input.taskId, input.phase),
|
|
1506
|
+
taskId: input.taskId,
|
|
1507
|
+
role: "implementation-worker",
|
|
1508
|
+
phase: input.phase,
|
|
1509
|
+
output: "diff",
|
|
1510
|
+
roots: input.task.roots.map((root) =>
|
|
1511
|
+
path.resolve(input.workspaceRoot, root),
|
|
1512
|
+
),
|
|
1513
|
+
allowedPaths: [
|
|
1514
|
+
...new Set([...phase.read, ...phase.write, ...phase.delete]),
|
|
1515
|
+
],
|
|
1516
|
+
timeoutMs: LIMITS.phaseTimeoutMs,
|
|
1517
|
+
signal: input.signal,
|
|
1518
|
+
failureOverride: phaseRuntime.failureOverride,
|
|
1519
|
+
ledgerProjection: input.ledgerProjection,
|
|
1520
|
+
candidateArtifact: input.candidateArtifact,
|
|
1521
|
+
onStreamStart: input.onHeaders,
|
|
1522
|
+
onStreamProgress: input.onProgress,
|
|
1523
|
+
});
|
|
1524
|
+
if (!child.ok) {
|
|
1525
|
+
if (child.failure.kind === "transport") {
|
|
1526
|
+
throw new Error(child.failure.code);
|
|
1527
|
+
}
|
|
1528
|
+
if (child.failure.kind === "cancelled") {
|
|
1529
|
+
return { kind: "operation-cancelled", code: "cancelled" };
|
|
1530
|
+
}
|
|
1531
|
+
if (child.failure.kind === "approval-boundary") {
|
|
1532
|
+
return { kind: "approval-needed", code: child.failure.code };
|
|
1533
|
+
}
|
|
1534
|
+
if (child.failure.kind === "result-limit") {
|
|
1535
|
+
return { kind: "paused", code: "needs-task-split" };
|
|
1536
|
+
}
|
|
1537
|
+
return child.failure.kind === "environment" ||
|
|
1538
|
+
child.failure.kind === "verification-adapter"
|
|
1539
|
+
? { kind: "paused", code: child.failure.code }
|
|
1540
|
+
: { kind: "retryable", code: child.failure.code };
|
|
1541
|
+
}
|
|
1542
|
+
const result = child.result;
|
|
1543
|
+
if (result.kind === "context-request") {
|
|
1544
|
+
return classifyCandidateContextRequest(result, [
|
|
1545
|
+
...phase.read,
|
|
1546
|
+
...phase.write,
|
|
1547
|
+
...phase.delete,
|
|
1548
|
+
]);
|
|
1549
|
+
}
|
|
1550
|
+
if (result.kind !== "sealed-candidate") {
|
|
1551
|
+
return { kind: "retryable", code: "candidate-diff-invalid" };
|
|
1552
|
+
}
|
|
1553
|
+
return {
|
|
1554
|
+
kind: "sealed-candidate",
|
|
1555
|
+
candidateId: result.candidateId,
|
|
1556
|
+
artifactHash: result.artifactHash,
|
|
1557
|
+
bytes: result.bytes,
|
|
1558
|
+
paths: [...result.paths],
|
|
1559
|
+
};
|
|
1560
|
+
},
|
|
1561
|
+
verifyPhase: (input) =>
|
|
1562
|
+
executePackageVerification({
|
|
1563
|
+
root: input.root,
|
|
1564
|
+
dependencyOwner: consumerRoot,
|
|
1565
|
+
verification: input.verification,
|
|
1566
|
+
signal: input.signal,
|
|
1567
|
+
}),
|
|
1568
|
+
verifyChange: async (input) => {
|
|
1569
|
+
const verifications = input.verification
|
|
1570
|
+
? [input.verification]
|
|
1571
|
+
: input.plan.tasks.map((task) => affectedVerification(task));
|
|
1572
|
+
for (const verification of verifications) {
|
|
1573
|
+
const result = await executePackageVerification({
|
|
1574
|
+
root: input.root,
|
|
1575
|
+
dependencyOwner: consumerRoot,
|
|
1576
|
+
verification,
|
|
1577
|
+
signal: input.signal,
|
|
1578
|
+
});
|
|
1579
|
+
if (!result.ok) {
|
|
1580
|
+
const adapter = new Set([
|
|
1581
|
+
"runner-missing",
|
|
1582
|
+
"script-missing",
|
|
1583
|
+
"script-command-mismatch",
|
|
1584
|
+
"local-executable-missing",
|
|
1585
|
+
]).has(result.code);
|
|
1586
|
+
return {
|
|
1587
|
+
ok: false,
|
|
1588
|
+
kind: adapter
|
|
1589
|
+
? ("verification-adapter" as const)
|
|
1590
|
+
: result.kind === "paused"
|
|
1591
|
+
? ("environment" as const)
|
|
1592
|
+
: ("verification" as const),
|
|
1593
|
+
code: result.code,
|
|
1594
|
+
...(result.failureIdentities
|
|
1595
|
+
? { failureIdentities: result.failureIdentities }
|
|
1596
|
+
: {}),
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
return {
|
|
1601
|
+
ok: true,
|
|
1602
|
+
exitCode: 0,
|
|
1603
|
+
classification: "expected-green",
|
|
1604
|
+
};
|
|
1605
|
+
},
|
|
1606
|
+
});
|
|
1607
|
+
return {
|
|
1608
|
+
async execute(
|
|
1609
|
+
command: unknown,
|
|
1610
|
+
context = initialContext,
|
|
1611
|
+
signal,
|
|
1612
|
+
onActivity,
|
|
1613
|
+
) {
|
|
1614
|
+
const operationRouteResolution = loadRoutePolicy({
|
|
1615
|
+
cwd: consumerRoot,
|
|
1616
|
+
home: homedir(),
|
|
1617
|
+
});
|
|
1618
|
+
engine.updateRoutePolicy(
|
|
1619
|
+
operationRouteResolution.ok
|
|
1620
|
+
? operationRouteResolution.policy
|
|
1621
|
+
: unavailableRoutePolicy(),
|
|
1622
|
+
);
|
|
1623
|
+
const validation = validateControlCommand(command);
|
|
1624
|
+
if (!validation.ok) {
|
|
1625
|
+
const error = new Error(validation.code);
|
|
1626
|
+
error.name = "ControlCommandError";
|
|
1627
|
+
throw error;
|
|
1628
|
+
}
|
|
1629
|
+
return contexts.run(context, async () => {
|
|
1630
|
+
const outcome = await engine.execute(
|
|
1631
|
+
validation.value,
|
|
1632
|
+
signal,
|
|
1633
|
+
onActivity,
|
|
1634
|
+
);
|
|
1635
|
+
const routePolicy = visibleRoutePolicyStatus(
|
|
1636
|
+
operationRouteResolution,
|
|
1637
|
+
engine.routePolicyStatus(),
|
|
1638
|
+
);
|
|
1639
|
+
const designStatus =
|
|
1640
|
+
validation.value.stage === "abel-design" &&
|
|
1641
|
+
typeof outcome.runId === "string"
|
|
1642
|
+
? { design: design.status(outcome.runId) }
|
|
1643
|
+
: {};
|
|
1644
|
+
return { ...outcome, ...designStatus, routePolicy };
|
|
1645
|
+
});
|
|
1646
|
+
},
|
|
1647
|
+
executeDesign(request: unknown) {
|
|
1648
|
+
return design.execute(request);
|
|
1649
|
+
},
|
|
1650
|
+
assertDesignRun(runId: string) {
|
|
1651
|
+
design.assertDesignRun(runId);
|
|
1652
|
+
},
|
|
1653
|
+
recordDesignEvidence(input) {
|
|
1654
|
+
return design.recordEvidence(input);
|
|
1655
|
+
},
|
|
1656
|
+
async close() {
|
|
1657
|
+
contexts.disable();
|
|
1658
|
+
await engine.close();
|
|
1659
|
+
design.close();
|
|
1660
|
+
},
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function visibleRoutePolicyStatus(
|
|
1665
|
+
resolution: RoutePolicyResolution,
|
|
1666
|
+
brokerStatus: Record<string, unknown> | undefined,
|
|
1667
|
+
): Record<string, unknown> {
|
|
1668
|
+
if (!resolution.ok) return inspectRoutePolicy(resolution);
|
|
1669
|
+
const inspected = brokerStatus ?? inspectRoutePolicy(resolution);
|
|
1670
|
+
return {
|
|
1671
|
+
...inspected,
|
|
1672
|
+
source: { kind: resolution.source.kind },
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
export function registerWorkflowControl(
|
|
1677
|
+
pi: ExtensionAPI,
|
|
1678
|
+
engineFactory: WorkflowControlEngineFactory = openPackageWorkflowControlEngine,
|
|
1679
|
+
): void {
|
|
222
1680
|
const parentPayloadBridge = new ParentPayloadBridge();
|
|
223
|
-
const runtime = new Runtime({ parentPayloadBridge });
|
|
224
1681
|
const activity = new ActivityController();
|
|
1682
|
+
const engines = new Map<string, Promise<WorkflowControlEngine>>();
|
|
1683
|
+
let pendingPrompt: EligiblePrompt | undefined;
|
|
1684
|
+
let activePrompt: EligiblePrompt | undefined;
|
|
1685
|
+
let designToolSnapshot: string[] | undefined;
|
|
1686
|
+
const activation = new (class implements Activation {
|
|
1687
|
+
state: Activation["state"] = "inactive";
|
|
225
1688
|
|
|
226
|
-
|
|
1689
|
+
isActive(): boolean {
|
|
1690
|
+
return this.state === "active";
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
request(): boolean {
|
|
1694
|
+
if (this.state !== "inactive") return false;
|
|
1695
|
+
this.state = "pending";
|
|
1696
|
+
return true;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
activate(): boolean {
|
|
1700
|
+
if (this.state !== "pending") return false;
|
|
1701
|
+
this.state = "active";
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
drain(): boolean {
|
|
1706
|
+
if (this.state === "inactive" || this.state === "pending") return false;
|
|
1707
|
+
this.state = "inactive";
|
|
1708
|
+
return true;
|
|
1709
|
+
}
|
|
1710
|
+
})();
|
|
1711
|
+
const packetRuntime = new PacketRuntime({ activation, parentPayloadBridge });
|
|
1712
|
+
|
|
1713
|
+
const engineFor = (ctx: ExtensionContext) => {
|
|
1714
|
+
const key = ctx.cwd;
|
|
1715
|
+
const existing = engines.get(key);
|
|
1716
|
+
if (existing) return existing;
|
|
1717
|
+
const opened = Promise.resolve(engineFactory(ctx, parentPayloadBridge));
|
|
1718
|
+
engines.set(key, opened);
|
|
1719
|
+
void opened.catch(() => {
|
|
1720
|
+
if (engines.get(key) === opened) engines.delete(key);
|
|
1721
|
+
});
|
|
1722
|
+
return opened;
|
|
1723
|
+
};
|
|
1724
|
+
const closeEngines = async () => {
|
|
1725
|
+
const current = [...engines.values()];
|
|
1726
|
+
engines.clear();
|
|
1727
|
+
const opened = await Promise.allSettled(current);
|
|
1728
|
+
await Promise.allSettled(
|
|
1729
|
+
opened.flatMap((result) =>
|
|
1730
|
+
result.status === "fulfilled"
|
|
1731
|
+
? [Promise.resolve(result.value.close())]
|
|
1732
|
+
: [],
|
|
1733
|
+
),
|
|
1734
|
+
);
|
|
1735
|
+
};
|
|
1736
|
+
const restoreDesignTools = () => {
|
|
1737
|
+
if (!designToolSnapshot) return;
|
|
1738
|
+
const snapshot = designToolSnapshot;
|
|
1739
|
+
designToolSnapshot = undefined;
|
|
1740
|
+
pi.setActiveTools([...snapshot]);
|
|
1741
|
+
};
|
|
1742
|
+
const enforceDesignTools = () => {
|
|
1743
|
+
if (!designToolSnapshot) {
|
|
1744
|
+
designToolSnapshot = pi
|
|
1745
|
+
.getActiveTools()
|
|
1746
|
+
.filter((name) => name !== DISPATCH_TOOL);
|
|
1747
|
+
}
|
|
1748
|
+
pi.setActiveTools(
|
|
1749
|
+
activateTool(
|
|
1750
|
+
designToolSnapshot.filter((name) => DESIGN_PARENT_READ_TOOLS.has(name)),
|
|
1751
|
+
DISPATCH_TOOL,
|
|
1752
|
+
),
|
|
1753
|
+
);
|
|
1754
|
+
};
|
|
1755
|
+
const deactivate = () => {
|
|
1756
|
+
activation.drain();
|
|
1757
|
+
if (designToolSnapshot) {
|
|
1758
|
+
restoreDesignTools();
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
const active = pi.getActiveTools();
|
|
1762
|
+
if (active.includes(DISPATCH_TOOL)) {
|
|
1763
|
+
pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
const deactivateStage = async () => {
|
|
1767
|
+
activePrompt = undefined;
|
|
1768
|
+
await packetRuntime.drain();
|
|
1769
|
+
deactivate();
|
|
1770
|
+
parentPayloadBridge.clear();
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1773
|
+
// Pi validates tool arguments before execute(), so expose only the schema
|
|
1774
|
+
// legal for the active stage instead of one ambiguous command/packet union.
|
|
1775
|
+
const CONTROL_COMMAND_PARAMETERS = {
|
|
1776
|
+
type: "object",
|
|
1777
|
+
properties: {
|
|
1778
|
+
command: {
|
|
1779
|
+
type: "string",
|
|
1780
|
+
enum: ["start", "status", "resume", "rebind", "cancel", "discard"],
|
|
1781
|
+
},
|
|
1782
|
+
stage: {
|
|
1783
|
+
type: "string",
|
|
1784
|
+
enum: ["abel-design", "abel-implement"],
|
|
1785
|
+
},
|
|
1786
|
+
change: { type: "string" },
|
|
1787
|
+
provisionalKey: { type: "string" },
|
|
1788
|
+
operationId: { type: "string" },
|
|
1789
|
+
deliveryRevision: { type: "integer", minimum: 1 },
|
|
1790
|
+
receiptHash: { type: "string" },
|
|
1791
|
+
routeId: { type: "string" },
|
|
1792
|
+
},
|
|
1793
|
+
required: ["command", "stage"],
|
|
1794
|
+
additionalProperties: false,
|
|
1795
|
+
} as const;
|
|
1796
|
+
const PACKET_PARAMETERS = {
|
|
1797
|
+
type: "object",
|
|
1798
|
+
properties: {
|
|
1799
|
+
action: { type: "string", enum: [...PACKET_ACTIONS, "design"] },
|
|
1800
|
+
request: {
|
|
1801
|
+
anyOf: [
|
|
1802
|
+
DESIGN_REQUEST_SCHEMA,
|
|
1803
|
+
DESIGN_CONTROL_REQUEST_SCHEMA,
|
|
1804
|
+
GENERIC_REQUEST_SCHEMA,
|
|
1805
|
+
],
|
|
1806
|
+
},
|
|
1807
|
+
},
|
|
1808
|
+
required: ["action"],
|
|
1809
|
+
additionalProperties: false,
|
|
1810
|
+
} as const;
|
|
1811
|
+
let registeredParameterKind: "command" | "packet" | undefined;
|
|
1812
|
+
|
|
1813
|
+
const registerDispatchTool = (kind: "command" | "packet") => {
|
|
1814
|
+
if (registeredParameterKind === kind) return;
|
|
1815
|
+
registeredParameterKind = kind;
|
|
227
1816
|
pi.registerTool({
|
|
228
1817
|
name: DISPATCH_TOOL,
|
|
229
|
-
label: "Abel
|
|
1818
|
+
label: "Abel Control",
|
|
230
1819
|
description:
|
|
231
|
-
|
|
232
|
-
? "
|
|
233
|
-
: "Private
|
|
1820
|
+
kind === "command"
|
|
1821
|
+
? "Private stage-bound Abel workflow control. Accepts durable Design and Implement change commands."
|
|
1822
|
+
: "Private stage-bound Abel packet control. Accepts bounded Design and Diagnose packet operations.",
|
|
234
1823
|
executionMode: "parallel",
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
type: "object",
|
|
238
|
-
properties: {
|
|
239
|
-
action: { type: "string", enum: [...ACTIONS] },
|
|
240
|
-
request: requestSchemaForStage(stage),
|
|
241
|
-
resultId: {
|
|
242
|
-
type: "string",
|
|
243
|
-
description: "Retained result id for apply/discard",
|
|
244
|
-
},
|
|
245
|
-
requestId: {
|
|
246
|
-
type: "string",
|
|
247
|
-
description: "Current Implement apply/discard operation identity",
|
|
248
|
-
},
|
|
249
|
-
rejection: {
|
|
250
|
-
type: "object",
|
|
251
|
-
description: "Typed parent rejection for Implement discard",
|
|
252
|
-
},
|
|
253
|
-
agentsCheckpoint: {
|
|
254
|
-
type: "object",
|
|
255
|
-
description:
|
|
256
|
-
"Parent-owned approved managed-block checkpoint for action=apply (mutually exclusive with resultId)",
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
required: ["action"],
|
|
260
|
-
},
|
|
1824
|
+
parameters:
|
|
1825
|
+
kind === "command" ? CONTROL_COMMAND_PARAMETERS : PACKET_PARAMETERS,
|
|
261
1826
|
async execute(
|
|
262
1827
|
toolCallId: string,
|
|
263
|
-
params:
|
|
264
|
-
action?: string;
|
|
265
|
-
request?: unknown;
|
|
266
|
-
resultId?: string;
|
|
267
|
-
requestId?: string;
|
|
268
|
-
rejection?: unknown;
|
|
269
|
-
agentsCheckpoint?: unknown;
|
|
270
|
-
},
|
|
1828
|
+
params: unknown,
|
|
271
1829
|
signal: AbortSignal | undefined,
|
|
272
1830
|
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
273
1831
|
ctx: ExtensionContext,
|
|
274
1832
|
) {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
action,
|
|
283
|
-
operation,
|
|
284
|
-
ctx,
|
|
285
|
-
signal,
|
|
286
|
-
activity.observe(
|
|
287
|
-
toolCallId,
|
|
288
|
-
onUpdate as ((result: unknown) => void) | undefined,
|
|
289
|
-
),
|
|
1833
|
+
const record =
|
|
1834
|
+
params && typeof params === "object" && !Array.isArray(params)
|
|
1835
|
+
? (params as Record<string, unknown>)
|
|
1836
|
+
: undefined;
|
|
1837
|
+
if (record && typeof record.action === "string") {
|
|
1838
|
+
if (
|
|
1839
|
+
Object.keys(record).some(
|
|
1840
|
+
(key) => key !== "action" && key !== "request",
|
|
290
1841
|
)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
1842
|
+
) {
|
|
1843
|
+
throw new Error("control-envelope-ambiguous");
|
|
1844
|
+
}
|
|
1845
|
+
if (
|
|
1846
|
+
activePrompt !== "abel-design" &&
|
|
1847
|
+
activePrompt !== "abel-diagnose"
|
|
1848
|
+
) {
|
|
1849
|
+
throw new Error("stage-control-mismatch");
|
|
1850
|
+
}
|
|
1851
|
+
if (record.action === "design") {
|
|
1852
|
+
if (activePrompt !== "abel-design") {
|
|
1853
|
+
throw new Error("stage-control-mismatch");
|
|
1854
|
+
}
|
|
1855
|
+
const designRequest = validateDesignControlRequest(record.request);
|
|
1856
|
+
if (!designRequest.ok) throw new Error(designRequest.code);
|
|
1857
|
+
const engine = await engineFor(ctx);
|
|
1858
|
+
if (!engine.executeDesign)
|
|
1859
|
+
throw new Error("design-control-unavailable");
|
|
1860
|
+
const payload = await engine.executeDesign(designRequest.value);
|
|
1861
|
+
if (
|
|
1862
|
+
designRequest.value.operation === "finalize-delivery" &&
|
|
1863
|
+
payload.state === "completed"
|
|
1864
|
+
) {
|
|
1865
|
+
await deactivateStage();
|
|
1866
|
+
}
|
|
1867
|
+
return {
|
|
1868
|
+
content: [
|
|
1869
|
+
{ type: "text" as const, text: JSON.stringify(payload) },
|
|
1870
|
+
],
|
|
1871
|
+
details: payload,
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
let designEngine: WorkflowControlEngine | undefined;
|
|
1875
|
+
if (record.action === "run") {
|
|
1876
|
+
const packet = packetRuntime.validateRequest(record.request);
|
|
1877
|
+
if (!packet.ok || packet.value.stage !== activePrompt) {
|
|
1878
|
+
throw new Error(
|
|
1879
|
+
packet.ok ? "stage-control-mismatch" : packet.reason,
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
if (packet.value.stage === "abel-design") {
|
|
1883
|
+
designEngine = await engineFor(ctx);
|
|
1884
|
+
if (!designEngine.assertDesignRun || !packet.value.runId) {
|
|
1885
|
+
throw new Error("design-control-unavailable");
|
|
1886
|
+
}
|
|
1887
|
+
designEngine.assertDesignRun(packet.value.runId);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
const operation = {
|
|
1891
|
+
request: record.request,
|
|
1892
|
+
};
|
|
1893
|
+
const tuiRun = ctx.mode === "tui" && record.action === "run";
|
|
1894
|
+
const packetPayload = tuiRun
|
|
1895
|
+
? await packetRuntime.execute(
|
|
1896
|
+
record.action,
|
|
1897
|
+
operation,
|
|
1898
|
+
ctx,
|
|
1899
|
+
signal,
|
|
1900
|
+
activity.observe(
|
|
1901
|
+
toolCallId,
|
|
1902
|
+
onUpdate as ((result: unknown) => void) | undefined,
|
|
1903
|
+
),
|
|
1904
|
+
)
|
|
1905
|
+
: await packetRuntime.execute(
|
|
1906
|
+
record.action,
|
|
1907
|
+
operation,
|
|
1908
|
+
ctx,
|
|
1909
|
+
signal,
|
|
1910
|
+
);
|
|
1911
|
+
let payload:
|
|
1912
|
+
| typeof packetPayload
|
|
1913
|
+
| (typeof packetPayload & {
|
|
1914
|
+
recordedEvidence: unknown;
|
|
1915
|
+
}) = packetPayload;
|
|
1916
|
+
if (
|
|
1917
|
+
packetPayload.ok &&
|
|
1918
|
+
record.action === "run" &&
|
|
1919
|
+
activePrompt === "abel-design"
|
|
1920
|
+
) {
|
|
1921
|
+
const packet = packetRuntime.validateRequest(record.request);
|
|
1922
|
+
if (
|
|
1923
|
+
!packet.ok ||
|
|
1924
|
+
!packet.value.runId ||
|
|
1925
|
+
!designEngine?.recordDesignEvidence
|
|
1926
|
+
) {
|
|
1927
|
+
throw new Error("design-control-unavailable");
|
|
300
1928
|
}
|
|
301
|
-
|
|
1929
|
+
const recordedEvidence = designEngine.recordDesignEvidence({
|
|
1930
|
+
runId: packet.value.runId,
|
|
1931
|
+
evidence: packetPayload.result as DesignEvidenceResult,
|
|
1932
|
+
});
|
|
1933
|
+
payload = { ...packetPayload, recordedEvidence };
|
|
1934
|
+
}
|
|
1935
|
+
if (record.action === "finish" && packetPayload.ok) {
|
|
1936
|
+
await deactivateStage();
|
|
1937
|
+
}
|
|
1938
|
+
const display = tuiRun
|
|
1939
|
+
? activity.finalize(toolCallId, payload)
|
|
1940
|
+
: undefined;
|
|
1941
|
+
const { payload: publicPayload, usage } = splitUsage(payload);
|
|
1942
|
+
const details = display
|
|
1943
|
+
? {
|
|
1944
|
+
...(publicPayload as Record<string, unknown>),
|
|
1945
|
+
[ACTIVITY_DETAILS_KEY]: display,
|
|
1946
|
+
}
|
|
1947
|
+
: publicPayload;
|
|
1948
|
+
return {
|
|
1949
|
+
content: [
|
|
1950
|
+
{ type: "text" as const, text: JSON.stringify(publicPayload) },
|
|
1951
|
+
],
|
|
1952
|
+
details,
|
|
1953
|
+
...(usage === undefined ? {} : { usage }),
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
const validation = validateControlCommand(params);
|
|
1957
|
+
if (!validation.ok) {
|
|
1958
|
+
const error = new Error(validation.code);
|
|
1959
|
+
error.name = "ControlCommandError";
|
|
1960
|
+
throw error;
|
|
1961
|
+
}
|
|
1962
|
+
if (
|
|
1963
|
+
activePrompt !== undefined &&
|
|
1964
|
+
(activePrompt === "abel-diagnose" ||
|
|
1965
|
+
validation.value.stage !== activePrompt)
|
|
1966
|
+
) {
|
|
1967
|
+
throw new Error("stage-control-mismatch");
|
|
1968
|
+
}
|
|
1969
|
+
const engine = await engineFor(ctx);
|
|
1970
|
+
const tuiControl = ctx.mode === "tui";
|
|
1971
|
+
if (tuiControl) {
|
|
1972
|
+
try {
|
|
1973
|
+
activity.beginWorkflow(
|
|
1974
|
+
toolCallId,
|
|
1975
|
+
validation.value,
|
|
1976
|
+
onUpdate as ((result: unknown) => void) | undefined,
|
|
1977
|
+
);
|
|
1978
|
+
} catch {
|
|
1979
|
+
// Presentation state must never alter workflow execution.
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
let payload: Record<string, unknown>;
|
|
1983
|
+
try {
|
|
1984
|
+
payload = await engine.execute(
|
|
1985
|
+
validation.value,
|
|
1986
|
+
ctx,
|
|
1987
|
+
signal,
|
|
1988
|
+
tuiControl
|
|
1989
|
+
? (event) => {
|
|
1990
|
+
try {
|
|
1991
|
+
activity.updateWorkflow(
|
|
1992
|
+
toolCallId,
|
|
1993
|
+
validation.value,
|
|
1994
|
+
event,
|
|
1995
|
+
);
|
|
1996
|
+
} catch {
|
|
1997
|
+
// Presentation state must never alter workflow execution.
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
: undefined,
|
|
2001
|
+
);
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
if (tuiControl) activity.failWorkflow(toolCallId);
|
|
2004
|
+
throw error;
|
|
2005
|
+
}
|
|
2006
|
+
if (
|
|
2007
|
+
["completed", "discarded", "rejected"].includes(String(payload.state))
|
|
2008
|
+
) {
|
|
2009
|
+
await deactivateStage();
|
|
2010
|
+
} else if (validation.value.stage === "abel-design") {
|
|
2011
|
+
// Design starts/statuses through the command surface, then switches to
|
|
2012
|
+
// its bounded evidence and artifact packet surface for the next turn.
|
|
2013
|
+
registerDispatchTool("packet");
|
|
2014
|
+
}
|
|
2015
|
+
let display: ReturnType<typeof activity.finalizeWorkflow>;
|
|
2016
|
+
if (tuiControl) {
|
|
2017
|
+
try {
|
|
2018
|
+
display = activity.finalizeWorkflow(
|
|
2019
|
+
toolCallId,
|
|
2020
|
+
validation.value,
|
|
2021
|
+
payload,
|
|
2022
|
+
);
|
|
2023
|
+
} catch {
|
|
2024
|
+
activity.failWorkflow(toolCallId);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
302
2027
|
return {
|
|
303
|
-
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
304
|
-
details
|
|
305
|
-
|
|
2028
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload) }],
|
|
2029
|
+
details: display
|
|
2030
|
+
? { ...payload, [ACTIVITY_DETAILS_KEY]: display }
|
|
2031
|
+
: payload,
|
|
306
2032
|
};
|
|
307
2033
|
},
|
|
308
2034
|
renderCall(args: unknown, theme: Theme, context: unknown) {
|
|
@@ -327,26 +2053,23 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
327
2053
|
},
|
|
328
2054
|
} as never);
|
|
329
2055
|
};
|
|
330
|
-
|
|
331
|
-
registerDispatchTool();
|
|
332
|
-
|
|
333
|
-
let pendingPrompt: EligiblePrompt | undefined;
|
|
334
|
-
let activePrompt: EligiblePrompt | undefined;
|
|
2056
|
+
registerDispatchTool("packet");
|
|
335
2057
|
|
|
336
2058
|
pi.on("input", (event) => {
|
|
337
2059
|
pendingPrompt = invokedPrompt(event.text);
|
|
338
2060
|
return { action: "continue" };
|
|
339
2061
|
});
|
|
340
|
-
|
|
341
2062
|
pi.on("before_agent_start", (event, ctx) => {
|
|
342
2063
|
const prompt = pendingPrompt;
|
|
343
2064
|
pendingPrompt = undefined;
|
|
344
2065
|
const verified =
|
|
345
|
-
prompt &&
|
|
346
|
-
isVerifiedStageInvocation(pi, runtime.activation, prompt, event.prompt);
|
|
2066
|
+
prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
|
|
347
2067
|
if (verified) {
|
|
2068
|
+
if (activePrompt === "abel-design" && prompt !== "abel-design") {
|
|
2069
|
+
restoreDesignTools();
|
|
2070
|
+
}
|
|
2071
|
+
registerDispatchTool(prompt === "abel-diagnose" ? "packet" : "command");
|
|
348
2072
|
activePrompt = prompt;
|
|
349
|
-
registerDispatchTool(prompt);
|
|
350
2073
|
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
351
2074
|
if (typeof sessionId === "string") {
|
|
352
2075
|
parentPayloadBridge.beginSession(sessionId);
|
|
@@ -357,10 +2080,19 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
357
2080
|
if (ctx.model) {
|
|
358
2081
|
parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
|
|
359
2082
|
}
|
|
360
|
-
if (prompt)
|
|
361
|
-
|
|
2083
|
+
if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
|
|
2084
|
+
if (verified && prompt === "abel-design") enforceDesignTools();
|
|
2085
|
+
});
|
|
2086
|
+
pi.on("model_select", (event, ctx) => {
|
|
2087
|
+
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
2088
|
+
if (typeof sessionId !== "string") {
|
|
2089
|
+
parentPayloadBridge.clear();
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
parentPayloadBridge.beginSession(sessionId);
|
|
2093
|
+
const model = event.model ?? ctx.model;
|
|
2094
|
+
if (model) parentPayloadBridge.install(model, ctx.modelRegistry);
|
|
362
2095
|
});
|
|
363
|
-
|
|
364
2096
|
pi.on("before_provider_request", (event, ctx) => {
|
|
365
2097
|
if (
|
|
366
2098
|
activePrompt !== "abel-design" ||
|
|
@@ -373,50 +2105,33 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
373
2105
|
}
|
|
374
2106
|
return { ...event.payload, parallel_tool_calls: true };
|
|
375
2107
|
});
|
|
376
|
-
|
|
377
2108
|
pi.on("session_start", async (_event, ctx) => {
|
|
378
2109
|
pendingPrompt = undefined;
|
|
379
2110
|
activePrompt = undefined;
|
|
380
2111
|
activity.detach();
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
await runtime.drain();
|
|
386
|
-
activity.clear();
|
|
2112
|
+
await packetRuntime.drain();
|
|
2113
|
+
await closeEngines();
|
|
2114
|
+
deactivate();
|
|
387
2115
|
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
388
2116
|
if (typeof sessionId === "string") {
|
|
389
2117
|
parentPayloadBridge.beginSession(sessionId);
|
|
390
|
-
if (ctx.model)
|
|
391
|
-
parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
|
|
392
|
-
}
|
|
2118
|
+
if (ctx.model) parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
|
|
393
2119
|
} else {
|
|
394
2120
|
parentPayloadBridge.clear();
|
|
395
2121
|
}
|
|
396
2122
|
if (ctx.mode === "tui") activity.attach(ctx.ui);
|
|
397
2123
|
});
|
|
398
|
-
|
|
399
|
-
pi.on("model_select", (event, ctx) => {
|
|
400
|
-
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
401
|
-
if (typeof sessionId !== "string") {
|
|
402
|
-
parentPayloadBridge.clear();
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
parentPayloadBridge.beginSession(sessionId);
|
|
406
|
-
const model = event.model ?? ctx.model;
|
|
407
|
-
if (model) {
|
|
408
|
-
parentPayloadBridge.install(model, ctx.modelRegistry);
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
|
|
412
2124
|
pi.on("session_shutdown", async () => {
|
|
413
2125
|
activePrompt = undefined;
|
|
414
2126
|
activity.detach();
|
|
415
|
-
await
|
|
2127
|
+
await packetRuntime.drain();
|
|
2128
|
+
await closeEngines();
|
|
416
2129
|
activity.clear();
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
|
|
420
|
-
}
|
|
2130
|
+
deactivate();
|
|
2131
|
+
parentPayloadBridge.clear();
|
|
421
2132
|
});
|
|
422
2133
|
}
|
|
2134
|
+
|
|
2135
|
+
export default function register(pi: ExtensionAPI): void {
|
|
2136
|
+
registerWorkflowControl(pi);
|
|
2137
|
+
}
|