@narumitw/pi-subagents 0.53.0 → 1.0.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 +85 -15
  2. package/package.json +1 -1
  3. package/src/agents/built-ins.ts +124 -0
  4. package/src/agents/catalog.ts +224 -0
  5. package/src/agents/discovery.ts +249 -0
  6. package/src/agents/types.ts +98 -0
  7. package/src/agents.ts +47 -670
  8. package/src/auto-transport.ts +2 -1
  9. package/src/automation.ts +7 -2
  10. package/src/capability-router.ts +1 -1
  11. package/src/completion-delivery.ts +82 -11
  12. package/src/config-status.ts +2 -2
  13. package/src/config-ui.ts +8 -8
  14. package/src/consult-resources.ts +1 -1
  15. package/src/consult.ts +9 -7
  16. package/src/create-stateful-transport.ts +2 -1
  17. package/src/cwd-policy.ts +1 -1
  18. package/src/execution/budget.ts +56 -0
  19. package/src/execution/runtime-policy.ts +19 -0
  20. package/src/execution-plan.ts +1 -1
  21. package/src/execution-profiles.ts +1 -1
  22. package/src/execution-ui.ts +1 -1
  23. package/src/execution.ts +269 -100
  24. package/src/in-process-transport.ts +3 -2
  25. package/src/inspect.ts +39 -8
  26. package/src/limits.ts +1 -0
  27. package/src/orchestration-metrics.ts +12 -5
  28. package/src/panel-execution.ts +1 -1
  29. package/src/panel-planning.ts +1 -1
  30. package/src/params.ts +3 -1
  31. package/src/persistence.ts +106 -7
  32. package/src/registry-types.ts +22 -5
  33. package/src/registry.ts +205 -37
  34. package/src/render.ts +1 -1
  35. package/src/retained-semantic-state.ts +1 -1
  36. package/src/rpc-transport-metadata.ts +1 -1
  37. package/src/rpc-transport.ts +2 -1
  38. package/src/runner.ts +6 -1
  39. package/src/settings/inspection.ts +275 -0
  40. package/src/settings/schema.ts +186 -0
  41. package/src/settings.ts +72 -420
  42. package/src/spawn-idempotency.ts +1 -1
  43. package/src/stateful-agent-view.ts +87 -0
  44. package/src/stateful-config.ts +1 -1
  45. package/src/stateful-guidance.ts +2 -2
  46. package/src/stateful-limits.ts +1 -1
  47. package/src/stateful-prompt.ts +2 -2
  48. package/src/stateful-render.ts +0 -1
  49. package/src/stateful-safety.ts +2 -1
  50. package/src/stateful-tool-params.ts +13 -20
  51. package/src/stateful.ts +36 -117
  52. package/src/subagents.ts +8 -9
  53. package/src/subprocess-transport.ts +2 -6
  54. package/src/transport-types.ts +1 -1
  55. package/src/transport-ui.ts +1 -1
  56. package/src/verification-harness.ts +516 -0
  57. package/src/verification-receipt.ts +275 -0
  58. package/src/verified-execution-benchmark.ts +86 -0
  59. package/src/verified-execution-contract.ts +219 -0
  60. package/src/work-item-ledger.ts +510 -37
  61. package/src/work-item-persistence.ts +31 -0
  62. package/src/workflow-completion-controller.ts +397 -0
  63. package/src/workflow-plan-compiler.ts +1 -1
  64. package/src/workflow-plan-patch.ts +1 -1
  65. package/src/workflow-planning.ts +11 -1
  66. package/src/workflow-ui.ts +1 -1
@@ -1,8 +1,14 @@
1
+ /**
2
+ * Ledger transitions and restore validators stay together so every persisted acceptance invariant
3
+ * is audited against the same atomic state machine rather than duplicated across modules.
4
+ */
1
5
  import {
2
6
  type ManagedIntegrationCandidate,
3
7
  type ManagedIntegrationExpectation,
4
8
  verifyManagedIntegration,
5
9
  } from "./integration-controller.js";
