@abelxiaoxing/cadence 1.4.0 → 1.5.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.
Files changed (66) hide show
  1. package/README.md +130 -8
  2. package/agents/design-explorer.md +17 -11
  3. package/agents/diagnosis-worker.md +9 -1
  4. package/agents/implementation-worker.md +3 -1
  5. package/config/plan-draft.example.json +122 -0
  6. package/config/plan-draft.multiple-tasks.example.json +345 -0
  7. package/config/plan-draft.quick.example.json +78 -0
  8. package/package.json +10 -4
  9. package/prompts/abel-design.md +101 -44
  10. package/prompts/abel-implement.md +9 -0
  11. package/src/activity-contracts.ts +82 -0
  12. package/src/candidate-artifact.ts +143 -0
  13. package/src/candidate-patch.ts +31 -11
  14. package/src/candidate-workspace.ts +74 -0
  15. package/src/change-contract-schema.ts +191 -0
  16. package/src/change-contract.ts +5 -0
  17. package/src/change-verification.ts +920 -0
  18. package/src/child-budget.ts +58 -0
  19. package/src/child-model.ts +142 -0
  20. package/src/child-session.ts +289 -326
  21. package/src/contracts.ts +200 -76
  22. package/src/delivery-compiler.ts +105 -120
  23. package/src/delivery-revision.ts +42 -0
  24. package/src/design-control.ts +113 -4
  25. package/src/design-diagnostics.ts +150 -0
  26. package/src/durable-contracts.ts +213 -0
  27. package/src/durable-workflow.ts +148 -3570
  28. package/src/evidence-draft.ts +122 -0
  29. package/src/execution-profile.ts +38 -0
  30. package/src/implement-graph.ts +3 -0
  31. package/src/implement-plan.ts +70 -0
  32. package/src/index.ts +59 -522
  33. package/src/isolation-backend.ts +62 -40
  34. package/src/model-source.ts +22 -0
  35. package/src/operator-cli-entry.mjs +24 -0
  36. package/src/operator-cli.mjs +1421 -0
  37. package/src/operator-tools.ts +249 -0
  38. package/src/package-candidate.ts +227 -0
  39. package/src/package-verification.ts +86 -8
  40. package/src/package-workflow.ts +291 -0
  41. package/src/packet-runtime.ts +27 -48
  42. package/src/parent-provider.ts +86 -198
  43. package/src/phase-execution.ts +2248 -0
  44. package/src/pi-adapter.ts +19 -0
  45. package/src/plan-draft-summary.ts +149 -0
  46. package/src/plan-draft.ts +493 -0
  47. package/src/single-task-draft.ts +142 -0
  48. package/src/subagent-activity.ts +32 -66
  49. package/src/submit-tool.ts +20 -59
  50. package/src/transport-budget.ts +52 -0
  51. package/src/verification-capability.ts +5 -3
  52. package/src/verification-diagnostics.ts +76 -0
  53. package/src/verification-environment-io.ts +280 -0
  54. package/src/verification-environment.ts +56 -154
  55. package/src/verification-scan.ts +64 -0
  56. package/src/worker-broker.ts +39 -57
  57. package/src/workflow-policy.ts +14 -53
  58. package/src/workflow-recovery-policy.ts +95 -0
  59. package/src/workflow-scheduling.ts +105 -0
  60. package/src/workflow-state-machine.ts +105 -497
  61. package/src/workflow-status.ts +384 -0
  62. package/src/workspace-io-entry.mjs +19 -1
  63. package/src/workspace-io-worker.mjs +275 -50
  64. package/src/workspace-io.ts +6 -1
  65. package/src/empty-resource-loader.ts +0 -38
  66. package/src/parent-payload-bridge.ts +0 -523
