@abelxiaoxing/cadence 1.0.13 → 1.1.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 +29 -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 +150 -228
- package/prompts/abel-diagnose.md +37 -18
- package/prompts/abel-implement.md +111 -178
- package/prompts/abel-init.md +36 -33
- package/skills/abel-workflow/SKILL.md +156 -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 +105 -696
- package/src/control-contracts.ts +259 -0
- package/src/delivery-compiler.ts +1088 -0
- package/src/file-snapshot.ts +5 -0
- package/src/implement-graph.ts +11 -4
- package/src/index.ts +1465 -133
- 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 +393 -0
- package/src/run-store.ts +918 -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 +529 -42
- package/src/worker-broker.ts +703 -0
- package/src/workflow-engine.ts +7391 -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,15 +27,56 @@ 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
|
+
isValidRelativePath,
|
|
35
|
+
LIMITS,
|
|
36
|
+
type StructuredVerificationContract,
|
|
37
|
+
verificationSteps,
|
|
38
|
+
} from "./contracts.ts";
|
|
39
|
+
import { validateControlCommand } from "./control-contracts.ts";
|
|
40
|
+
import {
|
|
41
|
+
assessDeliveryTraceability,
|
|
42
|
+
compileImplementPlan,
|
|
43
|
+
DeliveryValidationError,
|
|
44
|
+
IMPLEMENT_PLAN_SCHEMA_VERSION,
|
|
45
|
+
parseGateAReceipt,
|
|
46
|
+
parseImplementPlan,
|
|
47
|
+
parseReadyReceipt,
|
|
48
|
+
} from "./delivery-compiler.ts";
|
|
49
|
+
import { canonicalJson, hashCanonicalValue } from "./implement-graph.ts";
|
|
50
|
+
import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
|
|
51
|
+
import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
|
|
18
52
|
import { ParentPayloadBridge } from "./parent-payload-bridge.ts";
|
|
19
|
-
import {
|
|
53
|
+
import { runtimeForWorkerRoute } from "./parent-provider.ts";
|
|
54
|
+
import {
|
|
55
|
+
inspectRoutePolicy,
|
|
56
|
+
loadRoutePolicy,
|
|
57
|
+
type RoutePolicyResolution,
|
|
58
|
+
unavailableRoutePolicy,
|
|
59
|
+
type WorkerRoutePolicy,
|
|
60
|
+
} from "./route-policy.ts";
|
|
61
|
+
import { observeSafePath } from "./safe-path.ts";
|
|
62
|
+
import { resolveStateRoot } from "./state-root.ts";
|
|
20
63
|
import {
|
|
21
64
|
ACTIVITY_DETAILS_KEY,
|
|
22
65
|
ActivityController,
|
|
23
66
|
renderActivityCall,
|
|
24
67
|
renderActivityResult,
|
|
68
|
+
type WorkflowActivityUpdate,
|
|
25
69
|
} from "./subagent-activity.ts";
|
|
70
|
+
import { classifyCandidateContextRequest } from "./submit-tool.ts";
|
|
71
|
+
import {
|
|
72
|
+
bindCurrentVerificationCapability,
|
|
73
|
+
isVerificationCapabilityCurrent,
|
|
74
|
+
type VerificationRunnerBinding,
|
|
75
|
+
} from "./verification-capability.ts";
|
|
76
|
+
import {
|
|
77
|
+
openDurableWorkflowEngine,
|
|
78
|
+
type WorkflowDeliverySource,
|
|
79
|
+
} from "./workflow-engine.ts";
|
|
26
80
|
|
|
27
81
|
export const DISPATCH_TOOL = "abel_dispatch";
|
|
28
82
|
|
|
@@ -139,12 +193,6 @@ const DESIGN_REQUEST_SCHEMA = {
|
|
|
139
193
|
additionalProperties: false,
|
|
140
194
|
} as const;
|
|
141
195
|
|
|
142
|
-
function requestSchemaForStage(name?: EligiblePrompt): object {
|
|
143
|
-
return name === "abel-design"
|
|
144
|
-
? DESIGN_REQUEST_SCHEMA
|
|
145
|
-
: GENERIC_REQUEST_SCHEMA;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
196
|
function invokedPrompt(text: string): EligiblePrompt | undefined {
|
|
149
197
|
const name = text.match(/^\/([^\s]+)(?:\s|$)/)?.[1];
|
|
150
198
|
return ELIGIBLE_PROMPTS.find((candidate) => candidate === name);
|
|
@@ -218,68 +266,1303 @@ function splitUsage(result: unknown): { payload: unknown; usage?: unknown } {
|
|
|
218
266
|
return { payload, usage };
|
|
219
267
|
}
|
|
220
268
|
|
|
221
|
-
export
|
|
269
|
+
export interface WorkflowControlEngine {
|
|
270
|
+
execute(
|
|
271
|
+
command: unknown,
|
|
272
|
+
context?: ExtensionContext,
|
|
273
|
+
signal?: AbortSignal,
|
|
274
|
+
onActivity?: (event: WorkflowActivityUpdate) => void,
|
|
275
|
+
): Promise<Record<string, unknown>>;
|
|
276
|
+
close(): void | Promise<void>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export type WorkflowControlEngineFactory = (
|
|
280
|
+
ctx: ExtensionContext,
|
|
281
|
+
parentPayloadBridge: ParentPayloadBridge,
|
|
282
|
+
) => WorkflowControlEngine | Promise<WorkflowControlEngine>;
|
|
283
|
+
|
|
284
|
+
const PACKAGE_DELIVERY_MAX_BYTES = 16 * 1024 * 1024;
|
|
285
|
+
const execFileAsync = promisify(execFile);
|
|
286
|
+
|
|
287
|
+
export interface OpenSpecDeliveryInspection {
|
|
288
|
+
change: string;
|
|
289
|
+
schema: string;
|
|
290
|
+
planningComplete: boolean;
|
|
291
|
+
strictValid: boolean;
|
|
292
|
+
artifactPaths: string[];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface PackageDeliverySourceOptions {
|
|
296
|
+
inspectOpenSpec?: (
|
|
297
|
+
consumerRoot: string,
|
|
298
|
+
change: string,
|
|
299
|
+
) => Promise<OpenSpecDeliveryInspection>;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function sha256(value: Uint8Array | string): string {
|
|
303
|
+
return createHash("sha256").update(value).digest("hex");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function normalizedTrackingArtifact(
|
|
307
|
+
bytes: Uint8Array,
|
|
308
|
+
taskIds: readonly string[],
|
|
309
|
+
): Buffer | undefined {
|
|
310
|
+
let text: string;
|
|
311
|
+
try {
|
|
312
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
313
|
+
} catch {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
const identities = taskIds.map((taskId) => ({
|
|
317
|
+
taskId,
|
|
318
|
+
pattern: new RegExp(
|
|
319
|
+
`(?:^|[^A-Za-z0-9._:-])${taskId.replace(
|
|
320
|
+
/[.*+?^${}()|[\]\\]/gu,
|
|
321
|
+
"\\$&",
|
|
322
|
+
)}(?![A-Za-z0-9._:-])`,
|
|
323
|
+
"u",
|
|
324
|
+
),
|
|
325
|
+
matches: 0,
|
|
326
|
+
}));
|
|
327
|
+
const normalized = text
|
|
328
|
+
.split(/(?<=\n)/u)
|
|
329
|
+
.map((segment) => {
|
|
330
|
+
const line = segment.endsWith("\n") ? segment.slice(0, -1) : segment;
|
|
331
|
+
if (!/^\s*-\s+\[[ xX]\]/u.test(line)) return segment;
|
|
332
|
+
const matching = identities.filter(({ pattern }) => pattern.test(line));
|
|
333
|
+
if (matching.length !== 1) return segment;
|
|
334
|
+
matching[0].matches += 1;
|
|
335
|
+
return segment.replace(/^(\s*-\s+)\[[xX]\]/u, "$1[ ]");
|
|
336
|
+
})
|
|
337
|
+
.join("");
|
|
338
|
+
if (identities.some(({ matches }) => matches !== 1)) return undefined;
|
|
339
|
+
return Buffer.from(normalized, "utf8");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function readPackageDeliveryFile(
|
|
343
|
+
root: string,
|
|
344
|
+
relative: string,
|
|
345
|
+
maximumBytes = PACKAGE_DELIVERY_MAX_BYTES,
|
|
346
|
+
): Buffer {
|
|
347
|
+
const observation = observeSafePath(root, relative);
|
|
348
|
+
if (observation.kind !== "file") {
|
|
349
|
+
throw new Error("delivery-file-unavailable");
|
|
350
|
+
}
|
|
351
|
+
const target = path.join(root, ...relative.split("/"));
|
|
352
|
+
const stat = lstatSync(target);
|
|
353
|
+
if (stat.size < 1 || stat.size > maximumBytes) {
|
|
354
|
+
throw new Error("delivery-file-size-invalid");
|
|
355
|
+
}
|
|
356
|
+
return readFileSync(target);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function deliveryRecord(value: unknown): Record<string, unknown> | undefined {
|
|
360
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
361
|
+
? (value as Record<string, unknown>)
|
|
362
|
+
: undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function parseCommandJson(
|
|
366
|
+
value: string,
|
|
367
|
+
code: string,
|
|
368
|
+
): Record<string, unknown> {
|
|
369
|
+
let parsed: unknown;
|
|
370
|
+
try {
|
|
371
|
+
parsed = JSON.parse(value);
|
|
372
|
+
} catch {
|
|
373
|
+
throw new Error(code);
|
|
374
|
+
}
|
|
375
|
+
const record = deliveryRecord(parsed);
|
|
376
|
+
if (!record) throw new Error(code);
|
|
377
|
+
return record;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function inspectOpenSpecDelivery(
|
|
381
|
+
consumerRoot: string,
|
|
382
|
+
change: string,
|
|
383
|
+
): Promise<OpenSpecDeliveryInspection> {
|
|
384
|
+
const [statusExecution, validationExecution] = await Promise.all([
|
|
385
|
+
execFileAsync("openspec", ["status", "--change", change, "--json"], {
|
|
386
|
+
cwd: consumerRoot,
|
|
387
|
+
encoding: "utf8",
|
|
388
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
389
|
+
}),
|
|
390
|
+
execFileAsync(
|
|
391
|
+
"openspec",
|
|
392
|
+
["validate", change, "--strict", "--json", "--no-interactive"],
|
|
393
|
+
{
|
|
394
|
+
cwd: consumerRoot,
|
|
395
|
+
encoding: "utf8",
|
|
396
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
397
|
+
},
|
|
398
|
+
),
|
|
399
|
+
]);
|
|
400
|
+
const status = parseCommandJson(
|
|
401
|
+
statusExecution.stdout,
|
|
402
|
+
"delivery-openspec-status-invalid",
|
|
403
|
+
);
|
|
404
|
+
const validation = parseCommandJson(
|
|
405
|
+
validationExecution.stdout,
|
|
406
|
+
"delivery-openspec-validation-invalid",
|
|
407
|
+
);
|
|
408
|
+
const changeRoot = path.resolve(consumerRoot, "openspec", "changes", change);
|
|
409
|
+
const artifactPathsRecord = deliveryRecord(status.artifactPaths);
|
|
410
|
+
const artifactPaths = artifactPathsRecord
|
|
411
|
+
? Object.values(artifactPathsRecord).flatMap((entry) => {
|
|
412
|
+
const record = deliveryRecord(entry);
|
|
413
|
+
if (!record || !Array.isArray(record.existingOutputPaths)) return [];
|
|
414
|
+
return record.existingOutputPaths.flatMap((candidate) => {
|
|
415
|
+
if (typeof candidate !== "string") return [];
|
|
416
|
+
const resolved = path.resolve(candidate);
|
|
417
|
+
const relative = path.relative(changeRoot, resolved);
|
|
418
|
+
return relative &&
|
|
419
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
420
|
+
relative !== ".." &&
|
|
421
|
+
!path.isAbsolute(relative)
|
|
422
|
+
? [relative.split(path.sep).join("/")]
|
|
423
|
+
: [];
|
|
424
|
+
});
|
|
425
|
+
})
|
|
426
|
+
: [];
|
|
427
|
+
const items = Array.isArray(validation.items) ? validation.items : [];
|
|
428
|
+
const strictValid =
|
|
429
|
+
items.length === 1 &&
|
|
430
|
+
deliveryRecord(items[0])?.id === change &&
|
|
431
|
+
deliveryRecord(items[0])?.valid === true;
|
|
432
|
+
if (
|
|
433
|
+
status.changeName !== change ||
|
|
434
|
+
typeof status.schemaName !== "string" ||
|
|
435
|
+
artifactPaths.length === 0
|
|
436
|
+
) {
|
|
437
|
+
throw new Error("delivery-openspec-status-invalid");
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
change,
|
|
441
|
+
schema: status.schemaName,
|
|
442
|
+
planningComplete:
|
|
443
|
+
status.isPlanningComplete === true && status.isComplete === true,
|
|
444
|
+
strictValid,
|
|
445
|
+
artifactPaths: [...new Set(artifactPaths)].sort(),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function packageDeliverySource(
|
|
450
|
+
consumerRoot: string,
|
|
451
|
+
options: PackageDeliverySourceOptions = {},
|
|
452
|
+
): WorkflowDeliverySource {
|
|
453
|
+
const inspect = options.inspectOpenSpec ?? inspectOpenSpecDelivery;
|
|
454
|
+
return {
|
|
455
|
+
async load(input) {
|
|
456
|
+
if (input.stage !== "abel-implement") {
|
|
457
|
+
throw new DeliveryValidationError(["delivery-stage-invalid"]);
|
|
458
|
+
}
|
|
459
|
+
const changeRoot = `openspec/changes/${input.change}`;
|
|
460
|
+
const receiptRelative = `${changeRoot}/ready.yaml`;
|
|
461
|
+
const planRelative = `${changeRoot}/implement-plan.json`;
|
|
462
|
+
let receiptBytes: Buffer;
|
|
463
|
+
try {
|
|
464
|
+
receiptBytes = readPackageDeliveryFile(
|
|
465
|
+
consumerRoot,
|
|
466
|
+
receiptRelative,
|
|
467
|
+
4 * 1024 * 1024,
|
|
468
|
+
);
|
|
469
|
+
} catch {
|
|
470
|
+
throw new DeliveryValidationError(["delivery-receipt-unavailable"]);
|
|
471
|
+
}
|
|
472
|
+
let receipt: ReturnType<typeof parseReadyReceipt>;
|
|
473
|
+
try {
|
|
474
|
+
receipt = parseReadyReceipt(receiptBytes);
|
|
475
|
+
} catch {
|
|
476
|
+
throw new DeliveryValidationError(["delivery-receipt-invalid"]);
|
|
477
|
+
}
|
|
478
|
+
const diagnostics = new Set<string>();
|
|
479
|
+
if (receipt.change !== input.change) {
|
|
480
|
+
diagnostics.add("delivery-change-mismatch");
|
|
481
|
+
}
|
|
482
|
+
const receiptHash = sha256(receiptBytes);
|
|
483
|
+
if (
|
|
484
|
+
(input.deliveryRevision !== undefined &&
|
|
485
|
+
input.deliveryRevision !== receipt.deliveryRevision) ||
|
|
486
|
+
(input.receiptHash !== undefined && input.receiptHash !== receiptHash)
|
|
487
|
+
) {
|
|
488
|
+
diagnostics.add("delivery-revision-mismatch");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let inspection: OpenSpecDeliveryInspection | undefined;
|
|
492
|
+
try {
|
|
493
|
+
inspection = await inspect(consumerRoot, input.change);
|
|
494
|
+
} catch {
|
|
495
|
+
diagnostics.add("delivery-openspec-unavailable");
|
|
496
|
+
}
|
|
497
|
+
if (inspection) {
|
|
498
|
+
if (!inspection.strictValid) {
|
|
499
|
+
diagnostics.add("delivery-openspec-strict-invalid");
|
|
500
|
+
}
|
|
501
|
+
if (!inspection.planningComplete) {
|
|
502
|
+
diagnostics.add("delivery-openspec-incomplete");
|
|
503
|
+
}
|
|
504
|
+
if (inspection.schema !== receipt.schema) {
|
|
505
|
+
diagnostics.add("delivery-schema-mismatch");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
let gateABytes: Buffer | undefined;
|
|
510
|
+
let gateA: ReturnType<typeof parseGateAReceipt> | undefined;
|
|
511
|
+
try {
|
|
512
|
+
gateABytes = readPackageDeliveryFile(
|
|
513
|
+
consumerRoot,
|
|
514
|
+
`${changeRoot}/${receipt.approvals.gateA.path}`,
|
|
515
|
+
4 * 1024 * 1024,
|
|
516
|
+
);
|
|
517
|
+
if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
|
|
518
|
+
diagnostics.add("delivery-gate-a-hash-mismatch");
|
|
519
|
+
}
|
|
520
|
+
gateA = parseGateAReceipt(gateABytes);
|
|
521
|
+
if (
|
|
522
|
+
gateA.change !== receipt.change ||
|
|
523
|
+
gateA.schema !== receipt.schema
|
|
524
|
+
) {
|
|
525
|
+
diagnostics.add("delivery-gate-a-binding-mismatch");
|
|
526
|
+
}
|
|
527
|
+
} catch {
|
|
528
|
+
diagnostics.add("delivery-gate-a-invalid");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const artifactBytes = new Map<string, Buffer>();
|
|
532
|
+
const artifactHashes = new Map(
|
|
533
|
+
receipt.artifacts.map((artifact) => [
|
|
534
|
+
artifact.path,
|
|
535
|
+
artifact.rawSha256,
|
|
536
|
+
]),
|
|
537
|
+
);
|
|
538
|
+
for (const artifact of receipt.artifacts) {
|
|
539
|
+
try {
|
|
540
|
+
const bytes = readPackageDeliveryFile(
|
|
541
|
+
consumerRoot,
|
|
542
|
+
`${changeRoot}/${artifact.path}`,
|
|
543
|
+
);
|
|
544
|
+
artifactBytes.set(artifact.path, bytes);
|
|
545
|
+
if (
|
|
546
|
+
artifact.path !== receipt.traceability.taskPath &&
|
|
547
|
+
sha256(bytes) !== artifact.rawSha256
|
|
548
|
+
) {
|
|
549
|
+
diagnostics.add(`delivery-artifact-hash-mismatch:${artifact.path}`);
|
|
550
|
+
}
|
|
551
|
+
} catch {
|
|
552
|
+
diagnostics.add(`delivery-artifact-unavailable:${artifact.path}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (inspection) {
|
|
556
|
+
const covered = new Set(receipt.artifacts.map((entry) => entry.path));
|
|
557
|
+
const expected = new Set(inspection.artifactPaths);
|
|
558
|
+
for (const relative of expected) {
|
|
559
|
+
if (!isValidRelativePath(relative) || !covered.has(relative)) {
|
|
560
|
+
diagnostics.add(`delivery-artifact-unbound:${relative}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
for (const relative of covered) {
|
|
564
|
+
if (!expected.has(relative)) {
|
|
565
|
+
diagnostics.add(`delivery-artifact-not-in-openspec:${relative}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (gateA) {
|
|
570
|
+
for (const artifact of gateA.artifacts) {
|
|
571
|
+
if (artifactHashes.get(artifact.path) !== artifact.rawSha256) {
|
|
572
|
+
diagnostics.add(
|
|
573
|
+
`delivery-gate-a-artifact-mismatch:${artifact.path}`,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let plan: ReturnType<typeof parseImplementPlan> | undefined;
|
|
580
|
+
let planBytes: Buffer | undefined;
|
|
581
|
+
try {
|
|
582
|
+
planBytes = readPackageDeliveryFile(consumerRoot, planRelative);
|
|
583
|
+
plan = parseImplementPlan(planBytes);
|
|
584
|
+
} catch {
|
|
585
|
+
diagnostics.add("delivery-plan-invalid");
|
|
586
|
+
}
|
|
587
|
+
if (plan && planBytes) {
|
|
588
|
+
if (plan.changeId !== input.change) {
|
|
589
|
+
diagnostics.add("delivery-plan-change-mismatch");
|
|
590
|
+
}
|
|
591
|
+
let compiled: ReturnType<typeof compileImplementPlan> | undefined;
|
|
592
|
+
try {
|
|
593
|
+
compiled = compileImplementPlan(plan, { consumerRoot });
|
|
594
|
+
} catch {
|
|
595
|
+
diagnostics.add("delivery-verification-closure-invalid");
|
|
596
|
+
}
|
|
597
|
+
if (
|
|
598
|
+
receipt.plan.path !== "implement-plan.json" ||
|
|
599
|
+
receipt.plan.schemaVersion !== IMPLEMENT_PLAN_SCHEMA_VERSION ||
|
|
600
|
+
receipt.plan.rawSha256 !== sha256(planBytes) ||
|
|
601
|
+
receipt.plan.canonicalHash !== hashCanonicalValue(plan)
|
|
602
|
+
) {
|
|
603
|
+
diagnostics.add("delivery-plan-binding-invalid");
|
|
604
|
+
}
|
|
605
|
+
if (
|
|
606
|
+
compiled &&
|
|
607
|
+
canonicalJson(compiled.closure) !==
|
|
608
|
+
canonicalJson(receipt.verificationClosure)
|
|
609
|
+
) {
|
|
610
|
+
diagnostics.add("delivery-verification-closure-mismatch");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (plan) {
|
|
615
|
+
const tasksBytes = artifactBytes.get(receipt.traceability.taskPath);
|
|
616
|
+
const expectedTasksHash = artifactHashes.get(
|
|
617
|
+
receipt.traceability.taskPath,
|
|
618
|
+
);
|
|
619
|
+
if (
|
|
620
|
+
tasksBytes &&
|
|
621
|
+
expectedTasksHash &&
|
|
622
|
+
sha256(tasksBytes) !== expectedTasksHash
|
|
623
|
+
) {
|
|
624
|
+
const normalized = normalizedTrackingArtifact(
|
|
625
|
+
tasksBytes,
|
|
626
|
+
plan.tracking.taskIds,
|
|
627
|
+
);
|
|
628
|
+
if (!normalized || sha256(normalized) !== expectedTasksHash) {
|
|
629
|
+
diagnostics.add(
|
|
630
|
+
`delivery-artifact-hash-mismatch:${receipt.traceability.taskPath}`,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const specs = [...artifactBytes]
|
|
635
|
+
.filter(
|
|
636
|
+
([relative]) =>
|
|
637
|
+
relative.startsWith("specs/") && relative.endsWith("/spec.md"),
|
|
638
|
+
)
|
|
639
|
+
.flatMap(([relative, bytes]) => {
|
|
640
|
+
try {
|
|
641
|
+
return [
|
|
642
|
+
{
|
|
643
|
+
path: relative,
|
|
644
|
+
text: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
645
|
+
},
|
|
646
|
+
];
|
|
647
|
+
} catch {
|
|
648
|
+
diagnostics.add(`delivery-artifact-encoding-invalid:${relative}`);
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
if (!tasksBytes || specs.length === 0) {
|
|
653
|
+
diagnostics.add("delivery-traceability-input-unavailable");
|
|
654
|
+
} else {
|
|
655
|
+
try {
|
|
656
|
+
const traceability = assessDeliveryTraceability({
|
|
657
|
+
tasksMarkdown: new TextDecoder("utf-8", { fatal: true }).decode(
|
|
658
|
+
tasksBytes,
|
|
659
|
+
),
|
|
660
|
+
specs,
|
|
661
|
+
plan,
|
|
662
|
+
});
|
|
663
|
+
if (!traceability.ok) {
|
|
664
|
+
for (const diagnostic of traceability.diagnostics) {
|
|
665
|
+
diagnostics.add(diagnostic);
|
|
666
|
+
}
|
|
667
|
+
} else if (
|
|
668
|
+
canonicalJson(traceability.value) !==
|
|
669
|
+
canonicalJson(receipt.traceability)
|
|
670
|
+
) {
|
|
671
|
+
diagnostics.add("delivery-traceability-binding-mismatch");
|
|
672
|
+
}
|
|
673
|
+
} catch {
|
|
674
|
+
diagnostics.add("delivery-traceability-invalid");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (!plan || diagnostics.size > 0) {
|
|
679
|
+
throw new DeliveryValidationError([...diagnostics]);
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
version: 2,
|
|
683
|
+
gate: "gate-b",
|
|
684
|
+
revision: receipt.deliveryRevision,
|
|
685
|
+
receiptHash,
|
|
686
|
+
plan,
|
|
687
|
+
};
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function packageScriptInvocation(
|
|
693
|
+
packageManager: string,
|
|
694
|
+
script: string,
|
|
695
|
+
args: string[],
|
|
696
|
+
): string[] {
|
|
697
|
+
return packageManager === "npm" || packageManager === "pnpm"
|
|
698
|
+
? ["run", script, "--", ...args]
|
|
699
|
+
: ["run", script, ...args];
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function bindingFor(
|
|
703
|
+
bindings: readonly VerificationRunnerBinding[],
|
|
704
|
+
command: string,
|
|
705
|
+
): VerificationRunnerBinding | undefined {
|
|
706
|
+
return bindings.find((binding) => binding.command === command);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function prepareRunnerBindings(
|
|
710
|
+
root: string,
|
|
711
|
+
bindings: readonly VerificationRunnerBinding[],
|
|
712
|
+
): {
|
|
713
|
+
bindings: VerificationRunnerBinding[];
|
|
714
|
+
mounts: Array<{ source: string; target: string }>;
|
|
715
|
+
cleanupRoot?: string;
|
|
716
|
+
} {
|
|
717
|
+
if (!bindings.some((binding) => binding.mountSource)) {
|
|
718
|
+
return {
|
|
719
|
+
bindings: bindings.map((binding) => ({
|
|
720
|
+
...binding,
|
|
721
|
+
...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
|
|
722
|
+
})),
|
|
723
|
+
mounts: [],
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const cleanupRoot = mkdtempSync(path.join(root, ".cadence-runners-"));
|
|
727
|
+
try {
|
|
728
|
+
const mounted = new Map<
|
|
729
|
+
string,
|
|
730
|
+
{ source: string; target: string; sandboxTarget: string }
|
|
731
|
+
>();
|
|
732
|
+
const prepared = bindings.map((binding) => {
|
|
733
|
+
if (!binding.mountSource) return { ...binding };
|
|
734
|
+
const source = path.resolve(binding.mountSource);
|
|
735
|
+
const sourceStat = lstatSync(source);
|
|
736
|
+
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
|
|
737
|
+
throw new Error("runner-mount-unavailable");
|
|
738
|
+
}
|
|
739
|
+
let mount = mounted.get(source);
|
|
740
|
+
if (!mount) {
|
|
741
|
+
const target = path.join(cleanupRoot, String(mounted.size));
|
|
742
|
+
mkdirSync(target, { mode: 0o700 });
|
|
743
|
+
const sandboxRelative = path
|
|
744
|
+
.relative(root, target)
|
|
745
|
+
.split(path.sep)
|
|
746
|
+
.join("/");
|
|
747
|
+
mount = {
|
|
748
|
+
source,
|
|
749
|
+
target,
|
|
750
|
+
sandboxTarget: `/workspace/${sandboxRelative}`,
|
|
751
|
+
};
|
|
752
|
+
mounted.set(source, mount);
|
|
753
|
+
}
|
|
754
|
+
const executable = realpathSync(binding.executablePath);
|
|
755
|
+
const relative = path.relative(source, executable);
|
|
756
|
+
if (
|
|
757
|
+
relative === "" ||
|
|
758
|
+
relative.startsWith("..") ||
|
|
759
|
+
path.isAbsolute(relative)
|
|
760
|
+
) {
|
|
761
|
+
throw new Error("runner-mount-unavailable");
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
command: binding.command,
|
|
765
|
+
executablePath: path.posix.join(
|
|
766
|
+
mount.sandboxTarget,
|
|
767
|
+
...relative.split(path.sep),
|
|
768
|
+
),
|
|
769
|
+
...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
|
|
770
|
+
};
|
|
771
|
+
});
|
|
772
|
+
return {
|
|
773
|
+
bindings: prepared,
|
|
774
|
+
mounts: [...mounted.values()].map(({ source, sandboxTarget }) => ({
|
|
775
|
+
source,
|
|
776
|
+
target: sandboxTarget,
|
|
777
|
+
})),
|
|
778
|
+
cleanupRoot,
|
|
779
|
+
};
|
|
780
|
+
} catch (error) {
|
|
781
|
+
rmSync(cleanupRoot, { recursive: true, force: true });
|
|
782
|
+
throw error;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function verificationInvocation(
|
|
787
|
+
step: AtomicVerificationContract,
|
|
788
|
+
bindings: readonly VerificationRunnerBinding[],
|
|
789
|
+
): { executable: string; args: string[] } | undefined {
|
|
790
|
+
const reporter = step.kind === "vitest" ? ["--reporter=json"] : [];
|
|
791
|
+
if (step.kind === "package-script") {
|
|
792
|
+
const binding = bindingFor(bindings, step.packageManager);
|
|
793
|
+
return binding
|
|
794
|
+
? {
|
|
795
|
+
executable: binding.executablePath,
|
|
796
|
+
args: [
|
|
797
|
+
...(binding.fixedArgs ?? []),
|
|
798
|
+
...packageScriptInvocation(
|
|
799
|
+
step.packageManager,
|
|
800
|
+
step.script,
|
|
801
|
+
step.args,
|
|
802
|
+
),
|
|
803
|
+
],
|
|
804
|
+
}
|
|
805
|
+
: undefined;
|
|
806
|
+
}
|
|
807
|
+
const runner = step.runner;
|
|
808
|
+
if (runner.kind === "package-script") {
|
|
809
|
+
const binding = bindingFor(bindings, runner.packageManager);
|
|
810
|
+
const args =
|
|
811
|
+
step.kind === "vitest"
|
|
812
|
+
? [...step.testFiles, ...step.args, ...reporter]
|
|
813
|
+
: step.args;
|
|
814
|
+
return binding
|
|
815
|
+
? {
|
|
816
|
+
executable: binding.executablePath,
|
|
817
|
+
args: [
|
|
818
|
+
...(binding.fixedArgs ?? []),
|
|
819
|
+
...packageScriptInvocation(
|
|
820
|
+
runner.packageManager,
|
|
821
|
+
runner.script,
|
|
822
|
+
args,
|
|
823
|
+
),
|
|
824
|
+
],
|
|
825
|
+
}
|
|
826
|
+
: undefined;
|
|
827
|
+
}
|
|
828
|
+
if (runner.kind === "local-binary") {
|
|
829
|
+
const binding = bindingFor(bindings, runner.executable);
|
|
830
|
+
return {
|
|
831
|
+
executable:
|
|
832
|
+
binding?.executablePath ??
|
|
833
|
+
`/workspace/node_modules/.bin/${runner.executable}`,
|
|
834
|
+
args: [
|
|
835
|
+
...(binding?.fixedArgs ?? []),
|
|
836
|
+
...(step.kind === "vitest"
|
|
837
|
+
? [...step.testFiles, ...step.args, ...reporter]
|
|
838
|
+
: step.args),
|
|
839
|
+
],
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const command = runner.kind === "node" ? "node" : "npx";
|
|
843
|
+
const binding = bindingFor(bindings, command);
|
|
844
|
+
if (!binding) return undefined;
|
|
845
|
+
return {
|
|
846
|
+
executable: binding.executablePath,
|
|
847
|
+
args:
|
|
848
|
+
runner.kind === "node"
|
|
849
|
+
? [...(binding.fixedArgs ?? []), runner.script, ...step.args]
|
|
850
|
+
: [
|
|
851
|
+
...(binding.fixedArgs ?? []),
|
|
852
|
+
"--no-install",
|
|
853
|
+
runner.executable,
|
|
854
|
+
...(step.kind === "vitest" ? step.testFiles : []),
|
|
855
|
+
...step.args,
|
|
856
|
+
...reporter,
|
|
857
|
+
],
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function vitestReport(stdout: string):
|
|
862
|
+
| {
|
|
863
|
+
total: number;
|
|
864
|
+
failed: number;
|
|
865
|
+
success: boolean;
|
|
866
|
+
failures: string[];
|
|
867
|
+
failedText: string[];
|
|
868
|
+
}
|
|
869
|
+
| undefined {
|
|
870
|
+
let value: unknown;
|
|
871
|
+
try {
|
|
872
|
+
value = JSON.parse(stdout);
|
|
873
|
+
} catch {
|
|
874
|
+
return undefined;
|
|
875
|
+
}
|
|
876
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
877
|
+
return undefined;
|
|
878
|
+
}
|
|
879
|
+
const report = value as Record<string, unknown>;
|
|
880
|
+
if (
|
|
881
|
+
!Number.isSafeInteger(report.numTotalTests) ||
|
|
882
|
+
!Number.isSafeInteger(report.numFailedTests) ||
|
|
883
|
+
typeof report.success !== "boolean"
|
|
884
|
+
) {
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
const failedAssertions = (
|
|
888
|
+
Array.isArray(report.testResults) ? report.testResults : []
|
|
889
|
+
).flatMap((suite): Array<{ identity: string; text: string[] }> => {
|
|
890
|
+
if (!suite || typeof suite !== "object" || Array.isArray(suite)) return [];
|
|
891
|
+
const record = suite as Record<string, unknown>;
|
|
892
|
+
const suiteName =
|
|
893
|
+
typeof record.name === "string"
|
|
894
|
+
? record.name.replace(/^\/workspace\//u, "")
|
|
895
|
+
: "unknown-suite";
|
|
896
|
+
if (!Array.isArray(record.assertionResults)) return [];
|
|
897
|
+
return record.assertionResults.flatMap(
|
|
898
|
+
(assertion): Array<{ identity: string; text: string[] }> => {
|
|
899
|
+
if (
|
|
900
|
+
!assertion ||
|
|
901
|
+
typeof assertion !== "object" ||
|
|
902
|
+
Array.isArray(assertion)
|
|
903
|
+
) {
|
|
904
|
+
return [];
|
|
905
|
+
}
|
|
906
|
+
const entry = assertion as Record<string, unknown>;
|
|
907
|
+
if (entry.status !== "failed") return [];
|
|
908
|
+
const title =
|
|
909
|
+
typeof entry.fullName === "string"
|
|
910
|
+
? entry.fullName
|
|
911
|
+
: typeof entry.title === "string"
|
|
912
|
+
? entry.title
|
|
913
|
+
: "unknown-assertion";
|
|
914
|
+
const diagnostics = Array.isArray(entry.failureMessages)
|
|
915
|
+
? entry.failureMessages.filter(
|
|
916
|
+
(message): message is string => typeof message === "string",
|
|
917
|
+
)
|
|
918
|
+
: [];
|
|
919
|
+
return [
|
|
920
|
+
{
|
|
921
|
+
identity: `${suiteName}\0${title}`,
|
|
922
|
+
text: [title, ...diagnostics],
|
|
923
|
+
},
|
|
924
|
+
];
|
|
925
|
+
},
|
|
926
|
+
);
|
|
927
|
+
});
|
|
928
|
+
return {
|
|
929
|
+
total: report.numTotalTests as number,
|
|
930
|
+
failed: report.numFailedTests as number,
|
|
931
|
+
success: report.success,
|
|
932
|
+
failures: failedAssertions.map((failure) => failure.identity),
|
|
933
|
+
failedText: failedAssertions.flatMap((failure) => failure.text),
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function normalizedFailureIdentities(input: {
|
|
938
|
+
step: AtomicVerificationContract;
|
|
939
|
+
report?: ReturnType<typeof vitestReport>;
|
|
940
|
+
output: string;
|
|
941
|
+
exitCode: number;
|
|
942
|
+
}): string[] {
|
|
943
|
+
const vitestFailures = input.report?.failures ?? [];
|
|
944
|
+
if (vitestFailures.length > 0) {
|
|
945
|
+
return [...new Set(vitestFailures)]
|
|
946
|
+
.sort()
|
|
947
|
+
.map((failure) => sha256(`vitest-failure-v1\0${failure}`));
|
|
948
|
+
}
|
|
949
|
+
const normalizedOutput = input.output
|
|
950
|
+
.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu"), "")
|
|
951
|
+
.replace(/\r\n?/gu, "\n")
|
|
952
|
+
.replace(/\b\d+(?:\.\d+)?(?:ms|s)\b/gu, "<duration>")
|
|
953
|
+
.slice(0, 64 * 1024);
|
|
954
|
+
return [
|
|
955
|
+
sha256(
|
|
956
|
+
canonicalJson({
|
|
957
|
+
version: 1,
|
|
958
|
+
verificationId: input.step.id,
|
|
959
|
+
exitCode: input.exitCode,
|
|
960
|
+
output: normalizedOutput,
|
|
961
|
+
}),
|
|
962
|
+
),
|
|
963
|
+
];
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export async function executePackageVerification(input: {
|
|
967
|
+
root: string;
|
|
968
|
+
dependencyOwner: string;
|
|
969
|
+
verification: StructuredVerificationContract;
|
|
970
|
+
signal: AbortSignal;
|
|
971
|
+
}) {
|
|
972
|
+
const capability = bindCurrentVerificationCapability(
|
|
973
|
+
input.root,
|
|
974
|
+
input.verification,
|
|
975
|
+
{ dependencyOwner: input.dependencyOwner },
|
|
976
|
+
);
|
|
977
|
+
if (!capability.ok) {
|
|
978
|
+
return {
|
|
979
|
+
ok: false as const,
|
|
980
|
+
kind: "paused" as const,
|
|
981
|
+
code: capability.diagnostic.code,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
let runners: ReturnType<typeof prepareRunnerBindings>;
|
|
985
|
+
try {
|
|
986
|
+
runners = prepareRunnerBindings(
|
|
987
|
+
input.root,
|
|
988
|
+
capability.value.runnerBindings,
|
|
989
|
+
);
|
|
990
|
+
} catch {
|
|
991
|
+
return {
|
|
992
|
+
ok: false as const,
|
|
993
|
+
kind: "paused" as const,
|
|
994
|
+
code: "runner-mount-unavailable",
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
try {
|
|
998
|
+
const nodeModules = path.join(input.dependencyOwner, "node_modules");
|
|
999
|
+
const dependency = lstatSync(nodeModules, { throwIfNoEntry: false });
|
|
1000
|
+
const mounts = [] as Array<{
|
|
1001
|
+
source: string;
|
|
1002
|
+
target: string;
|
|
1003
|
+
writable?: boolean;
|
|
1004
|
+
}>;
|
|
1005
|
+
if (dependency) {
|
|
1006
|
+
if (!dependency.isDirectory() || dependency.isSymbolicLink()) {
|
|
1007
|
+
return {
|
|
1008
|
+
ok: false as const,
|
|
1009
|
+
kind: "paused" as const,
|
|
1010
|
+
code: "dependency-path-unsafe",
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
const target = path.join(input.root, "node_modules");
|
|
1014
|
+
const targetStat = lstatSync(target, { throwIfNoEntry: false });
|
|
1015
|
+
if (
|
|
1016
|
+
targetStat &&
|
|
1017
|
+
(!targetStat.isDirectory() || targetStat.isSymbolicLink())
|
|
1018
|
+
) {
|
|
1019
|
+
return {
|
|
1020
|
+
ok: false as const,
|
|
1021
|
+
kind: "paused" as const,
|
|
1022
|
+
code: "dependency-path-unsafe",
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
if (!targetStat) mkdirSync(target, { mode: 0o700 });
|
|
1026
|
+
mounts.push({ source: nodeModules, target: "/workspace/node_modules" });
|
|
1027
|
+
}
|
|
1028
|
+
mounts.push(...runners.mounts);
|
|
1029
|
+
const isolation = new BubblewrapIsolationBackend();
|
|
1030
|
+
const steps = verificationSteps(input.verification);
|
|
1031
|
+
let final:
|
|
1032
|
+
| {
|
|
1033
|
+
exitCode: number;
|
|
1034
|
+
classification: AtomicVerificationContract["classification"];
|
|
1035
|
+
diagnostic: { kind: "assertion" | "compiler"; id: string };
|
|
1036
|
+
}
|
|
1037
|
+
| undefined;
|
|
1038
|
+
for (const step of steps) {
|
|
1039
|
+
const invocation = verificationInvocation(step, runners.bindings);
|
|
1040
|
+
if (!invocation) {
|
|
1041
|
+
return {
|
|
1042
|
+
ok: false as const,
|
|
1043
|
+
kind: "paused" as const,
|
|
1044
|
+
code: "runner-missing",
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
const executed = await isolation.run({
|
|
1048
|
+
root: input.root,
|
|
1049
|
+
executable: invocation.executable,
|
|
1050
|
+
args: invocation.args,
|
|
1051
|
+
mounts,
|
|
1052
|
+
environment: { CI: "1" },
|
|
1053
|
+
signal: input.signal,
|
|
1054
|
+
});
|
|
1055
|
+
if (!executed.ok) {
|
|
1056
|
+
if (executed.state === "cancelled") throw input.signal.reason;
|
|
1057
|
+
return {
|
|
1058
|
+
ok: false as const,
|
|
1059
|
+
kind: "paused" as const,
|
|
1060
|
+
code: executed.code,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
const report =
|
|
1064
|
+
step.kind === "vitest" ? vitestReport(executed.stdout) : undefined;
|
|
1065
|
+
const output = `${executed.stdout}\n${executed.stderr}`;
|
|
1066
|
+
const identity =
|
|
1067
|
+
step.classification !== "expected-red" ||
|
|
1068
|
+
(step.kind === "vitest"
|
|
1069
|
+
? report?.failedText.some((text) =>
|
|
1070
|
+
text.includes(step.expectedFailure ?? ""),
|
|
1071
|
+
) === true
|
|
1072
|
+
: output.includes(step.expectedFailure ?? ""));
|
|
1073
|
+
const accepted =
|
|
1074
|
+
step.classification === "expected-red"
|
|
1075
|
+
? executed.exitCode !== 0 &&
|
|
1076
|
+
identity &&
|
|
1077
|
+
(step.kind !== "vitest" ||
|
|
1078
|
+
(report !== undefined &&
|
|
1079
|
+
report.failed > 0 &&
|
|
1080
|
+
report.total >= step.minTests))
|
|
1081
|
+
: executed.exitCode === 0 &&
|
|
1082
|
+
(step.kind !== "vitest" ||
|
|
1083
|
+
(report?.success === true && report.total >= step.minTests));
|
|
1084
|
+
if (!accepted) {
|
|
1085
|
+
return {
|
|
1086
|
+
ok: false as const,
|
|
1087
|
+
kind: "retryable" as const,
|
|
1088
|
+
code:
|
|
1089
|
+
step.classification === "expected-red" && executed.exitCode === 0
|
|
1090
|
+
? "red-not-witnessed"
|
|
1091
|
+
: "verification-rejected",
|
|
1092
|
+
failureIdentities: normalizedFailureIdentities({
|
|
1093
|
+
step,
|
|
1094
|
+
report,
|
|
1095
|
+
output,
|
|
1096
|
+
exitCode: executed.exitCode,
|
|
1097
|
+
}),
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
if (!isVerificationCapabilityCurrent(input.root, capability.value)) {
|
|
1101
|
+
return {
|
|
1102
|
+
ok: false as const,
|
|
1103
|
+
kind: "retryable" as const,
|
|
1104
|
+
code: "verification-input-unavailable",
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
final = {
|
|
1108
|
+
exitCode: executed.exitCode,
|
|
1109
|
+
classification: step.classification,
|
|
1110
|
+
diagnostic: {
|
|
1111
|
+
kind: step.kind === "vitest" ? "assertion" : "compiler",
|
|
1112
|
+
id: step.id,
|
|
1113
|
+
},
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
return final
|
|
1117
|
+
? { ok: true as const, ...final }
|
|
1118
|
+
: {
|
|
1119
|
+
ok: false as const,
|
|
1120
|
+
kind: "paused" as const,
|
|
1121
|
+
code: "verification-contract-unsupported",
|
|
1122
|
+
};
|
|
1123
|
+
} finally {
|
|
1124
|
+
if (runners.cleanupRoot) {
|
|
1125
|
+
rmSync(runners.cleanupRoot, { recursive: true, force: true });
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function affectedVerification(
|
|
1131
|
+
task: Parameters<
|
|
1132
|
+
Parameters<typeof openDurableWorkflowEngine>[0]["proposeCandidate"]
|
|
1133
|
+
>[0]["task"],
|
|
1134
|
+
): StructuredVerificationContract {
|
|
1135
|
+
if (task.affectedVerification) {
|
|
1136
|
+
return structuredClone(task.affectedVerification);
|
|
1137
|
+
}
|
|
1138
|
+
const finalPhase = task.phases.refactor ?? task.phases.green;
|
|
1139
|
+
const verification = structuredClone(finalPhase.verification);
|
|
1140
|
+
const affected = [...new Set(task.impactClosure.affectedSuite)].sort();
|
|
1141
|
+
if (affected.length === 0) return verification;
|
|
1142
|
+
if (verification.kind === "vitest") {
|
|
1143
|
+
return { ...verification, testFiles: affected };
|
|
1144
|
+
}
|
|
1145
|
+
if (verification.kind === "steps") {
|
|
1146
|
+
return {
|
|
1147
|
+
...verification,
|
|
1148
|
+
steps: verification.steps.map((step) =>
|
|
1149
|
+
step.kind === "vitest" ? { ...step, testFiles: affected } : step,
|
|
1150
|
+
),
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
return verification;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function childRequestId(
|
|
1157
|
+
operationId: string,
|
|
1158
|
+
taskId: string,
|
|
1159
|
+
phase: string,
|
|
1160
|
+
): string {
|
|
1161
|
+
return `child-${sha256(`${operationId}\0${taskId}\0${phase}`).slice(0, 40)}`;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
export function openPackageWorkflowControlEngine(
|
|
1165
|
+
initialContext: ExtensionContext,
|
|
1166
|
+
parentPayloadBridge: ParentPayloadBridge,
|
|
1167
|
+
): WorkflowControlEngine {
|
|
1168
|
+
const consumerRoot = path.resolve(initialContext.cwd);
|
|
1169
|
+
const routeResolution = loadRoutePolicy({
|
|
1170
|
+
cwd: consumerRoot,
|
|
1171
|
+
home: homedir(),
|
|
1172
|
+
});
|
|
1173
|
+
const stateRoot = resolveStateRoot({
|
|
1174
|
+
consumerRoot,
|
|
1175
|
+
xdgStateHome: process.env.XDG_STATE_HOME,
|
|
1176
|
+
});
|
|
1177
|
+
const contexts = new AsyncLocalStorage<ExtensionContext>();
|
|
1178
|
+
const implementationAgent = loadAgentDefinitions().find(
|
|
1179
|
+
(agent) => agent.role === "implementation-worker",
|
|
1180
|
+
);
|
|
1181
|
+
if (!implementationAgent) {
|
|
1182
|
+
throw new Error("implementation-worker-agent-unavailable");
|
|
1183
|
+
}
|
|
1184
|
+
const engine = openDurableWorkflowEngine({
|
|
1185
|
+
consumerRoot,
|
|
1186
|
+
stateRoot,
|
|
1187
|
+
deliverySource: packageDeliverySource(consumerRoot),
|
|
1188
|
+
routePolicy: routeResolution.ok
|
|
1189
|
+
? routeResolution.policy
|
|
1190
|
+
: unavailableRoutePolicy(),
|
|
1191
|
+
proposeCandidate: async (input) => {
|
|
1192
|
+
const context = contexts.getStore();
|
|
1193
|
+
if (!context) {
|
|
1194
|
+
return { kind: "paused", code: "parent-context-unavailable" };
|
|
1195
|
+
}
|
|
1196
|
+
const phaseRuntime = await runtimeForWorkerRoute(
|
|
1197
|
+
input.route as WorkerRoutePolicy,
|
|
1198
|
+
context,
|
|
1199
|
+
parentPayloadBridge,
|
|
1200
|
+
input.signal,
|
|
1201
|
+
);
|
|
1202
|
+
if (!phaseRuntime.ok) {
|
|
1203
|
+
if (phaseRuntime.failure.kind === "cancelled") {
|
|
1204
|
+
return { kind: "operation-cancelled", code: "cancelled" };
|
|
1205
|
+
}
|
|
1206
|
+
throw new Error(phaseRuntime.failure.code);
|
|
1207
|
+
}
|
|
1208
|
+
const phase = input.task.phases[input.phase];
|
|
1209
|
+
if (!phase) {
|
|
1210
|
+
return { kind: "paused", code: "task-phase-unavailable" };
|
|
1211
|
+
}
|
|
1212
|
+
const phaseContract = {
|
|
1213
|
+
candidateId: input.candidateArtifact.identity.candidateId,
|
|
1214
|
+
taskId: input.taskId,
|
|
1215
|
+
phase: input.phase,
|
|
1216
|
+
readSet: [...phase.read],
|
|
1217
|
+
writeSet: [...phase.write],
|
|
1218
|
+
deleteSet: [...phase.delete],
|
|
1219
|
+
verification: structuredClone(phase.verification),
|
|
1220
|
+
agentsImpact: input.task.agents.impact,
|
|
1221
|
+
agentsTarget: input.task.agents.target ?? null,
|
|
1222
|
+
agentsManagedOnly: true,
|
|
1223
|
+
agentsWriteAllowed: false,
|
|
1224
|
+
impactClosure: structuredClone(input.task.impactClosure),
|
|
1225
|
+
...(input.repair
|
|
1226
|
+
? {
|
|
1227
|
+
repair: {
|
|
1228
|
+
attempt: input.repair.attempt,
|
|
1229
|
+
attribution: input.repair.attribution,
|
|
1230
|
+
failureIdentities: [...input.repair.failureIdentities],
|
|
1231
|
+
verification: structuredClone(input.task.repairVerification),
|
|
1232
|
+
inBoundaryOnly: true,
|
|
1233
|
+
},
|
|
1234
|
+
}
|
|
1235
|
+
: {}),
|
|
1236
|
+
};
|
|
1237
|
+
const child = await runChildSession({
|
|
1238
|
+
cwd: input.workspaceRoot,
|
|
1239
|
+
modelRuntime: phaseRuntime.modelRuntime,
|
|
1240
|
+
model: phaseRuntime.model,
|
|
1241
|
+
systemPrompt: [
|
|
1242
|
+
implementationAgent.content,
|
|
1243
|
+
input.task.objective,
|
|
1244
|
+
input.task.context.agents,
|
|
1245
|
+
input.task.context.contract,
|
|
1246
|
+
`<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
|
|
1247
|
+
].join("\n\n"),
|
|
1248
|
+
requestId: childRequestId(input.operationId, input.taskId, input.phase),
|
|
1249
|
+
taskId: input.taskId,
|
|
1250
|
+
role: "implementation-worker",
|
|
1251
|
+
phase: input.phase,
|
|
1252
|
+
output: "diff",
|
|
1253
|
+
roots: input.task.roots.map((root) =>
|
|
1254
|
+
path.resolve(input.workspaceRoot, root),
|
|
1255
|
+
),
|
|
1256
|
+
allowedPaths: [
|
|
1257
|
+
...new Set([...phase.read, ...phase.write, ...phase.delete]),
|
|
1258
|
+
],
|
|
1259
|
+
timeoutMs: LIMITS.phaseTimeoutMs,
|
|
1260
|
+
signal: input.signal,
|
|
1261
|
+
failureOverride: phaseRuntime.failureOverride,
|
|
1262
|
+
ledgerProjection: input.ledgerProjection,
|
|
1263
|
+
candidateArtifact: input.candidateArtifact,
|
|
1264
|
+
onStreamStart: input.onHeaders,
|
|
1265
|
+
onStreamProgress: input.onProgress,
|
|
1266
|
+
});
|
|
1267
|
+
if (!child.ok) {
|
|
1268
|
+
if (child.failure.kind === "transport") {
|
|
1269
|
+
throw new Error(child.failure.code);
|
|
1270
|
+
}
|
|
1271
|
+
if (child.failure.kind === "cancelled") {
|
|
1272
|
+
return { kind: "operation-cancelled", code: "cancelled" };
|
|
1273
|
+
}
|
|
1274
|
+
if (child.failure.kind === "approval-boundary") {
|
|
1275
|
+
return { kind: "approval-needed", code: child.failure.code };
|
|
1276
|
+
}
|
|
1277
|
+
if (child.failure.kind === "result-limit") {
|
|
1278
|
+
return { kind: "paused", code: "needs-task-split" };
|
|
1279
|
+
}
|
|
1280
|
+
return child.failure.kind === "environment" ||
|
|
1281
|
+
child.failure.kind === "verification-adapter"
|
|
1282
|
+
? { kind: "paused", code: child.failure.code }
|
|
1283
|
+
: { kind: "retryable", code: child.failure.code };
|
|
1284
|
+
}
|
|
1285
|
+
const result = child.result;
|
|
1286
|
+
if (result.kind === "context-request") {
|
|
1287
|
+
return classifyCandidateContextRequest(result, [
|
|
1288
|
+
...phase.read,
|
|
1289
|
+
...phase.write,
|
|
1290
|
+
...phase.delete,
|
|
1291
|
+
]);
|
|
1292
|
+
}
|
|
1293
|
+
if (result.kind !== "sealed-candidate") {
|
|
1294
|
+
return { kind: "retryable", code: "candidate-diff-invalid" };
|
|
1295
|
+
}
|
|
1296
|
+
return {
|
|
1297
|
+
kind: "sealed-candidate",
|
|
1298
|
+
candidateId: result.candidateId,
|
|
1299
|
+
artifactHash: result.artifactHash,
|
|
1300
|
+
bytes: result.bytes,
|
|
1301
|
+
paths: [...result.paths],
|
|
1302
|
+
};
|
|
1303
|
+
},
|
|
1304
|
+
verifyPhase: (input) =>
|
|
1305
|
+
executePackageVerification({
|
|
1306
|
+
root: input.root,
|
|
1307
|
+
dependencyOwner: consumerRoot,
|
|
1308
|
+
verification: input.verification,
|
|
1309
|
+
signal: input.signal,
|
|
1310
|
+
}),
|
|
1311
|
+
verifyChange: async (input) => {
|
|
1312
|
+
const verifications = input.verification
|
|
1313
|
+
? [input.verification]
|
|
1314
|
+
: input.plan.tasks.map((task) => affectedVerification(task));
|
|
1315
|
+
for (const verification of verifications) {
|
|
1316
|
+
const result = await executePackageVerification({
|
|
1317
|
+
root: input.root,
|
|
1318
|
+
dependencyOwner: consumerRoot,
|
|
1319
|
+
verification,
|
|
1320
|
+
signal: input.signal,
|
|
1321
|
+
});
|
|
1322
|
+
if (!result.ok) {
|
|
1323
|
+
const adapter = new Set([
|
|
1324
|
+
"runner-missing",
|
|
1325
|
+
"script-missing",
|
|
1326
|
+
"script-command-mismatch",
|
|
1327
|
+
"local-executable-missing",
|
|
1328
|
+
]).has(result.code);
|
|
1329
|
+
return {
|
|
1330
|
+
ok: false,
|
|
1331
|
+
kind: adapter
|
|
1332
|
+
? ("verification-adapter" as const)
|
|
1333
|
+
: result.kind === "paused"
|
|
1334
|
+
? ("environment" as const)
|
|
1335
|
+
: ("verification" as const),
|
|
1336
|
+
code: result.code,
|
|
1337
|
+
...(result.failureIdentities
|
|
1338
|
+
? { failureIdentities: result.failureIdentities }
|
|
1339
|
+
: {}),
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return {
|
|
1344
|
+
ok: true,
|
|
1345
|
+
exitCode: 0,
|
|
1346
|
+
classification: "expected-green",
|
|
1347
|
+
};
|
|
1348
|
+
},
|
|
1349
|
+
});
|
|
1350
|
+
return {
|
|
1351
|
+
async execute(
|
|
1352
|
+
command: unknown,
|
|
1353
|
+
context = initialContext,
|
|
1354
|
+
signal,
|
|
1355
|
+
onActivity,
|
|
1356
|
+
) {
|
|
1357
|
+
const operationRouteResolution = loadRoutePolicy({
|
|
1358
|
+
cwd: consumerRoot,
|
|
1359
|
+
home: homedir(),
|
|
1360
|
+
});
|
|
1361
|
+
engine.updateRoutePolicy(
|
|
1362
|
+
operationRouteResolution.ok
|
|
1363
|
+
? operationRouteResolution.policy
|
|
1364
|
+
: unavailableRoutePolicy(),
|
|
1365
|
+
);
|
|
1366
|
+
const validation = validateControlCommand(command);
|
|
1367
|
+
if (!validation.ok) {
|
|
1368
|
+
const error = new Error(validation.code);
|
|
1369
|
+
error.name = "ControlCommandError";
|
|
1370
|
+
throw error;
|
|
1371
|
+
}
|
|
1372
|
+
return contexts.run(context, async () => {
|
|
1373
|
+
const outcome = await engine.execute(
|
|
1374
|
+
validation.value,
|
|
1375
|
+
signal,
|
|
1376
|
+
onActivity,
|
|
1377
|
+
);
|
|
1378
|
+
const routePolicy = visibleRoutePolicyStatus(
|
|
1379
|
+
operationRouteResolution,
|
|
1380
|
+
engine.routePolicyStatus(),
|
|
1381
|
+
);
|
|
1382
|
+
return { ...outcome, routePolicy };
|
|
1383
|
+
});
|
|
1384
|
+
},
|
|
1385
|
+
async close() {
|
|
1386
|
+
contexts.disable();
|
|
1387
|
+
await engine.close();
|
|
1388
|
+
},
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function visibleRoutePolicyStatus(
|
|
1393
|
+
resolution: RoutePolicyResolution,
|
|
1394
|
+
brokerStatus: Record<string, unknown> | undefined,
|
|
1395
|
+
): Record<string, unknown> {
|
|
1396
|
+
if (!resolution.ok) return inspectRoutePolicy(resolution);
|
|
1397
|
+
const inspected = brokerStatus ?? inspectRoutePolicy(resolution);
|
|
1398
|
+
return {
|
|
1399
|
+
...inspected,
|
|
1400
|
+
source: { kind: resolution.source.kind },
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
export function registerWorkflowControl(
|
|
1405
|
+
pi: ExtensionAPI,
|
|
1406
|
+
engineFactory: WorkflowControlEngineFactory = openPackageWorkflowControlEngine,
|
|
1407
|
+
): void {
|
|
222
1408
|
const parentPayloadBridge = new ParentPayloadBridge();
|
|
223
|
-
const runtime = new Runtime({ parentPayloadBridge });
|
|
224
1409
|
const activity = new ActivityController();
|
|
1410
|
+
const engines = new Map<string, Promise<WorkflowControlEngine>>();
|
|
1411
|
+
let pendingPrompt: EligiblePrompt | undefined;
|
|
1412
|
+
let activePrompt: EligiblePrompt | undefined;
|
|
1413
|
+
const activation = new (class implements Activation {
|
|
1414
|
+
state: Activation["state"] = "inactive";
|
|
225
1415
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
1416
|
+
isActive(): boolean {
|
|
1417
|
+
return this.state === "active";
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
request(): boolean {
|
|
1421
|
+
if (this.state !== "inactive") return false;
|
|
1422
|
+
this.state = "pending";
|
|
1423
|
+
return true;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
activate(): boolean {
|
|
1427
|
+
if (this.state !== "pending") return false;
|
|
1428
|
+
this.state = "active";
|
|
1429
|
+
return true;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
drain(): boolean {
|
|
1433
|
+
if (this.state === "inactive" || this.state === "pending") return false;
|
|
1434
|
+
this.state = "inactive";
|
|
1435
|
+
return true;
|
|
1436
|
+
}
|
|
1437
|
+
})();
|
|
1438
|
+
const packetRuntime = new PacketRuntime({ activation, parentPayloadBridge });
|
|
1439
|
+
|
|
1440
|
+
const engineFor = (ctx: ExtensionContext) => {
|
|
1441
|
+
const key = ctx.cwd;
|
|
1442
|
+
const existing = engines.get(key);
|
|
1443
|
+
if (existing) return existing;
|
|
1444
|
+
const opened = Promise.resolve(engineFactory(ctx, parentPayloadBridge));
|
|
1445
|
+
engines.set(key, opened);
|
|
1446
|
+
void opened.catch(() => {
|
|
1447
|
+
if (engines.get(key) === opened) engines.delete(key);
|
|
1448
|
+
});
|
|
1449
|
+
return opened;
|
|
1450
|
+
};
|
|
1451
|
+
const closeEngines = async () => {
|
|
1452
|
+
const current = [...engines.values()];
|
|
1453
|
+
engines.clear();
|
|
1454
|
+
const opened = await Promise.allSettled(current);
|
|
1455
|
+
await Promise.allSettled(
|
|
1456
|
+
opened.flatMap((result) =>
|
|
1457
|
+
result.status === "fulfilled"
|
|
1458
|
+
? [Promise.resolve(result.value.close())]
|
|
1459
|
+
: [],
|
|
1460
|
+
),
|
|
1461
|
+
);
|
|
1462
|
+
};
|
|
1463
|
+
const deactivate = () => {
|
|
1464
|
+
activation.drain();
|
|
1465
|
+
const active = pi.getActiveTools();
|
|
1466
|
+
if (active.includes(DISPATCH_TOOL)) {
|
|
1467
|
+
pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
pi.registerTool({
|
|
1472
|
+
name: DISPATCH_TOOL,
|
|
1473
|
+
label: "Abel Control",
|
|
1474
|
+
description:
|
|
1475
|
+
"Private stage-bound Abel workflow control. Implement accepts durable change commands; Design and Diagnose accept bounded packet commands.",
|
|
1476
|
+
executionMode: "parallel",
|
|
1477
|
+
parameters: {
|
|
1478
|
+
type: "object",
|
|
1479
|
+
properties: {
|
|
1480
|
+
version: { type: "integer", enum: [2] },
|
|
1481
|
+
command: {
|
|
1482
|
+
type: "string",
|
|
1483
|
+
enum: ["start", "status", "resume", "rebind", "cancel", "discard"],
|
|
1484
|
+
},
|
|
1485
|
+
stage: {
|
|
1486
|
+
type: "string",
|
|
1487
|
+
enum: ["abel-design", "abel-implement"],
|
|
1488
|
+
},
|
|
1489
|
+
change: { type: "string" },
|
|
1490
|
+
provisionalKey: { type: "string" },
|
|
1491
|
+
operationId: { type: "string" },
|
|
1492
|
+
deliveryRevision: { type: "integer", minimum: 1 },
|
|
1493
|
+
receiptHash: { type: "string" },
|
|
1494
|
+
routeId: { type: "string" },
|
|
1495
|
+
action: { type: "string", enum: [...PACKET_ACTIONS] },
|
|
1496
|
+
request: {
|
|
1497
|
+
anyOf: [DESIGN_REQUEST_SCHEMA, GENERIC_REQUEST_SCHEMA],
|
|
258
1498
|
},
|
|
259
|
-
required: ["action"],
|
|
260
1499
|
},
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
rejection?: unknown;
|
|
269
|
-
agentsCheckpoint?: unknown;
|
|
1500
|
+
additionalProperties: false,
|
|
1501
|
+
oneOf: [
|
|
1502
|
+
{
|
|
1503
|
+
required: ["version", "command", "stage"],
|
|
1504
|
+
not: {
|
|
1505
|
+
anyOf: [{ required: ["action"] }, { required: ["request"] }],
|
|
1506
|
+
},
|
|
270
1507
|
},
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
1508
|
+
{
|
|
1509
|
+
required: ["action"],
|
|
1510
|
+
not: {
|
|
1511
|
+
anyOf: [
|
|
1512
|
+
{ required: ["version"] },
|
|
1513
|
+
{ required: ["command"] },
|
|
1514
|
+
{ required: ["stage"] },
|
|
1515
|
+
{ required: ["change"] },
|
|
1516
|
+
{ required: ["provisionalKey"] },
|
|
1517
|
+
{ required: ["operationId"] },
|
|
1518
|
+
{ required: ["deliveryRevision"] },
|
|
1519
|
+
{ required: ["receiptHash"] },
|
|
1520
|
+
{ required: ["routeId"] },
|
|
1521
|
+
],
|
|
1522
|
+
},
|
|
1523
|
+
},
|
|
1524
|
+
],
|
|
1525
|
+
},
|
|
1526
|
+
async execute(
|
|
1527
|
+
toolCallId: string,
|
|
1528
|
+
params: unknown,
|
|
1529
|
+
signal: AbortSignal | undefined,
|
|
1530
|
+
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
1531
|
+
ctx: ExtensionContext,
|
|
1532
|
+
) {
|
|
1533
|
+
const record =
|
|
1534
|
+
params && typeof params === "object" && !Array.isArray(params)
|
|
1535
|
+
? (params as Record<string, unknown>)
|
|
1536
|
+
: undefined;
|
|
1537
|
+
if (record && typeof record.action === "string") {
|
|
1538
|
+
if (
|
|
1539
|
+
Object.keys(record).some(
|
|
1540
|
+
(key) => key !== "action" && key !== "request",
|
|
1541
|
+
)
|
|
1542
|
+
) {
|
|
1543
|
+
throw new Error("control-envelope-ambiguous");
|
|
1544
|
+
}
|
|
1545
|
+
if (
|
|
1546
|
+
activePrompt !== "abel-design" &&
|
|
1547
|
+
activePrompt !== "abel-diagnose"
|
|
1548
|
+
) {
|
|
1549
|
+
throw new Error("stage-control-mismatch");
|
|
1550
|
+
}
|
|
1551
|
+
if (record.action === "run") {
|
|
1552
|
+
const packet = packetRuntime.validateRequest(record.request);
|
|
1553
|
+
if (!packet.ok || packet.value.stage !== activePrompt) {
|
|
1554
|
+
throw new Error(
|
|
1555
|
+
packet.ok ? "stage-control-mismatch" : packet.reason,
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
const operation = {
|
|
1560
|
+
request: record.request,
|
|
1561
|
+
};
|
|
1562
|
+
const tuiRun = ctx.mode === "tui" && record.action === "run";
|
|
1563
|
+
const payload = tuiRun
|
|
1564
|
+
? await packetRuntime.execute(
|
|
1565
|
+
record.action,
|
|
283
1566
|
operation,
|
|
284
1567
|
ctx,
|
|
285
1568
|
signal,
|
|
@@ -288,65 +1571,123 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
288
1571
|
onUpdate as ((result: unknown) => void) | undefined,
|
|
289
1572
|
),
|
|
290
1573
|
)
|
|
291
|
-
: await
|
|
1574
|
+
: await packetRuntime.execute(record.action, operation, ctx, signal);
|
|
292
1575
|
const display = tuiRun
|
|
293
|
-
? activity.finalize(toolCallId,
|
|
1576
|
+
? activity.finalize(toolCallId, payload)
|
|
294
1577
|
: undefined;
|
|
295
|
-
const { payload, usage } = splitUsage(
|
|
1578
|
+
const { payload: publicPayload, usage } = splitUsage(payload);
|
|
296
1579
|
const details = display
|
|
297
1580
|
? {
|
|
298
|
-
...(
|
|
1581
|
+
...(publicPayload as Record<string, unknown>),
|
|
299
1582
|
[ACTIVITY_DETAILS_KEY]: display,
|
|
300
1583
|
}
|
|
301
|
-
:
|
|
1584
|
+
: publicPayload;
|
|
302
1585
|
return {
|
|
303
|
-
content: [
|
|
1586
|
+
content: [
|
|
1587
|
+
{ type: "text" as const, text: JSON.stringify(publicPayload) },
|
|
1588
|
+
],
|
|
304
1589
|
details,
|
|
305
1590
|
...(usage === undefined ? {} : { usage }),
|
|
306
1591
|
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
theme: Theme,
|
|
319
|
-
context: unknown,
|
|
1592
|
+
}
|
|
1593
|
+
const validation = validateControlCommand(params);
|
|
1594
|
+
if (!validation.ok) {
|
|
1595
|
+
const error = new Error(validation.code);
|
|
1596
|
+
error.name = "ControlCommandError";
|
|
1597
|
+
throw error;
|
|
1598
|
+
}
|
|
1599
|
+
if (
|
|
1600
|
+
activePrompt !== undefined &&
|
|
1601
|
+
(activePrompt === "abel-diagnose" ||
|
|
1602
|
+
validation.value.stage !== activePrompt)
|
|
320
1603
|
) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
1604
|
+
throw new Error("stage-control-mismatch");
|
|
1605
|
+
}
|
|
1606
|
+
const engine = await engineFor(ctx);
|
|
1607
|
+
const tuiControl = ctx.mode === "tui";
|
|
1608
|
+
if (tuiControl) {
|
|
1609
|
+
try {
|
|
1610
|
+
activity.beginWorkflow(
|
|
1611
|
+
toolCallId,
|
|
1612
|
+
validation.value,
|
|
1613
|
+
onUpdate as ((result: unknown) => void) | undefined,
|
|
1614
|
+
);
|
|
1615
|
+
} catch {
|
|
1616
|
+
// Presentation state must never alter workflow execution.
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
let payload: Record<string, unknown>;
|
|
1620
|
+
try {
|
|
1621
|
+
payload = await engine.execute(
|
|
1622
|
+
validation.value,
|
|
1623
|
+
ctx,
|
|
1624
|
+
signal,
|
|
1625
|
+
tuiControl
|
|
1626
|
+
? (event) => {
|
|
1627
|
+
try {
|
|
1628
|
+
activity.updateWorkflow(toolCallId, validation.value, event);
|
|
1629
|
+
} catch {
|
|
1630
|
+
// Presentation state must never alter workflow execution.
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
: undefined,
|
|
326
1634
|
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
if (tuiControl) activity.failWorkflow(toolCallId);
|
|
1637
|
+
throw error;
|
|
1638
|
+
}
|
|
1639
|
+
let display: ReturnType<typeof activity.finalizeWorkflow>;
|
|
1640
|
+
if (tuiControl) {
|
|
1641
|
+
try {
|
|
1642
|
+
display = activity.finalizeWorkflow(
|
|
1643
|
+
toolCallId,
|
|
1644
|
+
validation.value,
|
|
1645
|
+
payload,
|
|
1646
|
+
);
|
|
1647
|
+
} catch {
|
|
1648
|
+
activity.failWorkflow(toolCallId);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return {
|
|
1652
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload) }],
|
|
1653
|
+
details: display
|
|
1654
|
+
? { ...payload, [ACTIVITY_DETAILS_KEY]: display }
|
|
1655
|
+
: payload,
|
|
1656
|
+
};
|
|
1657
|
+
},
|
|
1658
|
+
renderCall(args: unknown, theme: Theme, context: unknown) {
|
|
1659
|
+
return renderActivityCall(
|
|
1660
|
+
args,
|
|
1661
|
+
theme,
|
|
1662
|
+
context as Parameters<typeof renderActivityCall>[2],
|
|
1663
|
+
);
|
|
1664
|
+
},
|
|
1665
|
+
renderResult(
|
|
1666
|
+
result: AgentToolResult<unknown>,
|
|
1667
|
+
options: ToolRenderResultOptions,
|
|
1668
|
+
theme: Theme,
|
|
1669
|
+
context: unknown,
|
|
1670
|
+
) {
|
|
1671
|
+
return renderActivityResult(
|
|
1672
|
+
result,
|
|
1673
|
+
options,
|
|
1674
|
+
theme,
|
|
1675
|
+
context as Parameters<typeof renderActivityResult>[3],
|
|
1676
|
+
);
|
|
1677
|
+
},
|
|
1678
|
+
} as never);
|
|
335
1679
|
|
|
336
1680
|
pi.on("input", (event) => {
|
|
337
1681
|
pendingPrompt = invokedPrompt(event.text);
|
|
338
1682
|
return { action: "continue" };
|
|
339
1683
|
});
|
|
340
|
-
|
|
341
1684
|
pi.on("before_agent_start", (event, ctx) => {
|
|
342
1685
|
const prompt = pendingPrompt;
|
|
343
1686
|
pendingPrompt = undefined;
|
|
344
1687
|
const verified =
|
|
345
|
-
prompt &&
|
|
346
|
-
isVerifiedStageInvocation(pi, runtime.activation, prompt, event.prompt);
|
|
1688
|
+
prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
|
|
347
1689
|
if (verified) {
|
|
348
1690
|
activePrompt = prompt;
|
|
349
|
-
registerDispatchTool(prompt);
|
|
350
1691
|
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
351
1692
|
if (typeof sessionId === "string") {
|
|
352
1693
|
parentPayloadBridge.beginSession(sessionId);
|
|
@@ -357,10 +1698,18 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
357
1698
|
if (ctx.model) {
|
|
358
1699
|
parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
|
|
359
1700
|
}
|
|
360
|
-
if (prompt)
|
|
361
|
-
|
|
1701
|
+
if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
|
|
1702
|
+
});
|
|
1703
|
+
pi.on("model_select", (event, ctx) => {
|
|
1704
|
+
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
1705
|
+
if (typeof sessionId !== "string") {
|
|
1706
|
+
parentPayloadBridge.clear();
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
parentPayloadBridge.beginSession(sessionId);
|
|
1710
|
+
const model = event.model ?? ctx.model;
|
|
1711
|
+
if (model) parentPayloadBridge.install(model, ctx.modelRegistry);
|
|
362
1712
|
});
|
|
363
|
-
|
|
364
1713
|
pi.on("before_provider_request", (event, ctx) => {
|
|
365
1714
|
if (
|
|
366
1715
|
activePrompt !== "abel-design" ||
|
|
@@ -373,50 +1722,33 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
373
1722
|
}
|
|
374
1723
|
return { ...event.payload, parallel_tool_calls: true };
|
|
375
1724
|
});
|
|
376
|
-
|
|
377
1725
|
pi.on("session_start", async (_event, ctx) => {
|
|
378
1726
|
pendingPrompt = undefined;
|
|
379
1727
|
activePrompt = undefined;
|
|
380
1728
|
activity.detach();
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
await runtime.drain();
|
|
386
|
-
activity.clear();
|
|
1729
|
+
await packetRuntime.drain();
|
|
1730
|
+
await closeEngines();
|
|
1731
|
+
deactivate();
|
|
387
1732
|
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
388
1733
|
if (typeof sessionId === "string") {
|
|
389
1734
|
parentPayloadBridge.beginSession(sessionId);
|
|
390
|
-
if (ctx.model)
|
|
391
|
-
parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
|
|
392
|
-
}
|
|
1735
|
+
if (ctx.model) parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
|
|
393
1736
|
} else {
|
|
394
1737
|
parentPayloadBridge.clear();
|
|
395
1738
|
}
|
|
396
1739
|
if (ctx.mode === "tui") activity.attach(ctx.ui);
|
|
397
1740
|
});
|
|
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
1741
|
pi.on("session_shutdown", async () => {
|
|
413
1742
|
activePrompt = undefined;
|
|
414
1743
|
activity.detach();
|
|
415
|
-
await
|
|
1744
|
+
await packetRuntime.drain();
|
|
1745
|
+
await closeEngines();
|
|
416
1746
|
activity.clear();
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
|
|
420
|
-
}
|
|
1747
|
+
deactivate();
|
|
1748
|
+
parentPayloadBridge.clear();
|
|
421
1749
|
});
|
|
422
1750
|
}
|
|
1751
|
+
|
|
1752
|
+
export default function register(pi: ExtensionAPI): void {
|
|
1753
|
+
registerWorkflowControl(pi);
|
|
1754
|
+
}
|