10
+ import type { VerificationSubmission } from "./verification-harness.js";
11
+ import { isVerificationReceipt, type VerificationReceipt } from "./verification-receipt.js";
6
12
  import {
7
13
  isWorkflowTreeIdentity,
8
14
  sameWorkflowTreeIdentity,
@@ -13,8 +19,18 @@ import {
13
19
  type WorkflowVerificationReceipt,
14
20
  } from "./workflow-verification.js";
15
21
 
16
- export const WORK_ITEM_LEDGER_VERSION = "pi-subagents:work-ledger:v2" as const;
17
- const LEGACY_WORK_ITEM_LEDGER_VERSION = "pi-subagents:work-ledger:v1" as const;
22
+ export const WORK_ITEM_LEDGER_VERSION = "pi-subagents:work-ledger:v3" as const;
23
+ const LEGACY_WORK_ITEM_LEDGER_VERSIONS = new Set([
24
+ "pi-subagents:work-ledger:v1",
25
+ "pi-subagents:work-ledger:v2",
26
+ ]);
27
+ export const WORK_ITEM_ACCEPTANCE_VERSION = "pi-subagents:work-acceptance:v1" as const;
28
+ export type WorkItemAcceptanceState =
29
+ | "not-required"
30
+ | "pending"
31
+ | "accepted"
32
+ | "rework-requested"
33
+ | "rejected";
18
34
 
19
35
  export type WorkItemState =
20
36
  | "pending"
@@ -53,9 +69,12 @@ export interface WorkItemDefinition {
53
69
  writePaths?: string[];
54
70
  ownershipKeys?: string[];
55
71
  acceptanceCriteria?: string[];
72
+ requiredEvidence?: string[];
56
73
  integrationOwner?: boolean;
57
74
  verifierFor?: string;
58
75
  dependencyPolicy?: "completed" | "settled";
76
+ acceptanceRequired?: boolean;
77
+ maxReworkCycles?: 0 | 1;
59
78
  }
60
79
 
61
80
  export interface WorkItemRecord {
@@ -81,12 +100,21 @@ export interface WorkItemRecord {
81
100
  writePaths: string[];
82
101
  ownershipKeys: string[];
83
102
  acceptanceCriteria: string[];
103
+ requiredEvidence: string[];
84
104
  integrationOwner: boolean;
85
105
  verifierFor?: string;
86
106
  dependencyPolicy: "completed" | "settled";
107
+ acceptanceStateVersion: typeof WORK_ITEM_ACCEPTANCE_VERSION;
108
+ acceptanceRequired: boolean;
109
+ acceptanceState: WorkItemAcceptanceState;
110
+ reworkCount: number;
111
+ maxReworkCycles: 0 | 1;
87
112
  verificationAccepted: boolean;
88
113
  stagedTreeIdentity?: WorkflowTreeIdentity;
89
114
  verificationReceipt?: WorkflowVerificationReceipt;
115
+ acceptanceReceipt?: VerificationReceipt;
116
+ acceptanceReceiptHistory: VerificationReceipt[];
117
+ submission?: VerificationSubmission;
90
118
  invalidationReasons: string[];
91
119
  outcomeReason?: string;
92
120
  }
@@ -114,12 +142,31 @@ export interface StageWorkItemVerificationInput extends CompleteWorkItemInput {
114
142
  treeIdentity: WorkflowTreeIdentity;
115
143
  }
116
144
 
145
+ export interface StageVerifiedAcceptanceInput
146
+ extends CompleteWorkItemInput,
147
+ VerificationSubmission {
148
+ executionPlanId: string;
149
+ }
150
+
117
151
  export interface CompleteWorkItemVerificationInput {
118
152
  taskGeneration: number;
119
153
  executionPlanId: string;
120
154
  receipt: WorkflowVerificationReceipt;
121
155
  }
122
156
 
157
+ export interface RecordVerificationDecisionInput {
158
+ taskGeneration: number;
159
+ executionPlanId: string;
160
+ receipt: VerificationReceipt;
161
+ }
162
+
163
+ export interface VerifiedIntegrationInput {
164
+ verifierId: string;
165
+ verifierTaskGeneration: number;
166
+ verifierExecutionPlanId: string;
167
+ receipt: VerificationReceipt;
168
+ }
169
+
123
170
  const MAX_ITEMS = 64;
124
171
  const MAX_IDENTIFIER_LENGTH = 256;
125
172
  const MAX_TEXT_LENGTH = 16 * 1024;
@@ -179,6 +226,9 @@ export class WorkItemLedger {
179
226
  complete(id: string, input: CompleteWorkItemInput): WorkItemRecord {
180
227
  const item = this.require(id);
181
228
  this.assertMutable(item);
229
+ if (item.acceptanceRequired) {
230
+ throw new Error(`WorkItem ${id} requires executor-owned acceptance staging`);
231
+ }
182
232
  if (item.state !== "running") {
183
233
  throw new Error(`WorkItem ${id} cannot complete while ${item.state}`);
184
234
  }
@@ -191,6 +241,41 @@ export class WorkItemLedger {
191
241
  item.verificationAccepted = false;
192
242
  item.stagedTreeIdentity = undefined;
193
243
  item.verificationReceipt = undefined;
244
+ item.acceptanceState = "not-required";
245
+ item.state = "completed";
246
+ item.generation = ++this.generation;
247
+ this.refreshReadyState();
248
+ return structuredClone(item);
249
+ }
250
+
251
+ stageForVerifiedAcceptance(id: string, input: StageVerifiedAcceptanceInput): WorkItemRecord {
252
+ const item = this.require(id);
253
+ this.assertMutable(item);
254
+ if (!item.acceptanceRequired || !item.integrationOwner) {
255
+ throw new Error(`WorkItem ${id} is not a verified integration target`);
256
+ }
257
+ if (item.state !== "running") {
258
+ throw new Error(`WorkItem ${id} cannot stage acceptance while ${item.state}`);
259
+ }
260
+ if (input.taskGeneration !== item.taskGeneration) {
261
+ throw new Error(`WorkItem ${id} rejected a stale task generation`);
262
+ }
263
+ validatePlanId(input.executionPlanId, "staged execution plan");
264
+ validateSubmission(input);
265
+ item.acceptedExecutionPlanId = input.executionPlanId;
266
+ item.artifactHistory.push(...item.artifacts.map((artifact) => structuredClone(artifact)));
267
+ item.artifacts = normalizeArtifacts(input.artifacts ?? [], id, this.generation + 1);
268
+ item.verificationAccepted = false;
269
+ item.acceptanceState = "pending";
270
+ item.stagedTreeIdentity = structuredClone(input.treeIdentity);
271
+ item.submission = {
272
+ treeIdentity: structuredClone(input.treeIdentity),
273
+ baseRepositoryGeneration: input.baseRepositoryGeneration,
274
+ patchDigest: input.patchDigest,
275
+ changedPaths: [...input.changedPaths],
276
+ fileVersions: { ...input.fileVersions },
277
+ };
278
+ item.acceptanceReceipt = undefined;
194
279
  item.state = "completed";
195
280
  item.generation = ++this.generation;
196
281
  this.refreshReadyState();
@@ -268,6 +353,80 @@ export class WorkItemLedger {
268
353
  return { target: structuredClone(target), verifier: structuredClone(verifier) };
269
354
  }
270
355
 
356
+ recordVerificationDecision(
357
+ verifierId: string,
358
+ input: RecordVerificationDecisionInput,
359
+ ): { target: WorkItemRecord; verifier: WorkItemRecord } {
360
+ const verifier = this.require(verifierId);
361
+ if (verifier.state !== "running" || !verifier.verifierFor) {
362
+ throw new Error(`WorkItem ${verifierId} is not a running verifier`);
363
+ }
364
+ if (input.taskGeneration !== verifier.taskGeneration) {
365
+ throw new Error(`WorkItem ${verifierId} rejected a stale verifier generation`);
366
+ }
367
+ validatePlanId(input.executionPlanId, "verifier execution plan");
368
+ if (!isVerificationReceipt(input.receipt) || input.receipt.decision === "accept") {
369
+ throw new Error(`WorkItem ${verifierId} received an invalid non-acceptance receipt`);
370
+ }
371
+ const target = this.require(verifier.verifierFor);
372
+ assertVerifiedReceiptMatches(target, verifier, input.executionPlanId, input.receipt);
373
+ verifier.acceptedExecutionPlanId = input.executionPlanId;
374
+ verifier.state = "completed";
375
+ verifier.generation = ++this.generation;
376
+ target.acceptanceReceipt = structuredClone(input.receipt);
377
+ target.acceptanceReceiptHistory.push(structuredClone(input.receipt));
378
+ target.verificationAccepted = false;
379
+ if (input.receipt.decision === "rework" && target.reworkCount < target.maxReworkCycles) {
380
+ target.reworkCount++;
381
+ target.acceptanceState = "rework-requested";
382
+ target.state = "blocked";
383
+ target.outcomeReason = "verification-rework";
384
+ } else {
385
+ target.acceptanceState = "rejected";
386
+ target.state = "failed";
387
+ target.outcomeReason =
388
+ input.receipt.decision === "rework"
389
+ ? "verification-rework-exhausted"
390
+ : "verification-rejected";
391
+ }
392
+ target.generation = ++this.generation;
393
+ this.invalidateDependents(target.id, verifier.id, target.outcomeReason);
394
+ this.refreshReadyState();
395
+ return { target: structuredClone(target), verifier: structuredClone(verifier) };
396
+ }
397
+
398
+ beginVerificationRework(id: string): WorkItemRecord {
399
+ const target = this.require(id);
400
+ if (target.acceptanceState !== "rework-requested" || target.state !== "blocked") {
401
+ throw new Error(`WorkItem ${id} has no accepted rework request`);
402
+ }
403
+ const verifier = [...this.items.values()].find((item) => item.verifierFor === id);
404
+ if (verifier?.state !== "completed") {
405
+ throw new Error(`WorkItem ${id} has no completed verifier for rework`);
406
+ }
407
+ target.artifactHistory.push(...target.artifacts.map((artifact) => structuredClone(artifact)));
408
+ target.artifacts = [];
409
+ target.state = "pending";
410
+ target.acceptanceState = "pending";
411
+ target.taskGeneration++;
412
+ target.assignedAgentId = undefined;
413
+ target.acceptedExecutionPlanId = undefined;
414
+ target.stagedTreeIdentity = undefined;
415
+ target.submission = undefined;
416
+ target.verificationAccepted = false;
417
+ target.outcomeReason = undefined;
418
+ target.generation = ++this.generation;
419
+ verifier.state = "pending";
420
+ verifier.taskGeneration++;
421
+ verifier.assignedAgentId = undefined;
422
+ verifier.acceptedExecutionPlanId = undefined;
423
+ verifier.outcomeReason = undefined;
424
+ verifier.generation = ++this.generation;
425
+ this.resetReworkDependents(target.id, verifier.id);
426
+ this.refreshReadyState();
427
+ return structuredClone(target);
428
+ }
429
+
271
430
  failVerification(verifierId: string, reason: string): WorkItemRecord[] {
272
431
  const verifier = this.require(verifierId);
273
432
  if (!verifier.verifierFor) throw new Error(`WorkItem ${verifierId} is not a verifier`);
@@ -279,8 +438,12 @@ export class WorkItemLedger {
279
438
  verifier.outcomeReason = boundedReason;
280
439
  verifier.generation = ++this.generation;
281
440
  }
282
- if (target.state === "awaiting-verification") {
441
+ if (
442
+ target.state === "awaiting-verification" ||
443
+ (target.state === "completed" && target.acceptanceState === "pending")
444
+ ) {
283
445
  target.state = "failed";
446
+ if (target.acceptanceRequired) target.acceptanceState = "rejected";
284
447
  target.verificationAccepted = false;
285
448
  target.outcomeReason = boundedReason;
286
449
  target.generation = ++this.generation;
@@ -299,12 +462,14 @@ export class WorkItemLedger {
299
462
  if (
300
463
  item.state !== "running" &&
301
464
  item.state !== "awaiting-verification" &&
465
+ !(item.state === "completed" && item.acceptanceState === "pending") &&
302
466
  item.state !== "ready" &&
303
467
  item.state !== "pending"
304
468
  ) {
305
469
  throw new Error(`WorkItem ${id} cannot settle while ${item.state}`);
306
470
  }
307
471
  item.state = state;
472
+ if (item.acceptanceRequired) item.acceptanceState = "rejected";
308
473
  item.outcomeReason = reason ? bounded(reason, MAX_TEXT_LENGTH) : undefined;
309
474
  item.generation = ++this.generation;
310
475
  this.refreshReadyState();
@@ -315,25 +480,64 @@ export class WorkItemLedger {
315
480
  id: string,
316
481
  expected: ManagedIntegrationExpectation,
317
482
  candidate: ManagedIntegrationCandidate,
483
+ verified?: VerifiedIntegrationInput,
318
484
  ): WorkItemRecord {
319
485
  const item = this.require(id);
320
486
  if (!item.integrationOwner) {
321
487
  throw new Error(`WorkItem ${id} is not the integration owner`);
322
488
  }
323
- if (item.state !== "running") {
324
- throw new Error(`WorkItem ${id} cannot integrate while ${item.state}`);
325
- }
326
489
  if (expected.taskId !== id || expected.taskGeneration !== item.taskGeneration) {
327
490
  throw new Error(`WorkItem ${id} integration expectation has a stale generation`);
328
491
  }
492
+ if (!verified) {
493
+ if (item.state !== "running" || item.acceptanceRequired) {
494
+ throw new Error(`WorkItem ${id} cannot integrate while ${item.state}`);
495
+ }
496
+ verifyManagedIntegration(expected, candidate);
497
+ item.verificationAccepted = true;
498
+ item.generation = ++this.generation;
499
+ return structuredClone(item);
500
+ }
501
+ if (item.state !== "completed" || item.acceptanceState !== "pending") {
502
+ throw new Error(`WorkItem ${id} is not pending verified acceptance`);
503
+ }
504
+ const verifier = this.require(verified.verifierId);
505
+ if (
506
+ verifier.state !== "running" ||
507
+ verifier.verifierFor !== id ||
508
+ verified.verifierTaskGeneration !== verifier.taskGeneration
509
+ ) {
510
+ throw new Error(`WorkItem ${verified.verifierId} is not the current verifier`);
511
+ }
512
+ if (!isVerificationReceipt(verified.receipt) || verified.receipt.decision !== "accept") {
513
+ throw new Error("Managed integration requires an accepting verification receipt");
514
+ }
515
+ assertVerifiedReceiptMatches(
516
+ item,
517
+ verifier,
518
+ verified.verifierExecutionPlanId,
519
+ verified.receipt,
520
+ );
329
521
  verifyManagedIntegration(expected, candidate);
330
522
  item.verificationAccepted = true;
523
+ item.acceptanceState = "accepted";
524
+ item.acceptanceReceipt = structuredClone(verified.receipt);
525
+ item.acceptanceReceiptHistory.push(structuredClone(verified.receipt));
526
+ item.artifacts = item.artifacts.map((artifact) => ({ ...artifact, verified: true }));
527
+ item.outcomeReason = undefined;
331
528
  item.generation = ++this.generation;
529
+ verifier.acceptedExecutionPlanId = verified.verifierExecutionPlanId;
530
+ verifier.state = "completed";
531
+ verifier.generation = ++this.generation;
532
+ this.refreshReadyState();
332
533
  return structuredClone(item);
333
534
  }
334
535
 
335
536
  rerun(id: string): WorkItemRecord {
336
537
  const item = this.require(id);
538
+ if (item.acceptanceState === "accepted") {
539
+ throw new Error(`WorkItem ${id} has terminal accepted evidence`);
540
+ }
337
541
  if (
338
542
  !["blocked", "needs-input", "failed", "interrupted", "stale", "invalidated"].includes(
339
543
  item.state,
@@ -347,8 +551,11 @@ export class WorkItemLedger {
347
551
  item.acceptedExecutionPlanId = undefined;
348
552
  item.outcomeReason = undefined;
349
553
  item.verificationAccepted = false;
554
+ item.acceptanceState = item.acceptanceRequired ? "pending" : "not-required";
350
555
  item.stagedTreeIdentity = undefined;
556
+ item.submission = undefined;
351
557
  item.verificationReceipt = undefined;
558
+ item.acceptanceReceipt = undefined;
352
559
  item.generation = ++this.generation;
353
560
  this.refreshReadyState();
354
561
  return structuredClone(item);
@@ -358,7 +565,7 @@ export class WorkItemLedger {
358
565
  const root = this.require(id);
359
566
  const normalizedReason = bounded(reason, MAX_TEXT_LENGTH);
360
567
  if (!normalizedReason) throw new Error("WorkItem invalidation requires a reason");
361
- const affected: WorkItemRecord[] = [];
568
+ const ordered: WorkItemRecord[] = [];
362
569
  const queue = [root.id];
363
570
  const seen = new Set<string>();
364
571
  while (queue.length > 0) {
@@ -366,14 +573,20 @@ export class WorkItemLedger {
366
573
  if (!currentId || seen.has(currentId)) continue;
367
574
  seen.add(currentId);
368
575
  const item = this.require(currentId);
369
- item.state = currentId === root.id ? "stale" : "invalidated";
576
+ if (item.acceptanceState === "accepted") {
577
+ throw new Error(`WorkItem ${item.id} has terminal accepted evidence`);
578
+ }
579
+ ordered.push(item);
580
+ queue.push(...item.dependents);
581
+ }
582
+ return ordered.map((item) => {
583
+ item.state = item.id === root.id ? "stale" : "invalidated";
584
+ if (item.acceptanceRequired) item.acceptanceState = "rejected";
370
585
  item.taskGeneration++;
371
586
  item.invalidationReasons.push(`${root.id}:${normalizedReason}`);
372
587
  item.generation = ++this.generation;
373
- affected.push(structuredClone(item));
374
- queue.push(...item.dependents);
375
- }
376
- return affected;
588
+ return structuredClone(item);
589
+ });
377
590
  }
378
591
 
379
592
  snapshot(): WorkItemLedgerSnapshot {
@@ -388,16 +601,18 @@ export class WorkItemLedger {
388
601
  }
389
602
 
390
603
  static restore(snapshot: WorkItemLedgerSnapshot): WorkItemLedger {
604
+ const storedVersion = (snapshot as { version?: unknown } | undefined)?.version;
391
605
  if (
392
606
  !snapshot ||
393
- (snapshot.version !== WORK_ITEM_LEDGER_VERSION &&
394
- (snapshot.version as string) !== LEGACY_WORK_ITEM_LEDGER_VERSION) ||
607
+ (storedVersion !== WORK_ITEM_LEDGER_VERSION &&
608
+ !LEGACY_WORK_ITEM_LEDGER_VERSIONS.has(String(storedVersion))) ||
395
609
  !Number.isSafeInteger(snapshot.generation) ||
396
610
  snapshot.generation < 0
397
611
  ) {
398
612
  throw new Error("Unsupported or malformed WorkItem ledger snapshot");
399
613
  }
400
- const isLegacySnapshot = (snapshot.version as string) === LEGACY_WORK_ITEM_LEDGER_VERSION;
614
+ const isLegacySnapshot = storedVersion !== WORK_ITEM_LEDGER_VERSION;
615
+ const isV1Snapshot = storedVersion === "pi-subagents:work-ledger:v1";
401
616
  if (
402
617
  !Array.isArray(snapshot.items) ||
403
618
  snapshot.items.length < 1 ||
@@ -405,10 +620,13 @@ export class WorkItemLedger {
405
620
  ) {
406
621
  throw new Error("Malformed WorkItem ledger items");
407
622
  }
408
- for (const item of snapshot.items) validateStoredRecord(item, snapshot.generation);
623
+ const storedItems = snapshot.items.map((item) =>
624
+ isLegacySnapshot ? normalizeLegacyAcceptance(item, isV1Snapshot) : item,
625
+ );
626
+ for (const item of storedItems) validateStoredRecord(item, snapshot.generation);
409
627
  const ledger = WorkItemLedger.create({
410
628
  workflowId: snapshot.workflowId,
411
- items: snapshot.items.map((item) => ({
629
+ items: storedItems.map((item) => ({
412
630
  id: item.id,
413
631
  objective: item.objective,
414
632
  dependencies: item.dependencies,
@@ -422,16 +640,25 @@ export class WorkItemLedger {
422
640
  writePaths: item.writePaths,
423
641
  ownershipKeys: item.ownershipKeys,
424
642
  acceptanceCriteria: item.acceptanceCriteria,
643
+ requiredEvidence: item.requiredEvidence,
425
644
  integrationOwner: item.integrationOwner,
426
645
  verifierFor: item.verifierFor,
427
646
  dependencyPolicy: item.dependencyPolicy,
647
+ acceptanceRequired: item.acceptanceRequired,
648
+ maxReworkCycles: item.maxReworkCycles,
428
649
  })),
429
650
  });
430
651
  ledger.generation = snapshot.generation;
431
- for (const stored of snapshot.items) {
652
+ for (const stored of storedItems) {
432
653
  const item = ledger.require(stored.id);
654
+ const interruptedAcceptance =
655
+ stored.acceptanceRequired &&
656
+ stored.state === "completed" &&
657
+ stored.acceptanceState === "pending";
433
658
  item.state =
434
- stored.state === "running" || stored.state === "awaiting-verification"
659
+ stored.state === "running" ||
660
+ stored.state === "awaiting-verification" ||
661
+ interruptedAcceptance
435
662
  ? "interrupted"
436
663
  : stored.state;
437
664
  item.generation = stored.generation;
@@ -443,25 +670,38 @@ export class WorkItemLedger {
443
670
  stored.artifacts,
444
671
  stored.id,
445
672
  stored.generation,
446
- !isLegacySnapshot,
673
+ !isV1Snapshot,
447
674
  );
448
675
  item.artifactHistory = normalizeStoredArtifacts(
449
676
  stored.artifactHistory ?? [],
450
677
  stored.id,
451
678
  stored.generation,
452
- !isLegacySnapshot,
679
+ !isV1Snapshot,
453
680
  );
454
- item.verificationAccepted = !isLegacySnapshot && stored.verificationAccepted;
455
- item.stagedTreeIdentity =
456
- !isLegacySnapshot && stored.stagedTreeIdentity
457
- ? structuredClone(stored.stagedTreeIdentity)
458
- : undefined;
681
+ item.acceptanceStateVersion = WORK_ITEM_ACCEPTANCE_VERSION;
682
+ item.acceptanceRequired = stored.acceptanceRequired;
683
+ item.acceptanceState = interruptedAcceptance ? "rejected" : stored.acceptanceState;
684
+ item.reworkCount = stored.reworkCount;
685
+ item.maxReworkCycles = stored.maxReworkCycles;
686
+ item.verificationAccepted = isV1Snapshot ? false : stored.verificationAccepted;
687
+ item.stagedTreeIdentity = stored.stagedTreeIdentity
688
+ ? structuredClone(stored.stagedTreeIdentity)
689
+ : undefined;
459
690
  item.verificationReceipt =
460
- !isLegacySnapshot && stored.verificationReceipt
691
+ !isV1Snapshot && stored.verificationReceipt
461
692
  ? structuredClone(stored.verificationReceipt)
462
693
  : undefined;
694
+ item.acceptanceReceipt = stored.acceptanceReceipt
695
+ ? structuredClone(stored.acceptanceReceipt)
696
+ : undefined;
697
+ item.acceptanceReceiptHistory = stored.acceptanceReceiptHistory.map((receipt) =>
698
+ structuredClone(receipt),
699
+ );
700
+ item.submission = stored.submission ? structuredClone(stored.submission) : undefined;
463
701
  item.invalidationReasons = [...stored.invalidationReasons];
464
- item.outcomeReason = stored.outcomeReason;
702
+ item.outcomeReason = interruptedAcceptance
703
+ ? "verification-interrupted"
704
+ : stored.outcomeReason;
465
705
  }
466
706
  ledger.validateRestoredVerificationLinks();
467
707
  return ledger;
@@ -470,16 +710,32 @@ export class WorkItemLedger {
470
710
  private validateRestoredVerificationLinks(): void {
471
711
  for (const target of this.items.values()) {
472
712
  const receipt = target.verificationReceipt;
473
- if (!receipt) continue;
474
- const verifier = this.items.get(receipt.verifierTaskId);
713
+ if (receipt) {
714
+ const verifier = this.items.get(receipt.verifierTaskId);
715
+ if (
716
+ !verifier ||
717
+ verifier.verifierFor !== target.id ||
718
+ verifier.state !== "completed" ||
719
+ verifier.taskGeneration !== receipt.verifierTaskGeneration ||
720
+ verifier.acceptedExecutionPlanId !== receipt.verifierExecutionPlanId
721
+ ) {
722
+ throw new Error(`Malformed stored WorkItem verification link for ${target.id}`);
723
+ }
724
+ }
725
+ const acceptanceReceipt = target.acceptanceReceipt;
726
+ if (!acceptanceReceipt || target.acceptanceState === "pending") continue;
727
+ const verifier = this.items.get(acceptanceReceipt.verifierTaskId);
475
728
  if (
476
729
  !verifier ||
477
730
  verifier.verifierFor !== target.id ||
478
731
  verifier.state !== "completed" ||
479
- verifier.taskGeneration !== receipt.verifierTaskGeneration ||
480
- verifier.acceptedExecutionPlanId !== receipt.verifierExecutionPlanId
732
+ verifier.taskGeneration !== acceptanceReceipt.verifierTaskGeneration ||
733
+ verifier.acceptedExecutionPlanId !== acceptanceReceipt.verifierExecutionPlanId ||
734
+ acceptanceReceipt.targetTaskId !== target.id ||
735
+ acceptanceReceipt.targetTaskGeneration !== target.taskGeneration ||
736
+ acceptanceReceipt.targetExecutionPlanId !== target.acceptedExecutionPlanId
481
737
  ) {
482
- throw new Error(`Malformed stored WorkItem verification link for ${target.id}`);
738
+ throw new Error(`Malformed stored WorkItem acceptance link for ${target.id}`);
483
739
  }
484
740
  }
485
741
  }
@@ -517,13 +773,23 @@ export class WorkItemLedger {
517
773
  definition.acceptanceCriteria ?? [],
518
774
  "acceptance criterion",
519
775
  ),
776
+ requiredEvidence: uniqueBounded(definition.requiredEvidence ?? [], "required evidence"),
520
777
  integrationOwner: definition.integrationOwner === true,
521
778
  verifierFor: definition.verifierFor,
522
779
  dependencyPolicy: definition.dependencyPolicy ?? "completed",
780
+ acceptanceStateVersion: WORK_ITEM_ACCEPTANCE_VERSION,
781
+ acceptanceRequired: definition.acceptanceRequired === true,
782
+ acceptanceState: definition.acceptanceRequired === true ? "pending" : "not-required",
783
+ reworkCount: 0,
784
+ maxReworkCycles: definition.maxReworkCycles === 0 ? 0 : 1,
523
785
  verificationAccepted: false,
524
786
  stagedTreeIdentity: undefined,
525
787
  verificationReceipt: undefined,
788
+ acceptanceReceipt: undefined,
789
+ acceptanceReceiptHistory: [],
790
+ submission: undefined,
526
791
  invalidationReasons: [],
792
+ outcomeReason: undefined,
527
793
  });
528
794
  }
529
795
 
@@ -532,6 +798,9 @@ export class WorkItemLedger {
532
798
  if (integrationOwners.length > 1)
533
799
  throw new Error("Workflow can have only one integration owner");
534
800
  for (const item of this.items.values()) {
801
+ if (item.acceptanceRequired && !item.integrationOwner) {
802
+ throw new Error(`WorkItem ${item.id} requires acceptance but is not integration owner`);
803
+ }
535
804
  for (const dependency of item.dependencies) {
536
805
  if (dependency === item.id) throw new Error(`WorkItem ${item.id} has a self cycle`);
537
806
  const parent = this.items.get(dependency);
@@ -564,20 +833,26 @@ export class WorkItemLedger {
564
833
  for (const item of this.items.values()) {
565
834
  if (item.state !== "pending") continue;
566
835
  const dependencies = item.dependencies.map((id) => this.require(id));
836
+ const acceptedDependency = (dependency: WorkItemRecord) =>
837
+ dependency.state === "completed" &&
838
+ (!dependency.acceptanceRequired || dependency.acceptanceState === "accepted");
567
839
  const dependenciesReady = item.verifierFor
568
840
  ? dependencies.every((dependency) =>
569
841
  dependency.id === item.verifierFor
570
- ? dependency.state === "awaiting-verification"
571
- : dependency.state === "completed",
842
+ ? dependency.acceptanceRequired
843
+ ? dependency.state === "completed" && dependency.acceptanceState === "pending"
844
+ : dependency.state === "awaiting-verification"
845
+ : acceptedDependency(dependency),
572
846
  )
573
847
  : item.dependencyPolicy === "settled"
574
848
  ? dependencies.every(
575
849
  (dependency) =>
850
+ (!dependency.acceptanceRequired || dependency.acceptanceState === "accepted") &&
576
851
  !["pending", "ready", "running", "awaiting-verification"].includes(
577
852
  dependency.state,
578
853
  ),
579
854
  )
580
- : dependencies.every((dependency) => dependency.state === "completed");
855
+ : dependencies.every(acceptedDependency);
581
856
  if (!dependenciesReady) continue;
582
857
  const available = new Map<string, WorkArtifactReference>();
583
858
  for (const dependency of dependencies) {
@@ -599,6 +874,39 @@ export class WorkItemLedger {
599
874
  }
600
875
  }
601
876
 
877
+ private resetReworkDependents(targetId: string, excludedId: string): void {
878
+ const target = this.require(targetId);
879
+ const reworkReason = `${targetId}:verification-rework`;
880
+ const queue = target.dependents.filter((id) => id !== excludedId);
881
+ const seen = new Set<string>();
882
+ while (queue.length > 0) {
883
+ const currentId = queue.shift();
884
+ if (!currentId || seen.has(currentId)) continue;
885
+ seen.add(currentId);
886
+ const current = this.require(currentId);
887
+ queue.push(...current.dependents.filter((id) => id !== excludedId));
888
+ if (current.state !== "invalidated" || current.invalidationReasons.at(-1) !== reworkReason) {
889
+ continue;
890
+ }
891
+ current.artifactHistory.push(
892
+ ...current.artifacts.map((artifact) => structuredClone(artifact)),
893
+ );
894
+ current.artifacts = [];
895
+ current.inputArtifactVersions = {};
896
+ current.state = "pending";
897
+ current.assignedAgentId = undefined;
898
+ current.acceptedExecutionPlanId = undefined;
899
+ current.outcomeReason = undefined;
900
+ current.verificationAccepted = false;
901
+ current.acceptanceState = current.acceptanceRequired ? "pending" : "not-required";
902
+ current.stagedTreeIdentity = undefined;
903
+ current.submission = undefined;
904
+ current.verificationReceipt = undefined;
905
+ current.acceptanceReceipt = undefined;
906
+ current.generation = ++this.generation;
907
+ }
908
+ }
909
+
602
910
  private invalidateDependents(
603
911
  targetId: string,
604
912
  excludedId: string,
@@ -631,7 +939,10 @@ export class WorkItemLedger {
631
939
  }
632
940
 
633
941
  private assertMutable(item: WorkItemRecord): void {
634
- if (TERMINAL_STATES.has(item.state)) {
942
+ if (
943
+ TERMINAL_STATES.has(item.state) &&
944
+ !(item.state === "completed" && item.acceptanceState === "pending")
945
+ ) {
635
946
  throw new Error(`WorkItem ${item.id} is terminal (${item.state})`);
636
947
  }
637
948
  }
@@ -657,10 +968,108 @@ function assertReceiptMatches(
657
968
  }
658
969
  }
659
970
 
971
+ function assertVerifiedReceiptMatches(
972
+ target: WorkItemRecord,
973
+ verifier: WorkItemRecord,
974
+ verifierExecutionPlanId: string,
975
+ receipt: VerificationReceipt,
976
+ ): void {
977
+ if (
978
+ !target.acceptanceRequired ||
979
+ target.acceptanceState !== "pending" ||
980
+ target.state !== "completed" ||
981
+ !target.submission ||
982
+ receipt.targetTaskId !== target.id ||
983
+ receipt.targetTaskGeneration !== target.taskGeneration ||
984
+ receipt.targetExecutionPlanId !== target.acceptedExecutionPlanId ||
985
+ receipt.verifierTaskId !== verifier.id ||
986
+ receipt.verifierTaskGeneration !== verifier.taskGeneration ||
987
+ receipt.verifierExecutionPlanId !== verifierExecutionPlanId ||
988
+ receipt.verifierAgent !== verifier.assignedAgentId?.replace(/^agent:/u, "") ||
989
+ !sameWorkflowTreeIdentity(receipt.beforeTreeIdentity, target.submission.treeIdentity) ||
990
+ !sameWorkflowTreeIdentity(receipt.afterTreeIdentity, target.submission.treeIdentity) ||
991
+ receipt.baseRepositoryGeneration !== target.submission.baseRepositoryGeneration ||
992
+ receipt.patchDigest !== target.submission.patchDigest ||
993
+ JSON.stringify(receipt.changedPaths) !== JSON.stringify(target.submission.changedPaths) ||
994
+ JSON.stringify(receipt.allowedScopes) !== JSON.stringify(target.writePaths) ||
995
+ JSON.stringify(receipt.dependencyVersions) !== JSON.stringify(target.inputArtifactVersions) ||
996
+ JSON.stringify(receipt.readSetVersions) !== JSON.stringify(target.submission.fileVersions) ||
997
+ JSON.stringify(receipt.acceptanceCriteria) !== JSON.stringify(target.acceptanceCriteria) ||
998
+ JSON.stringify(receipt.requiredEvidenceIds) !== JSON.stringify(target.requiredEvidence)
999
+ ) {
1000
+ throw new Error("WorkItem verification receipt has stale or mismatched managed identity");
1001
+ }
1002
+ }
1003
+
660
1004
  function validatePlanId(value: string, label: string): void {
661
1005
  if (!/^[a-f0-9]{64}$/u.test(value)) throw new Error(`Invalid ${label}`);
662
1006
  }
663
1007
 
1008
+ function validateSubmission(value: VerificationSubmission): void {
1009
+ if (
1010
+ !isWorkflowTreeIdentity(value.treeIdentity) ||
1011
+ !/^[a-f0-9]{40,64}$/u.test(value.baseRepositoryGeneration) ||
1012
+ !/^[a-f0-9]{64}$/u.test(value.patchDigest) ||
1013
+ !Array.isArray(value.changedPaths) ||
1014
+ value.changedPaths.length > MAX_LIST_ITEMS ||
1015
+ value.changedPaths.some(
1016
+ (item) =>
1017
+ typeof item !== "string" ||
1018
+ !item ||
1019
+ item.length > 4096 ||
1020
+ item.startsWith("/") ||
1021
+ item.startsWith("\\") ||
1022
+ item.split(/[\\/]/u).includes(".."),
1023
+ ) ||
1024
+ new Set(value.changedPaths).size !== value.changedPaths.length ||
1025
+ value.changedPaths.reduce((total, item) => total + Buffer.byteLength(item, "utf8"), 0) >
1026
+ 8 * 1024 ||
1027
+ JSON.stringify([...value.changedPaths].sort((left, right) => left.localeCompare(right))) !==
1028
+ JSON.stringify(value.changedPaths) ||
1029
+ !value.fileVersions ||
1030
+ typeof value.fileVersions !== "object" ||
1031
+ Array.isArray(value.fileVersions) ||
1032
+ Object.entries(value.fileVersions).length > MAX_LIST_ITEMS ||
1033
+ JSON.stringify(
1034
+ Object.keys(value.fileVersions).sort((left, right) => left.localeCompare(right)),
1035
+ ) !== JSON.stringify(value.changedPaths) ||
1036
+ Object.entries(value.fileVersions).some(
1037
+ ([key, digest]) =>
1038
+ !key ||
1039
+ key.length > 4096 ||
1040
+ typeof digest !== "string" ||
1041
+ (digest !== "deleted" && !/^[a-f0-9]{64}$/u.test(digest)),
1042
+ )
1043
+ ) {
1044
+ throw new Error("WorkItem received malformed verification submission metadata");
1045
+ }
1046
+ }
1047
+
1048
+ function validSubmission(value: VerificationSubmission): boolean {
1049
+ try {
1050
+ validateSubmission(value);
1051
+ return true;
1052
+ } catch {
1053
+ return false;
1054
+ }
1055
+ }
1056
+
1057
+ function normalizeLegacyAcceptance(item: WorkItemRecord, isV1: boolean): WorkItemRecord {
1058
+ return {
1059
+ ...structuredClone(item),
1060
+ requiredEvidence: [],
1061
+ acceptanceStateVersion: WORK_ITEM_ACCEPTANCE_VERSION,
1062
+ acceptanceRequired: false,
1063
+ acceptanceState: "not-required",
1064
+ reworkCount: 0,
1065
+ maxReworkCycles: 1,
1066
+ verificationAccepted: isV1 ? false : item.verificationAccepted,
1067
+ acceptanceReceipt: undefined,
1068
+ acceptanceReceiptHistory: [],
1069
+ submission: undefined,
1070
+ };
1071
+ }
1072
+
664
1073
  function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): void {
665
1074
  const states: WorkItemState[] = [
666
1075
  "pending",
@@ -702,6 +1111,17 @@ function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): v
702
1111
  item.selectedAgentName.length === 0 ||
703
1112
  item.selectedAgentName.length > MAX_IDENTIFIER_LENGTH ||
704
1113
  item.selectedAgentName.trim() !== item.selectedAgentName)) ||
1114
+ item.acceptanceStateVersion !== WORK_ITEM_ACCEPTANCE_VERSION ||
1115
+ typeof item.acceptanceRequired !== "boolean" ||
1116
+ !["not-required", "pending", "accepted", "rework-requested", "rejected"].includes(
1117
+ item.acceptanceState,
1118
+ ) ||
1119
+ !Number.isSafeInteger(item.reworkCount) ||
1120
+ item.reworkCount < 0 ||
1121
+ item.reworkCount > 1 ||
1122
+ (item.maxReworkCycles !== 0 && item.maxReworkCycles !== 1) ||
1123
+ (item.acceptanceRequired && !item.integrationOwner) ||
1124
+ (!item.acceptanceRequired && item.acceptanceState !== "not-required") ||
705
1125
  typeof item.integrationOwner !== "boolean" ||
706
1126
  !(["completed", "settled"] as const).includes(item.dependencyPolicy) ||
707
1127
  !(["read-only", "idempotent", "mutating"] as const).includes(item.sideEffectPolicy) ||
@@ -716,6 +1136,7 @@ function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): v
716
1136
  item.writePaths,
717
1137
  item.ownershipKeys,
718
1138
  item.acceptanceCriteria,
1139
+ item.requiredEvidence,
719
1140
  ].every(
720
1141
  (values) =>
721
1142
  Array.isArray(values) &&
@@ -766,16 +1187,68 @@ function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): v
766
1187
  (item.verificationReceipt !== undefined &&
767
1188
  (!isWorkflowVerificationReceipt(item.verificationReceipt) ||
768
1189
  !storedVerificationMatchesItem(item, item.verificationReceipt))) ||
1190
+ (item.acceptanceReceipt !== undefined &&
1191
+ (!isVerificationReceipt(item.acceptanceReceipt) ||
1192
+ (item.acceptanceState !== "pending" &&
1193
+ !storedAcceptanceMatchesItem(item, item.acceptanceReceipt)))) ||
1194
+ !Array.isArray(item.acceptanceReceiptHistory) ||
1195
+ item.acceptanceReceiptHistory.length > 2 ||
1196
+ !item.acceptanceReceiptHistory.every(isVerificationReceipt) ||
1197
+ (item.acceptanceReceipt !== undefined &&
1198
+ JSON.stringify(item.acceptanceReceiptHistory.at(-1)) !==
1199
+ JSON.stringify(item.acceptanceReceipt)) ||
1200
+ item.reworkCount !==
1201
+ Math.min(
1202
+ item.maxReworkCycles,
1203
+ item.acceptanceReceiptHistory.filter((receipt) => receipt.decision === "rework").length,
1204
+ ) ||
1205
+ (item.submission !== undefined && !validSubmission(item.submission)) ||
1206
+ (item.acceptanceRequired &&
1207
+ item.state === "completed" &&
1208
+ item.acceptanceState === "pending" &&
1209
+ (!item.submission || !item.acceptedExecutionPlanId)) ||
1210
+ (item.acceptanceState === "accepted" &&
1211
+ (item.acceptanceReceipt?.decision !== "accept" || !item.verificationAccepted)) ||
1212
+ (item.acceptanceState === "rejected" &&
1213
+ (!["failed", "interrupted", "stale", "invalidated", "blocked", "needs-input"].includes(
1214
+ item.state,
1215
+ ) ||
1216
+ (item.acceptanceReceipt !== undefined &&
1217
+ item.acceptanceReceipt.decision !== "reject" &&
1218
+ item.acceptanceReceipt.decision !== "rework"))) ||
1219
+ (item.acceptanceState === "rework-requested" &&
1220
+ (item.state !== "blocked" || item.acceptanceReceipt?.decision !== "rework")) ||
769
1221
  (item.state === "awaiting-verification" &&
770
1222
  (!item.stagedTreeIdentity || !item.acceptedExecutionPlanId)) ||
771
1223
  (item.stagedTreeIdentity !== undefined &&
772
1224
  item.state === "completed" &&
1225
+ !item.acceptanceRequired &&
773
1226
  item.verificationReceipt === undefined)
774
1227
  ) {
775
1228
  throw new Error(`Malformed stored WorkItem ${String(item?.id ?? "unknown")}`);
776
1229
  }
777
1230
  }
778
1231
 
1232
+ function storedAcceptanceMatchesItem(item: WorkItemRecord, receipt: VerificationReceipt): boolean {
1233
+ return (
1234
+ item.acceptanceRequired &&
1235
+ item.submission !== undefined &&
1236
+ receipt.targetTaskId === item.id &&
1237
+ receipt.targetTaskGeneration === item.taskGeneration &&
1238
+ receipt.targetExecutionPlanId === item.acceptedExecutionPlanId &&
1239
+ sameWorkflowTreeIdentity(receipt.beforeTreeIdentity, item.submission.treeIdentity) &&
1240
+ sameWorkflowTreeIdentity(receipt.afterTreeIdentity, item.submission.treeIdentity) &&
1241
+ receipt.baseRepositoryGeneration === item.submission.baseRepositoryGeneration &&
1242
+ receipt.patchDigest === item.submission.patchDigest &&
1243
+ JSON.stringify(receipt.changedPaths) === JSON.stringify(item.submission.changedPaths) &&
1244
+ JSON.stringify(receipt.allowedScopes) === JSON.stringify(item.writePaths) &&
1245
+ JSON.stringify(receipt.dependencyVersions) === JSON.stringify(item.inputArtifactVersions) &&
1246
+ JSON.stringify(receipt.readSetVersions) === JSON.stringify(item.submission.fileVersions) &&
1247
+ JSON.stringify(receipt.acceptanceCriteria) === JSON.stringify(item.acceptanceCriteria) &&
1248
+ JSON.stringify(receipt.requiredEvidenceIds) === JSON.stringify(item.requiredEvidence)
1249
+ );
1250
+ }
1251
+
779
1252
  function storedVerificationMatchesItem(
780
1253
  item: WorkItemRecord,
781
1254
  receipt: WorkflowVerificationReceipt,