@@ -0,0 +1,920 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { verifyCumulativeRevision } from "./apply-transaction.ts";
5
+ import type { ArtifactStore } from "./artifact-store.ts";
6
+ import type { StructuredVerificationContract } from "./contracts.ts";
7
+ import type { PlanTaskDraft } from "./delivery-compiler.ts";
8
+ import type {
9
+ DurableAffectedResult,
10
+ DurableBaselineResult,
11
+ DurableChangeVerificationResult,
12
+ DurableExecutionResources,
13
+ DurableObservedVerification,
14
+ DurablePhaseVerificationResult,
15
+ DurableResourceInput,
16
+ DurableVerificationBaseline,
17
+ DurableVerificationObservation,
18
+ DurableWorkflowEngineOptions,
19
+ } from "./durable-contracts.ts";
20
+
21
+ import { observeSafePath } from "./safe-path.ts";
22
+
23
+ import type { TaskLedger } from "./task-ledger.ts";
24
+ import {
25
+ type DurableVerificationScope,
26
+ hash,
27
+ hasVerificationLifecycle,
28
+ IDENTIFIER,
29
+ isRecord,
30
+ SHA256,
31
+ type WorkflowAttemptOutcome,
32
+ type WorkflowChangeVerifier,
33
+ } from "./workflow-policy.ts";
34
+ import type { WorkspaceRevision } from "./workspace-store.ts";
35
+ export function sha256Bytes(bytes: Uint8Array): string {
36
+ return createHash("sha256").update(bytes).digest("hex");
37
+ }
38
+ function verificationIdentities(
39
+ result: DurableChangeVerificationResult,
40
+ verification: StructuredVerificationContract,
41
+ scope: DurableVerificationScope,
42
+ ): string[] {
43
+ // Keep the complete comparison set; only Worker/status summaries are bounded.
44
+ const supplied = result.failureIdentities;
45
+ if (
46
+ Array.isArray(supplied) &&
47
+ supplied.every(
48
+ (identity) => typeof identity === "string" && SHA256.test(identity),
49
+ )
50
+ ) {
51
+ return [...new Set(supplied)].sort();
52
+ }
53
+ if (result.ok) return [];
54
+ return [hash("verification-failure", scope, verification.id, result.code)];
55
+ }
56
+ export function verificationBaselineFact(
57
+ baseline: DurableVerificationBaseline,
58
+ artifacts: ArtifactStore,
59
+ ) {
60
+ // Complete private evidence must not inherit the bounded model projection limits.
61
+ const artifact = artifacts.put(Buffer.from(JSON.stringify(baseline)));
62
+ return {
63
+ revisionId: baseline.revisionId,
64
+ baselineArtifactHash: artifact.hash,
65
+ };
66
+ }
67
+ export function parseVerificationBaseline(
68
+ value: unknown,
69
+ artifacts: ArtifactStore,
70
+ ): DurableVerificationBaseline {
71
+ if (isRecord(value) && typeof value.baselineArtifactHash === "string") {
72
+ const revisionId = value.revisionId;
73
+ value = JSON.parse(
74
+ Buffer.from(artifacts.read(value.baselineArtifactHash)).toString("utf8"),
75
+ );
76
+ if (!isRecord(value) || value.revisionId !== revisionId)
77
+ throw new Error("workflow-verification-baseline-conflict");
78
+ }
79
+ if (
80
+ !isRecord(value) ||
81
+ typeof value.revisionId !== "string" ||
82
+ !SHA256.test(value.revisionId) ||
83
+ !Array.isArray(value.targetContracts) ||
84
+ !Array.isArray(value.affected) ||
85
+ !isRecord(value.fullSuite)
86
+ ) {
87
+ throw new Error("workflow-verification-baseline-invalid");
88
+ }
89
+ const baseline = structuredClone(
90
+ value,
91
+ ) as unknown as DurableVerificationBaseline;
92
+ const validObservation = (observation: DurableVerificationObservation) =>
93
+ (observation.status === "passed" || observation.status === "failed") &&
94
+ typeof observation.verificationId === "string" &&
95
+ IDENTIFIER.test(observation.verificationId) &&
96
+ Array.isArray(observation.failureIdentities) &&
97
+ observation.failureIdentities.every((identity) => SHA256.test(identity)) &&
98
+ (observation.code === undefined || typeof observation.code === "string") &&
99
+ (observation.attributionReliable === undefined ||
100
+ typeof observation.attributionReliable === "boolean");
101
+ if (
102
+ !baseline.targetContracts.every(
103
+ (entry) =>
104
+ isRecord(entry) &&
105
+ typeof entry.taskId === "string" &&
106
+ typeof entry.verificationId === "string" &&
107
+ typeof entry.expectedFailure === "string",
108
+ ) ||
109
+ !baseline.affected.every(
110
+ (entry) =>
111
+ isRecord(entry) &&
112
+ typeof entry.taskId === "string" &&
113
+ isRecord(entry.observation) &&
114
+ validObservation(entry.observation),
115
+ ) ||
116
+ !validObservation(baseline.fullSuite)
117
+ ) {
118
+ throw new Error("workflow-verification-baseline-invalid");
119
+ }
120
+ return baseline;
121
+ }
122
+
123
+ export interface ChangeVerificationServices {
124
+ prepareResources(
125
+ input: DurableResourceInput,
126
+ signal: AbortSignal,
127
+ ): Promise<DurableExecutionResources>;
128
+ ledger(
129
+ resources: DurableExecutionResources,
130
+ deliveryRevision: number,
131
+ ): TaskLedger;
132
+ revalidatePhasePolicy(
133
+ resources: DurableExecutionResources,
134
+ ledger: TaskLedger,
135
+ task: PlanTaskDraft,
136
+ signal: AbortSignal,
137
+ ): Promise<WorkflowAttemptOutcome | undefined>;
138
+ run(runId: string): DurableExecutionResources | undefined;
139
+ }
140
+ export class ChangeVerification {
141
+ readonly #services: ChangeVerificationServices;
142
+ readonly #options: Pick<
143
+ DurableWorkflowEngineOptions,
144
+ | "verificationEnvironment"
145
+ | "verifyPhase"
146
+ | "verifyChange"
147
+ | "verificationPolicy"
148
+ >;
149
+ constructor(
150
+ options: Pick<
151
+ DurableWorkflowEngineOptions,
152
+ | "verificationEnvironment"
153
+ | "verifyPhase"
154
+ | "verifyChange"
155
+ | "verificationPolicy"
156
+ >,
157
+ services: ChangeVerificationServices,
158
+ ) {
159
+ this.#options = options;
160
+ this.#services = services;
161
+ }
162
+ async #refreshEnvironment(
163
+ resources: DurableExecutionResources,
164
+ signal: AbortSignal,
165
+ ): Promise<void> {
166
+ if (!this.#options.verificationEnvironment) return;
167
+ const identity = await this.#options.verificationEnvironment(
168
+ resources.plan,
169
+ signal,
170
+ );
171
+ if (identity !== resources.environmentIdentity) {
172
+ resources.environmentIdentity = identity;
173
+ resources.baselinePromises.clear();
174
+ resources.verificationFact = undefined;
175
+ }
176
+ }
177
+
178
+ async #environmentCurrent(
179
+ runId: string,
180
+ signal: AbortSignal,
181
+ expected?: string,
182
+ ): Promise<boolean> {
183
+ if (!this.#options.verificationEnvironment) return true;
184
+ const resources = this.#services.run(runId);
185
+ if (!resources) return false;
186
+ try {
187
+ const identity = await this.#options.verificationEnvironment(
188
+ resources.plan,
189
+ signal,
190
+ );
191
+ resources.environmentIdentity ??= identity;
192
+ return identity === (expected ?? resources.environmentIdentity);
193
+ } catch {
194
+ signal.throwIfAborted();
195
+ return false;
196
+ }
197
+ }
198
+
199
+ async verifyPhase(
200
+ input: Parameters<DurableWorkflowEngineOptions["verifyPhase"]>[0],
201
+ ): Promise<DurablePhaseVerificationResult> {
202
+ if (await this.#environmentCurrent(input.runId, input.signal)) {
203
+ const identity = this.#services.run(input.runId)?.environmentIdentity;
204
+ const result = await this.#options.verifyPhase(input);
205
+ if (await this.#environmentCurrent(input.runId, input.signal, identity))
206
+ return result;
207
+ }
208
+ return {
209
+ ok: false,
210
+ kind: "paused",
211
+ code: "verification-environment-changed",
212
+ };
213
+ }
214
+
215
+ async #verifyChange(
216
+ input: Parameters<DurableWorkflowEngineOptions["verifyChange"]>[0],
217
+ ): Promise<DurableChangeVerificationResult> {
218
+ if (await this.#environmentCurrent(input.runId, input.signal)) {
219
+ const identity = this.#services.run(input.runId)?.environmentIdentity;
220
+ const result = await this.#options.verifyChange(input);
221
+ if (await this.#environmentCurrent(input.runId, input.signal, identity))
222
+ return result;
223
+ }
224
+ return {
225
+ ok: false,
226
+ kind: "environment",
227
+ code: "verification-environment-changed",
228
+ };
229
+ }
230
+
231
+ async verifyPostApply(
232
+ resources: DurableExecutionResources,
233
+ root: string,
234
+ signal: AbortSignal,
235
+ transactionId: string,
236
+ ): Promise<{ ok: true } | { ok: false; code: string }> {
237
+ if (this.#options.verificationEnvironment) {
238
+ const fact = this.#services
239
+ .ledger(resources, resources.deliveryRevision)
240
+ .durableFact(`apply-environment-${hash(transactionId).slice(0, 40)}`);
241
+ if (!isRecord(fact) || typeof fact.identity !== "string")
242
+ return { ok: false, code: "verification-environment-changed" };
243
+ resources.environmentIdentity = fact.identity;
244
+ if (
245
+ !(await this.#environmentCurrent(
246
+ resources.runId,
247
+ signal,
248
+ fact.identity,
249
+ ))
250
+ )
251
+ return { ok: false, code: "verification-environment-changed" };
252
+ }
253
+ if (
254
+ resources.plan.outputs.some(
255
+ (output) => observeSafePath(root, output.path).kind !== "file",
256
+ )
257
+ ) {
258
+ return { ok: false, code: "producer-output-unavailable" };
259
+ }
260
+ const lifecycle = hasVerificationLifecycle(resources.plan);
261
+ const result = await this.#verifyChange({
262
+ runId: resources.runId,
263
+ deliveryRevision: resources.deliveryRevision,
264
+ root,
265
+ plan: structuredClone(resources.plan),
266
+ ...(lifecycle
267
+ ? {
268
+ scope: "post-apply" as const,
269
+ verification: structuredClone(
270
+ resources.plan.verification.change.postApply,
271
+ ),
272
+ }
273
+ : {}),
274
+ signal,
275
+ });
276
+ return result.ok &&
277
+ result.exitCode === 0 &&
278
+ result.classification.length > 0
279
+ ? { ok: true }
280
+ : {
281
+ ok: false,
282
+ code: result.ok ? "post-apply-verification-invalid" : result.code,
283
+ };
284
+ }
285
+
286
+ async #observeVerification(input: {
287
+ resources: DurableExecutionResources;
288
+ revisionId: string;
289
+ scope: DurableVerificationScope;
290
+ verification: StructuredVerificationContract;
291
+ signal: AbortSignal;
292
+ taskId?: string;
293
+ }): Promise<DurableObservedVerification> {
294
+ if (input.signal.aborted) {
295
+ return {
296
+ ok: false,
297
+ outcome: { kind: "operation-cancelled", code: "cancelled" },
298
+ };
299
+ }
300
+ const root = mkdtempSync(path.join(input.resources.root, "verification-"));
301
+ try {
302
+ await input.resources.workspaces.materializeAsync(
303
+ input.revisionId,
304
+ root,
305
+ input.signal,
306
+ );
307
+ const result = await this.#verifyChange({
308
+ runId: input.resources.runId,
309
+ deliveryRevision: input.resources.deliveryRevision,
310
+ root,
311
+ plan: structuredClone(input.resources.plan),
312
+ scope: input.scope,
313
+ verification: structuredClone(input.verification),
314
+ ...(input.taskId ? { taskId: input.taskId } : {}),
315
+ signal: input.signal,
316
+ });
317
+ if (result.ok) {
318
+ return {
319
+ ok: true,
320
+ observation: {
321
+ status: "passed",
322
+ verificationId: input.verification.id,
323
+ failureIdentities: verificationIdentities(
324
+ result,
325
+ input.verification,
326
+ input.scope,
327
+ ),
328
+ },
329
+ };
330
+ }
331
+ if (result.kind === "verification") {
332
+ return {
333
+ ok: true,
334
+ observation: {
335
+ status: "failed",
336
+ verificationId: input.verification.id,
337
+ failureIdentities: verificationIdentities(
338
+ result,
339
+ input.verification,
340
+ input.scope,
341
+ ),
342
+ code: result.code,
343
+ attributionReliable: result.attributionReliable,
344
+ },
345
+ };
346
+ }
347
+ if (result.kind === "approval-boundary") {
348
+ return {
349
+ ok: false,
350
+ outcome: { kind: "approval-needed", code: result.code },
351
+ };
352
+ }
353
+ if (result.kind === "cancelled" || input.signal.aborted) {
354
+ return {
355
+ ok: false,
356
+ outcome: { kind: "operation-cancelled", code: "cancelled" },
357
+ };
358
+ }
359
+ return {
360
+ ok: false,
361
+ outcome: { kind: "paused", code: result.code },
362
+ };
363
+ } finally {
364
+ rmSync(root, { recursive: true, force: true });
365
+ }
366
+ }
367
+
368
+ async #captureVerificationBaseline(input: {
369
+ resources: DurableExecutionResources;
370
+ ledger: TaskLedger;
371
+ signal: AbortSignal;
372
+ }): Promise<DurableBaselineResult> {
373
+ const environmentIdentity = input.resources.environmentIdentity;
374
+ const stored = input.ledger.durableFact(
375
+ this.baselineFactKey(input.resources),
376
+ );
377
+ if (stored !== undefined) {
378
+ const baseline = parseVerificationBaseline(
379
+ stored,
380
+ input.resources.artifacts,
381
+ );
382
+ if (baseline.revisionId !== input.resources.baselineRevisionId) {
383
+ throw new Error("workflow-verification-baseline-conflict");
384
+ }
385
+ return { ok: true, baseline };
386
+ }
387
+ const plan = input.resources.plan;
388
+ const affected: DurableVerificationBaseline["affected"] = [];
389
+ for (const task of plan.tasks) {
390
+ const observed = await this.#observeVerification({
391
+ resources: input.resources,
392
+ revisionId: input.resources.baselineRevisionId,
393
+ scope: "baseline-task-affected",
394
+ verification: task.affectedVerification,
395
+ taskId: task.taskId,
396
+ signal: input.signal,
397
+ });
398
+ if (!observed.ok) return observed;
399
+ affected.push({
400
+ taskId: task.taskId,
401
+ observation: observed.observation,
402
+ });
403
+ }
404
+ const fullSuite = await this.#observeVerification({
405
+ resources: input.resources,
406
+ revisionId: input.resources.baselineRevisionId,
407
+ scope: "baseline-full-suite",
408
+ verification: plan.verification.baseline.fullSuite,
409
+ signal: input.signal,
410
+ });
411
+ if (!fullSuite.ok) return fullSuite;
412
+ const baseline: DurableVerificationBaseline = {
413
+ revisionId: input.resources.baselineRevisionId,
414
+ targetContracts: plan.tasks
415
+ .filter(
416
+ (task) =>
417
+ !task.verificationMode || task.verificationMode === "behavior",
418
+ )
419
+ .map((task) => ({
420
+ taskId: task.taskId,
421
+ verificationId: task.phases.red.verification.id,
422
+ expectedFailure:
423
+ "expectedFailure" in task.phases.red.verification
424
+ ? (task.phases.red.verification.expectedFailure ??
425
+ task.phases.red.verification.id)
426
+ : task.phases.red.verification.id,
427
+ })),
428
+ affected,
429
+ fullSuite: fullSuite.observation,
430
+ };
431
+ if (environmentIdentity !== input.resources.environmentIdentity)
432
+ return {
433
+ ok: false,
434
+ outcome: { kind: "paused", code: "verification-environment-changed" },
435
+ };
436
+ input.ledger.putDurableFact(
437
+ this.baselineFactKey(input.resources),
438
+ verificationBaselineFact(baseline, input.resources.artifacts),
439
+ );
440
+ return { ok: true, baseline };
441
+ }
442
+
443
+ async ensureVerificationBaseline(input: {
444
+ resources: DurableExecutionResources;
445
+ ledger: TaskLedger;
446
+ signal: AbortSignal;
447
+ }): Promise<DurableBaselineResult> {
448
+ try {
449
+ await this.#refreshEnvironment(input.resources, input.signal);
450
+ } catch {
451
+ return {
452
+ ok: false,
453
+ outcome: {
454
+ kind: "paused",
455
+ code: "verification-environment-unavailable",
456
+ },
457
+ };
458
+ }
459
+ const existing = input.resources.baselinePromises.get(
460
+ input.resources.deliveryRevision,
461
+ );
462
+ if (existing) return existing;
463
+ const pending = this.#captureVerificationBaseline(input).then((result) => {
464
+ if (!result.ok) {
465
+ input.resources.baselinePromises.delete(
466
+ input.resources.deliveryRevision,
467
+ );
468
+ }
469
+ return result;
470
+ });
471
+ input.resources.baselinePromises.set(
472
+ input.resources.deliveryRevision,
473
+ pending,
474
+ );
475
+ return pending;
476
+ }
477
+
478
+ async verifyTaskAffected(input: {
479
+ resources: DurableExecutionResources;
480
+ baseline: DurableVerificationBaseline;
481
+ task: PlanTaskDraft;
482
+ revisionId: string;
483
+ signal: AbortSignal;
484
+ }): Promise<DurableAffectedResult> {
485
+ const baseline = input.baseline.affected.find(
486
+ (entry) => entry.taskId === input.task.taskId,
487
+ );
488
+ if (!baseline) throw new Error("workflow-task-baseline-unavailable");
489
+ const current = await this.#observeVerification({
490
+ resources: input.resources,
491
+ revisionId: input.revisionId,
492
+ scope: "task-affected",
493
+ verification: input.task.affectedVerification,
494
+ taskId: input.task.taskId,
495
+ signal: input.signal,
496
+ });
497
+ if (!current.ok) return current.outcome;
498
+ if (current.observation.status === "passed") {
499
+ return { kind: "verified", attribution: "none" };
500
+ }
501
+ if (
502
+ current.observation.attributionReliable === false &&
503
+ baseline.observation.status === "failed"
504
+ )
505
+ return { kind: "paused", code: "verification-attribution-unresolved" };
506
+ const prior = new Set(baseline.observation.failureIdentities);
507
+ const introduced = current.observation.failureIdentities.filter(
508
+ (identity) => !prior.has(identity),
509
+ );
510
+ if (introduced.length === 0) {
511
+ return { kind: "verified", attribution: "pre-existing" };
512
+ }
513
+ return {
514
+ kind: "repairable",
515
+ attribution: "introduced",
516
+ failureIdentities: introduced.slice(0, 256),
517
+ };
518
+ }
519
+
520
+ #agentsManagedParts(
521
+ text: string,
522
+ ): { prefix: string; suffix: string } | undefined {
523
+ const startMarker = "<!-- ABEL:AGENTS-INDEX:START -->";
524
+ const endMarker = "<!-- ABEL:AGENTS-INDEX:END -->";
525
+ const lines = text.split("\n");
526
+ const starts: Array<{ line: number; start: number }> = [];
527
+ const ends: Array<{ line: number; end: number }> = [];
528
+ let offset = 0;
529
+ for (const [line, value] of lines.entries()) {
530
+ if (value === startMarker) starts.push({ line, start: offset });
531
+ if (value === endMarker) {
532
+ ends.push({ line, end: offset + value.length });
533
+ }
534
+ offset += value.length + 1;
535
+ }
536
+ if (
537
+ starts.length !== 1 ||
538
+ ends.length !== 1 ||
539
+ starts[0].line >= ends[0].line
540
+ ) {
541
+ return undefined;
542
+ }
543
+ return {
544
+ prefix: text.slice(0, starts[0].start),
545
+ suffix: text.slice(ends[0].end),
546
+ };
547
+ }
548
+
549
+ #applyAgentsCheckpoint(
550
+ resources: DurableExecutionResources,
551
+ ): { ok: true; revision: WorkspaceRevision } | { ok: false; code: string } {
552
+ const checkpoint = resources.plan.verification.agentsCheckpoint;
553
+ const current = resources.workspaces.getRevision(
554
+ resources.currentRevisionId,
555
+ );
556
+ if (!checkpoint.required) return { ok: true, revision: current };
557
+ const changes: Record<
558
+ string,
559
+ { kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
560
+ > = {};
561
+ for (const operation of checkpoint.operations) {
562
+ const entry = current.entries[operation.target];
563
+ let before: string | null;
564
+ if (!entry || entry.kind === "absent") {
565
+ before = null;
566
+ } else {
567
+ try {
568
+ before = new TextDecoder("utf-8", { fatal: true }).decode(
569
+ resources.artifacts.read(entry.hash),
570
+ );
571
+ } catch {
572
+ return { ok: false, code: "agents-checkpoint-contract-stale" };
573
+ }
574
+ }
575
+ if (operation.impact === "create-index") {
576
+ if (operation.managedBlock === null) {
577
+ throw new Error("workflow-agents-checkpoint-invalid");
578
+ }
579
+ const desired = `${operation.managedBlock}\n`;
580
+ if (before !== null && before !== desired) {
581
+ return { ok: false, code: "agents-checkpoint-contract-stale" };
582
+ }
583
+ if (before === null) {
584
+ changes[operation.target] = {
585
+ kind: "file",
586
+ bytes: new TextEncoder().encode(desired),
587
+ mode: 0o644,
588
+ };
589
+ }
590
+ continue;
591
+ }
592
+ if (operation.impact === "remove-index") {
593
+ if (operation.managedBlock !== null) {
594
+ throw new Error("workflow-agents-checkpoint-invalid");
595
+ }
596
+ if (before === null) continue;
597
+ const parts = this.#agentsManagedParts(before);
598
+ if (!parts) {
599
+ return { ok: false, code: "agents-checkpoint-contract-stale" };
600
+ }
601
+ const after = `${parts.prefix}${parts.suffix}`;
602
+ changes[operation.target] =
603
+ after.trim().length === 0
604
+ ? { kind: "absent" }
605
+ : {
606
+ kind: "file",
607
+ bytes: new TextEncoder().encode(after),
608
+ mode: entry?.kind === "file" ? entry.mode : 0o644,
609
+ };
610
+ continue;
611
+ }
612
+ if (operation.managedBlock === null || before === null) {
613
+ return { ok: false, code: "agents-checkpoint-contract-stale" };
614
+ }
615
+ const parts = this.#agentsManagedParts(before);
616
+ if (!parts) {
617
+ return { ok: false, code: "agents-checkpoint-contract-stale" };
618
+ }
619
+ const after = `${parts.prefix}${operation.managedBlock}${parts.suffix}`;
620
+ if (after !== before) {
621
+ changes[operation.target] = {
622
+ kind: "file",
623
+ bytes: new TextEncoder().encode(after),
624
+ mode: entry?.kind === "file" ? entry.mode : 0o644,
625
+ };
626
+ }
627
+ }
628
+ if (Object.keys(changes).length === 0) {
629
+ return { ok: true, revision: current };
630
+ }
631
+ return {
632
+ ok: true,
633
+ revision: resources.workspaces.createRevision({
634
+ parentRevisionId: current.revisionId,
635
+ changes,
636
+ }),
637
+ };
638
+ }
639
+
640
+ baselineFactKey(resources: DurableExecutionResources): string {
641
+ return this.#options.verificationPolicy || resources.environmentIdentity
642
+ ? `verification-baseline-${hash(this.#options.verificationPolicy ?? "legacy", resources.environmentIdentity ?? "legacy").slice(0, 24)}`
643
+ : "verification-baseline";
644
+ }
645
+
646
+ async verify(
647
+ input: Parameters<WorkflowChangeVerifier["verify"]>[0],
648
+ ): Promise<Awaited<ReturnType<WorkflowChangeVerifier["verify"]>>> {
649
+ if (!input.currentWorkspaceRevisionId) {
650
+ return { kind: "paused", code: "workspace-revision-unavailable" };
651
+ }
652
+ const resources = await this.#services.prepareResources(
653
+ {
654
+ runId: input.runId,
655
+ deliveryRevision: input.deliveryRevision,
656
+ plan: input.plan,
657
+ currentWorkspaceRevisionId: input.currentWorkspaceRevisionId,
658
+ baselineRevisionId:
659
+ input.baselineRevisionId ??
660
+ this.#services.run(input.runId)?.baselineRevisionId,
661
+ },
662
+ input.signal,
663
+ );
664
+ try {
665
+ await this.#refreshEnvironment(resources, input.signal);
666
+ } catch {
667
+ return { kind: "paused", code: "verification-environment-unavailable" };
668
+ }
669
+ for (const reference of input.taskEvidence ?? []) {
670
+ const task = input.plan.tasks.find(
671
+ (task) => task.taskId === reference.taskId,
672
+ );
673
+ if (!task) throw new Error("workflow-ledger-task-invalid");
674
+ const failure = await this.#services.revalidatePhasePolicy(
675
+ resources,
676
+ this.#services.ledger(resources, reference.deliveryRevision),
677
+ task,
678
+ input.signal,
679
+ );
680
+ if (failure)
681
+ return {
682
+ kind: "paused",
683
+ code:
684
+ "code" in failure ? failure.code : "verification-policy-rejected",
685
+ };
686
+ }
687
+ const cumulativeRevision = resources.workspaces.getRevision(
688
+ resources.currentRevisionId,
689
+ );
690
+ if (
691
+ input.plan.outputs.some(
692
+ (output) => cumulativeRevision.entries[output.path]?.kind !== "file",
693
+ )
694
+ ) {
695
+ return { kind: "paused", code: "producer-output-unavailable" };
696
+ }
697
+ if (hasVerificationLifecycle(input.plan)) {
698
+ const ledger = this.#services.ledger(resources, input.deliveryRevision);
699
+ const captured = await this.ensureVerificationBaseline({
700
+ resources,
701
+ ledger,
702
+ signal: input.signal,
703
+ });
704
+ if (!captured.ok) {
705
+ return captured.outcome.kind === "approval-needed"
706
+ ? captured.outcome
707
+ : captured.outcome.kind === "operation-cancelled"
708
+ ? { kind: "paused", code: "cancelled" }
709
+ : {
710
+ ...captured.outcome,
711
+ verification: {
712
+ scope: "baseline",
713
+ attribution: "environment",
714
+ },
715
+ };
716
+ }
717
+ for (const task of input.plan.tasks) {
718
+ const baseline = captured.baseline.affected.find(
719
+ (entry) => entry.taskId === task.taskId,
720
+ );
721
+ if (!baseline) throw new Error("workflow-task-baseline-unavailable");
722
+ const current = await this.#observeVerification({
723
+ resources,
724
+ revisionId: resources.currentRevisionId,
725
+ scope: "change-task-affected",
726
+ verification: task.affectedVerification,
727
+ taskId: task.taskId,
728
+ signal: input.signal,
729
+ });
730
+ if (!current.ok) {
731
+ return current.outcome.kind === "approval-needed"
732
+ ? current.outcome
733
+ : current.outcome.kind === "operation-cancelled"
734
+ ? { kind: "paused", code: "cancelled" }
735
+ : {
736
+ ...current.outcome,
737
+ verification: {
738
+ scope: "change-task-affected",
739
+ attribution: "environment",
740
+ taskId: task.taskId,
741
+ },
742
+ };
743
+ }
744
+ if (current.observation.status === "failed") {
745
+ if (
746
+ current.observation.attributionReliable === false &&
747
+ baseline.observation.status === "failed"
748
+ )
749
+ return {
750
+ kind: "paused",
751
+ code: "verification-attribution-unresolved",
752
+ };
753
+ const prior = new Set(baseline.observation.failureIdentities);
754
+ const introduced = current.observation.failureIdentities.filter(
755
+ (identity) => !prior.has(identity),
756
+ );
757
+ if (introduced.length > 0) {
758
+ return {
759
+ kind: "paused",
760
+ code: "verification-repair-required",
761
+ verification: {
762
+ scope: "change-task-affected",
763
+ attribution: "introduced",
764
+ taskId: task.taskId,
765
+ failureIdentities: introduced.slice(0, 256),
766
+ },
767
+ };
768
+ }
769
+ }
770
+ }
771
+ const fullSuite = await this.#observeVerification({
772
+ resources,
773
+ revisionId: resources.currentRevisionId,
774
+ scope: "change-full-suite",
775
+ verification: input.plan.verification.change.fullSuite,
776
+ signal: input.signal,
777
+ });
778
+ if (!fullSuite.ok) {
779
+ return fullSuite.outcome.kind === "approval-needed"
780
+ ? fullSuite.outcome
781
+ : fullSuite.outcome.kind === "operation-cancelled"
782
+ ? { kind: "paused", code: "cancelled" }
783
+ : {
784
+ ...fullSuite.outcome,
785
+ verification: {
786
+ scope: "change-full-suite",
787
+ attribution: "environment",
788
+ },
789
+ };
790
+ }
791
+ if (fullSuite.observation.status === "failed") {
792
+ if (
793
+ fullSuite.observation.attributionReliable === false &&
794
+ captured.baseline.fullSuite.status === "failed"
795
+ )
796
+ return {
797
+ kind: "paused",
798
+ code: "verification-attribution-unresolved",
799
+ };
800
+ const baselineFailures = new Set(
801
+ captured.baseline.fullSuite.failureIdentities,
802
+ );
803
+ const introduced = fullSuite.observation.failureIdentities.filter(
804
+ (identity) => !baselineFailures.has(identity),
805
+ );
806
+ if (introduced.length > 0) {
807
+ return {
808
+ kind: "paused",
809
+ code: "verification-attribution-unresolved",
810
+ verification: {
811
+ scope: "change-full-suite",
812
+ attribution: "unresolved",
813
+ failureIdentities: introduced.slice(0, 256),
814
+ },
815
+ };
816
+ }
817
+ }
818
+ const checkpoint = input.plan.verification.agentsCheckpoint;
819
+ if (checkpoint.required && checkpoint.verification) {
820
+ const applied = this.#applyAgentsCheckpoint(resources);
821
+ if (!applied.ok) {
822
+ return { kind: "paused", code: applied.code };
823
+ }
824
+ const observed = await this.#observeVerification({
825
+ resources,
826
+ revisionId: applied.revision.revisionId,
827
+ scope: "agents-checkpoint",
828
+ verification: checkpoint.verification,
829
+ signal: input.signal,
830
+ });
831
+ if (!observed.ok) {
832
+ return observed.outcome.kind === "approval-needed"
833
+ ? observed.outcome
834
+ : observed.outcome.kind === "operation-cancelled"
835
+ ? { kind: "paused", code: "cancelled" }
836
+ : {
837
+ ...observed.outcome,
838
+ verification: {
839
+ scope: "agents-checkpoint",
840
+ attribution: "environment",
841
+ },
842
+ };
843
+ }
844
+ if (observed.observation.status === "failed") {
845
+ return {
846
+ kind: "paused",
847
+ code: "agents-checkpoint-verification-failed",
848
+ verification: {
849
+ scope: "agents-checkpoint",
850
+ attribution: "introduced",
851
+ failureIdentities: observed.observation.failureIdentities.slice(
852
+ 0,
853
+ 256,
854
+ ),
855
+ },
856
+ };
857
+ }
858
+ resources.currentRevisionId = applied.revision.revisionId;
859
+ }
860
+ const verificationId = `change-${hash(
861
+ input.plan.changeId,
862
+ String(input.deliveryRevision),
863
+ resources.currentRevisionId,
864
+ ...(resources.environmentIdentity
865
+ ? [resources.environmentIdentity]
866
+ : []),
867
+ ).slice(0, 40)}`;
868
+ const sealed = await verifyCumulativeRevision({
869
+ workspaceStore: resources.workspaces,
870
+ revisionId: resources.currentRevisionId,
871
+ verificationId,
872
+ signal: input.signal,
873
+ execute: async () => ({
874
+ ok: true as const,
875
+ exitCode: 0 as const,
876
+ classification: "lifecycle-verified",
877
+ }),
878
+ });
879
+ if (!sealed.ok) return { kind: "paused", code: sealed.code };
880
+ resources.verificationFact = sealed.fact;
881
+ return {
882
+ kind: "verified",
883
+ verificationId: sealed.verificationId,
884
+ currentWorkspaceRevisionId: resources.currentRevisionId,
885
+ };
886
+ }
887
+ const verificationId = `change-${hash(
888
+ input.plan.changeId,
889
+ String(input.deliveryRevision),
890
+ ...(resources.environmentIdentity ? [resources.environmentIdentity] : []),
891
+ ).slice(0, 40)}`;
892
+ const result = await verifyCumulativeRevision({
893
+ workspaceStore: resources.workspaces,
894
+ revisionId: resources.currentRevisionId,
895
+ verificationId,
896
+ signal: input.signal,
897
+ execute: ({ root, signal }) =>
898
+ this.#verifyChange({
899
+ runId: input.runId,
900
+ deliveryRevision: input.deliveryRevision,
901
+ root,
902
+ plan: structuredClone(input.plan),
903
+ signal: signal ?? input.signal,
904
+ }),
905
+ });
906
+ if (!result.ok) {
907
+ return {
908
+ kind:
909
+ result.kind === "approval-boundary"
910
+ ? "approval-needed"
911
+ : result.kind === "verification"
912
+ ? "retryable"
913
+ : "paused",
914
+ code: result.code,
915
+ };
916
+ }
917
+ resources.verificationFact = result.fact;
918
+ return { kind: "verified", verificationId: result.verificationId };
919
+ }
920
+ }