@abelxiaoxing/cadence 1.3.0 → 1.4.1

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