@narumitw/pi-subagents 0.51.0 → 0.52.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.
@@ -6,6 +6,14 @@ export interface VerificationRiskInput {
6
6
  requiredCapabilities: string[];
7
7
  }
8
8
 
9
+ export interface WorkflowVerificationTaskProjection {
10
+ id: string;
11
+ agent: string;
12
+ dependsOn?: readonly string[];
13
+ verifierFor?: string;
14
+ resultFormat?: string;
15
+ }
16
+
9
17
  export function requiresIndependentVerification(input: VerificationRiskInput): boolean {
10
18
  return (
11
19
  input.contract?.admission?.verificationRequired === true ||
@@ -15,3 +23,45 @@ export function requiresIndependentVerification(input: VerificationRiskInput): b
15
23
  (input.integrationOwner && input.contract?.sideEffectPolicy !== "read-only")
16
24
  );
17
25
  }
26
+
27
+ export function validateWorkflowVerificationGraph(
28
+ tasks: readonly WorkflowVerificationTaskProjection[],
29
+ requiredTargetIds: ReadonlySet<string>,
30
+ ): void {
31
+ const byId = new Map(tasks.map((task) => [task.id, task]));
32
+ const verifierByTarget = new Map<string, WorkflowVerificationTaskProjection[]>();
33
+ for (const task of tasks) {
34
+ if (!task.verifierFor) continue;
35
+ const target = byId.get(task.verifierFor);
36
+ if (!target) throw new Error(`Workflow verifier ${task.id} targets a missing task`);
37
+ if (target.verifierFor) {
38
+ throw new Error(`Workflow verifier ${task.id} cannot verify another verifier`);
39
+ }
40
+ if (target.resultFormat !== "structured-v2") {
41
+ throw new Error(`Workflow verification target ${target.id} must request structured-v2`);
42
+ }
43
+ if (task.dependsOn?.length !== 1 || task.dependsOn[0] !== target.id) {
44
+ throw new Error(`Workflow verifier ${task.id} must depend directly and only on ${target.id}`);
45
+ }
46
+ if (task.agent === target.agent) {
47
+ throw new Error(`Workflow verifier ${task.id} must use a distinct agent`);
48
+ }
49
+ if (task.resultFormat !== "structured-v2") {
50
+ throw new Error(`Workflow verifier ${task.id} must request structured-v2`);
51
+ }
52
+ const entries = verifierByTarget.get(target.id) ?? [];
53
+ entries.push(task);
54
+ verifierByTarget.set(target.id, entries);
55
+ }
56
+ for (const targetId of requiredTargetIds) {
57
+ const verifiers = verifierByTarget.get(targetId) ?? [];
58
+ if (verifiers.length !== 1) {
59
+ throw new Error(`Workflow task ${targetId} requires exactly one independent verifier`);
60
+ }
61
+ }
62
+ for (const [targetId, verifiers] of verifierByTarget) {
63
+ if (verifiers.length !== 1) {
64
+ throw new Error(`Workflow task ${targetId} must have exactly one verifier`);
65
+ }
66
+ }
67
+ }
@@ -3,13 +3,24 @@ import {
3
3
  type ManagedIntegrationExpectation,
4
4
  verifyManagedIntegration,
5
5
  } from "./integration-controller.js";
6
+ import {
7
+ isWorkflowTreeIdentity,
8
+ sameWorkflowTreeIdentity,
9
+ type WorkflowTreeIdentity,
10
+ } from "./workflow-tree-identity.js";
11
+ import {
12
+ isWorkflowVerificationReceipt,
13
+ type WorkflowVerificationReceipt,
14
+ } from "./workflow-verification.js";
6
15
 
7
- export const WORK_ITEM_LEDGER_VERSION = "pi-subagents:work-ledger:v1" as const;
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;
8
18
 
9
19
  export type WorkItemState =
10
20
  | "pending"
11
21
  | "ready"
12
22
  | "running"
23
+ | "awaiting-verification"
13
24
  | "blocked"
14
25
  | "needs-input"
15
26
  | "completed"
@@ -74,6 +85,8 @@ export interface WorkItemRecord {
74
85
  verifierFor?: string;
75
86
  dependencyPolicy: "completed" | "settled";
76
87
  verificationAccepted: boolean;
88
+ stagedTreeIdentity?: WorkflowTreeIdentity;
89
+ verificationReceipt?: WorkflowVerificationReceipt;
77
90
  invalidationReasons: string[];
78
91
  outcomeReason?: string;
79
92
  }
@@ -94,7 +107,17 @@ export interface CompleteWorkItemInput {
94
107
  taskGeneration: number;
95
108
  executionPlanId?: string;
96
109
  artifacts?: Array<Omit<WorkArtifactReference, "producerTaskId" | "generation">>;
97
- verificationAccepted?: boolean;
110
+ }
111
+
112
+ export interface StageWorkItemVerificationInput extends CompleteWorkItemInput {
113
+ executionPlanId: string;
114
+ treeIdentity: WorkflowTreeIdentity;
115
+ }
116
+
117
+ export interface CompleteWorkItemVerificationInput {
118
+ taskGeneration: number;
119
+ executionPlanId: string;
120
+ receipt: WorkflowVerificationReceipt;
98
121
  }
99
122
 
100
123
  const MAX_ITEMS = 64;
@@ -165,18 +188,107 @@ export class WorkItemLedger {
165
188
  item.acceptedExecutionPlanId = input.executionPlanId?.slice(0, 256);
166
189
  item.artifactHistory.push(...item.artifacts.map((artifact) => structuredClone(artifact)));
167
190
  item.artifacts = normalizeArtifacts(input.artifacts ?? [], id, this.generation + 1);
168
- item.verificationAccepted = input.verificationAccepted === true;
191
+ item.verificationAccepted = false;
192
+ item.stagedTreeIdentity = undefined;
193
+ item.verificationReceipt = undefined;
169
194
  item.state = "completed";
170
195
  item.generation = ++this.generation;
171
- if (item.verifierFor && item.verificationAccepted) {
172
- const verified = this.require(item.verifierFor);
173
- verified.verificationAccepted = true;
174
- verified.generation = this.generation;
196
+ this.refreshReadyState();
197
+ return structuredClone(item);
198
+ }
199
+
200
+ stageForVerification(id: string, input: StageWorkItemVerificationInput): WorkItemRecord {
201
+ const item = this.require(id);
202
+ this.assertMutable(item);
203
+ if (item.state !== "running") {
204
+ throw new Error(`WorkItem ${id} cannot stage verification while ${item.state}`);
205
+ }
206
+ if (input.taskGeneration !== item.taskGeneration) {
207
+ throw new Error(`WorkItem ${id} rejected a stale task generation`);
208
+ }
209
+ validatePlanId(input.executionPlanId, "staged execution plan");
210
+ if (!isWorkflowTreeIdentity(input.treeIdentity)) {
211
+ throw new Error(`WorkItem ${id} received an invalid staged tree identity`);
175
212
  }
213
+ item.acceptedExecutionPlanId = input.executionPlanId;
214
+ item.artifactHistory.push(...item.artifacts.map((artifact) => structuredClone(artifact)));
215
+ item.artifacts = normalizeArtifacts(input.artifacts ?? [], id, this.generation + 1);
216
+ item.verificationAccepted = false;
217
+ item.stagedTreeIdentity = structuredClone(input.treeIdentity);
218
+ item.verificationReceipt = undefined;
219
+ item.state = "awaiting-verification";
220
+ item.generation = ++this.generation;
176
221
  this.refreshReadyState();
177
222
  return structuredClone(item);
178
223
  }
179
224
 
225
+ completeVerification(
226
+ verifierId: string,
227
+ input: CompleteWorkItemVerificationInput,
228
+ ): { target: WorkItemRecord; verifier: WorkItemRecord } {
229
+ const verifier = this.require(verifierId);
230
+ this.assertMutable(verifier);
231
+ if (verifier.state !== "running" || !verifier.verifierFor) {
232
+ throw new Error(`WorkItem ${verifierId} is not a running verifier`);
233
+ }
234
+ if (input.taskGeneration !== verifier.taskGeneration) {
235
+ throw new Error(`WorkItem ${verifierId} rejected a stale verifier generation`);
236
+ }
237
+ validatePlanId(input.executionPlanId, "verifier execution plan");
238
+ if (!isWorkflowVerificationReceipt(input.receipt)) {
239
+ throw new Error(`WorkItem ${verifierId} received an invalid verification receipt`);
240
+ }
241
+ const target = this.require(verifier.verifierFor);
242
+ if (target.state !== "awaiting-verification") {
243
+ throw new Error(`WorkItem ${target.id} is not awaiting verification`);
244
+ }
245
+ assertReceiptMatches(target, verifier, input.executionPlanId, input.receipt);
246
+ verifier.acceptedExecutionPlanId = input.executionPlanId;
247
+ verifier.verificationReceipt = undefined;
248
+ verifier.verificationAccepted = false;
249
+ verifier.state = "completed";
250
+ verifier.generation = ++this.generation;
251
+ target.verificationReceipt = structuredClone(input.receipt);
252
+ target.verificationAccepted = input.receipt.decision === "accept";
253
+ target.outcomeReason =
254
+ input.receipt.decision === "accept"
255
+ ? undefined
256
+ : input.receipt.decision === "rework"
257
+ ? "verification-rework"
258
+ : "verification-rejected";
259
+ if (input.receipt.decision === "accept") {
260
+ target.artifacts = target.artifacts.map((artifact) => ({ ...artifact, verified: true }));
261
+ target.state = "completed";
262
+ } else {
263
+ target.state = input.receipt.decision === "rework" ? "blocked" : "failed";
264
+ this.invalidateDependents(target.id, verifier.id, target.outcomeReason);
265
+ }
266
+ target.generation = ++this.generation;
267
+ this.refreshReadyState();
268
+ return { target: structuredClone(target), verifier: structuredClone(verifier) };
269
+ }
270
+
271
+ failVerification(verifierId: string, reason: string): WorkItemRecord[] {
272
+ const verifier = this.require(verifierId);
273
+ if (!verifier.verifierFor) throw new Error(`WorkItem ${verifierId} is not a verifier`);
274
+ const target = this.require(verifier.verifierFor);
275
+ const boundedReason = bounded(reason, MAX_TEXT_LENGTH);
276
+ if (!boundedReason) throw new Error("Verification failure requires a reason");
277
+ if (!TERMINAL_STATES.has(verifier.state)) {
278
+ verifier.state = "failed";
279
+ verifier.outcomeReason = boundedReason;
280
+ verifier.generation = ++this.generation;
281
+ }
282
+ if (target.state === "awaiting-verification") {
283
+ target.state = "failed";
284
+ target.verificationAccepted = false;
285
+ target.outcomeReason = boundedReason;
286
+ target.generation = ++this.generation;
287
+ }
288
+ const invalidated = this.invalidateDependents(target.id, verifier.id, boundedReason);
289
+ return [structuredClone(target), structuredClone(verifier), ...invalidated];
290
+ }
291
+
180
292
  settle(
181
293
  id: string,
182
294
  state: "blocked" | "needs-input" | "failed" | "interrupted",
@@ -184,7 +296,12 @@ export class WorkItemLedger {
184
296
  ): WorkItemRecord {
185
297
  const item = this.require(id);
186
298
  this.assertMutable(item);
187
- if (item.state !== "running" && item.state !== "ready" && item.state !== "pending") {
299
+ if (
300
+ item.state !== "running" &&
301
+ item.state !== "awaiting-verification" &&
302
+ item.state !== "ready" &&
303
+ item.state !== "pending"
304
+ ) {
188
305
  throw new Error(`WorkItem ${id} cannot settle while ${item.state}`);
189
306
  }
190
307
  item.state = state;
@@ -230,6 +347,8 @@ export class WorkItemLedger {
230
347
  item.acceptedExecutionPlanId = undefined;
231
348
  item.outcomeReason = undefined;
232
349
  item.verificationAccepted = false;
350
+ item.stagedTreeIdentity = undefined;
351
+ item.verificationReceipt = undefined;
233
352
  item.generation = ++this.generation;
234
353
  this.refreshReadyState();
235
354
  return structuredClone(item);
@@ -271,12 +390,14 @@ export class WorkItemLedger {
271
390
  static restore(snapshot: WorkItemLedgerSnapshot): WorkItemLedger {
272
391
  if (
273
392
  !snapshot ||
274
- snapshot.version !== WORK_ITEM_LEDGER_VERSION ||
393
+ (snapshot.version !== WORK_ITEM_LEDGER_VERSION &&
394
+ (snapshot.version as string) !== LEGACY_WORK_ITEM_LEDGER_VERSION) ||
275
395
  !Number.isSafeInteger(snapshot.generation) ||
276
396
  snapshot.generation < 0
277
397
  ) {
278
398
  throw new Error("Unsupported or malformed WorkItem ledger snapshot");
279
399
  }
400
+ const isLegacySnapshot = (snapshot.version as string) === LEGACY_WORK_ITEM_LEDGER_VERSION;
280
401
  if (
281
402
  !Array.isArray(snapshot.items) ||
282
403
  snapshot.items.length < 1 ||
@@ -309,25 +430,60 @@ export class WorkItemLedger {
309
430
  ledger.generation = snapshot.generation;
310
431
  for (const stored of snapshot.items) {
311
432
  const item = ledger.require(stored.id);
312
- item.state = stored.state === "running" ? "interrupted" : stored.state;
433
+ item.state =
434
+ stored.state === "running" || stored.state === "awaiting-verification"
435
+ ? "interrupted"
436
+ : stored.state;
313
437
  item.generation = stored.generation;
314
438
  item.taskGeneration = stored.taskGeneration ?? 1;
315
439
  item.assignedAgentId = stored.assignedAgentId;
316
440
  item.acceptedExecutionPlanId = stored.acceptedExecutionPlanId;
317
441
  item.inputArtifactVersions = { ...stored.inputArtifactVersions };
318
- item.artifacts = normalizeStoredArtifacts(stored.artifacts, stored.id, stored.generation);
442
+ item.artifacts = normalizeStoredArtifacts(
443
+ stored.artifacts,
444
+ stored.id,
445
+ stored.generation,
446
+ !isLegacySnapshot,
447
+ );
319
448
  item.artifactHistory = normalizeStoredArtifacts(
320
449
  stored.artifactHistory ?? [],
321
450
  stored.id,
322
451
  stored.generation,
452
+ !isLegacySnapshot,
323
453
  );
324
- item.verificationAccepted = stored.verificationAccepted;
454
+ item.verificationAccepted = !isLegacySnapshot && stored.verificationAccepted;
455
+ item.stagedTreeIdentity =
456
+ !isLegacySnapshot && stored.stagedTreeIdentity
457
+ ? structuredClone(stored.stagedTreeIdentity)
458
+ : undefined;
459
+ item.verificationReceipt =
460
+ !isLegacySnapshot && stored.verificationReceipt
461
+ ? structuredClone(stored.verificationReceipt)
462
+ : undefined;
325
463
  item.invalidationReasons = [...stored.invalidationReasons];
326
464
  item.outcomeReason = stored.outcomeReason;
327
465
  }
466
+ ledger.validateRestoredVerificationLinks();
328
467
  return ledger;
329
468
  }
330
469
 
470
+ private validateRestoredVerificationLinks(): void {
471
+ for (const target of this.items.values()) {
472
+ const receipt = target.verificationReceipt;
473
+ if (!receipt) continue;
474
+ const verifier = this.items.get(receipt.verifierTaskId);
475
+ if (
476
+ !verifier ||
477
+ verifier.verifierFor !== target.id ||
478
+ verifier.state !== "completed" ||
479
+ verifier.taskGeneration !== receipt.verifierTaskGeneration ||
480
+ verifier.acceptedExecutionPlanId !== receipt.verifierExecutionPlanId
481
+ ) {
482
+ throw new Error(`Malformed stored WorkItem verification link for ${target.id}`);
483
+ }
484
+ }
485
+ }
486
+
331
487
  private addDefinition(definition: WorkItemDefinition): void {
332
488
  validateIdentifier(definition.id, "WorkItem id");
333
489
  if (this.items.has(definition.id)) throw new Error(`Duplicate WorkItem id ${definition.id}`);
@@ -365,6 +521,8 @@ export class WorkItemLedger {
365
521
  verifierFor: definition.verifierFor,
366
522
  dependencyPolicy: definition.dependencyPolicy ?? "completed",
367
523
  verificationAccepted: false,
524
+ stagedTreeIdentity: undefined,
525
+ verificationReceipt: undefined,
368
526
  invalidationReasons: [],
369
527
  });
370
528
  }
@@ -406,10 +564,18 @@ export class WorkItemLedger {
406
564
  for (const item of this.items.values()) {
407
565
  if (item.state !== "pending") continue;
408
566
  const dependencies = item.dependencies.map((id) => this.require(id));
409
- const dependenciesReady =
410
- item.dependencyPolicy === "settled"
567
+ const dependenciesReady = item.verifierFor
568
+ ? dependencies.every((dependency) =>
569
+ dependency.id === item.verifierFor
570
+ ? dependency.state === "awaiting-verification"
571
+ : dependency.state === "completed",
572
+ )
573
+ : item.dependencyPolicy === "settled"
411
574
  ? dependencies.every(
412
- (dependency) => !["pending", "ready", "running"].includes(dependency.state),
575
+ (dependency) =>
576
+ !["pending", "ready", "running", "awaiting-verification"].includes(
577
+ dependency.state,
578
+ ),
413
579
  )
414
580
  : dependencies.every((dependency) => dependency.state === "completed");
415
581
  if (!dependenciesReady) continue;
@@ -433,6 +599,31 @@ export class WorkItemLedger {
433
599
  }
434
600
  }
435
601
 
602
+ private invalidateDependents(
603
+ targetId: string,
604
+ excludedId: string,
605
+ reason: string | undefined,
606
+ ): WorkItemRecord[] {
607
+ const target = this.require(targetId);
608
+ const normalizedReason = bounded(reason ?? "verification-not-accepted", MAX_TEXT_LENGTH);
609
+ const queue = target.dependents.filter((id) => id !== excludedId);
610
+ const seen = new Set<string>();
611
+ const affected: WorkItemRecord[] = [];
612
+ while (queue.length > 0) {
613
+ const currentId = queue.shift();
614
+ if (!currentId || seen.has(currentId)) continue;
615
+ seen.add(currentId);
616
+ const current = this.require(currentId);
617
+ current.state = "invalidated";
618
+ current.taskGeneration++;
619
+ current.invalidationReasons.push(`${targetId}:${normalizedReason}`);
620
+ current.generation = ++this.generation;
621
+ affected.push(structuredClone(current));
622
+ queue.push(...current.dependents.filter((id) => id !== excludedId));
623
+ }
624
+ return affected;
625
+ }
626
+
436
627
  private require(id: string): WorkItemRecord {
437
628
  const item = this.items.get(id);
438
629
  if (!item) throw new Error(`Unknown WorkItem ${id}`);
@@ -446,11 +637,36 @@ export class WorkItemLedger {
446
637
  }
447
638
  }
448
639
 
640
+ function assertReceiptMatches(
641
+ target: WorkItemRecord,
642
+ verifier: WorkItemRecord,
643
+ verifierExecutionPlanId: string,
644
+ receipt: WorkflowVerificationReceipt,
645
+ ): void {
646
+ if (
647
+ receipt.targetTaskId !== target.id ||
648
+ receipt.targetTaskGeneration !== target.taskGeneration ||
649
+ receipt.targetExecutionPlanId !== target.acceptedExecutionPlanId ||
650
+ receipt.verifierTaskId !== verifier.id ||
651
+ receipt.verifierTaskGeneration !== verifier.taskGeneration ||
652
+ receipt.verifierExecutionPlanId !== verifierExecutionPlanId ||
653
+ !target.stagedTreeIdentity ||
654
+ !sameWorkflowTreeIdentity(receipt.treeIdentity, target.stagedTreeIdentity)
655
+ ) {
656
+ throw new Error("WorkItem verification receipt has stale or mismatched executor identity");
657
+ }
658
+ }
659
+
660
+ function validatePlanId(value: string, label: string): void {
661
+ if (!/^[a-f0-9]{64}$/u.test(value)) throw new Error(`Invalid ${label}`);
662
+ }
663
+
449
664
  function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): void {
450
665
  const states: WorkItemState[] = [
451
666
  "pending",
452
667
  "ready",
453
668
  "running",
669
+ "awaiting-verification",
454
670
  "blocked",
455
671
  "needs-input",
456
672
  "completed",
@@ -545,12 +761,41 @@ function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): v
545
761
  (typeof item.outcomeReason !== "string" ||
546
762
  item.outcomeReason.length > MAX_TEXT_LENGTH ||
547
763
  item.outcomeReason.trim() !== item.outcomeReason)) ||
548
- typeof item.verificationAccepted !== "boolean"
764
+ typeof item.verificationAccepted !== "boolean" ||
765
+ (item.stagedTreeIdentity !== undefined && !isWorkflowTreeIdentity(item.stagedTreeIdentity)) ||
766
+ (item.verificationReceipt !== undefined &&
767
+ (!isWorkflowVerificationReceipt(item.verificationReceipt) ||
768
+ !storedVerificationMatchesItem(item, item.verificationReceipt))) ||
769
+ (item.state === "awaiting-verification" &&
770
+ (!item.stagedTreeIdentity || !item.acceptedExecutionPlanId)) ||
771
+ (item.stagedTreeIdentity !== undefined &&
772
+ item.state === "completed" &&
773
+ item.verificationReceipt === undefined)
549
774
  ) {
550
775
  throw new Error(`Malformed stored WorkItem ${String(item?.id ?? "unknown")}`);
551
776
  }
552
777
  }
553
778
 
779
+ function storedVerificationMatchesItem(
780
+ item: WorkItemRecord,
781
+ receipt: WorkflowVerificationReceipt,
782
+ ): boolean {
783
+ return (
784
+ !item.verifierFor &&
785
+ receipt.targetTaskId === item.id &&
786
+ receipt.targetTaskGeneration === item.taskGeneration &&
787
+ receipt.targetExecutionPlanId === item.acceptedExecutionPlanId &&
788
+ item.stagedTreeIdentity !== undefined &&
789
+ sameWorkflowTreeIdentity(receipt.treeIdentity, item.stagedTreeIdentity) &&
790
+ item.verificationAccepted === (receipt.decision === "accept") &&
791
+ (receipt.decision === "accept"
792
+ ? item.state === "completed"
793
+ : receipt.decision === "rework"
794
+ ? item.state === "blocked"
795
+ : item.state === "failed")
796
+ );
797
+ }
798
+
554
799
  function validStoredArtifacts(
555
800
  values: WorkArtifactReference[],
556
801
  producerTaskId: string,
@@ -599,11 +844,15 @@ function normalizeStoredArtifacts(
599
844
  values: WorkArtifactReference[],
600
845
  defaultProducerTaskId: string,
601
846
  defaultGeneration: number,
847
+ preserveVerification: boolean,
602
848
  ): WorkArtifactReference[] {
603
849
  if (!validStoredArtifacts(values, defaultProducerTaskId, defaultGeneration)) {
604
850
  throw new Error(`Malformed stored artifacts for WorkItem ${defaultProducerTaskId}`);
605
851
  }
606
- return values.map((value) => structuredClone(value));
852
+ return values.map((value) => ({
853
+ ...structuredClone(value),
854
+ verified: preserveVerification && value.verified,
855
+ }));
607
856
  }
608
857
 
609
858
  function normalizeArtifacts(
@@ -627,7 +876,7 @@ function normalizeArtifacts(
627
876
  ...(value.digest ? { digest: bounded(value.digest, MAX_TEXT_LENGTH) } : {}),
628
877
  producerTaskId,
629
878
  generation,
630
- verified: value.verified === true,
879
+ verified: false,
631
880
  };
632
881
  });
633
882
  }
@@ -203,6 +203,11 @@ function sanitizeWorkflowSnapshot(snapshot: WorkItemLedgerSnapshot): WorkItemLed
203
203
  item.acceptanceCriteria = item.acceptanceCriteria.map(redact);
204
204
  item.invalidationReasons = item.invalidationReasons.map(redact);
205
205
  item.outcomeReason = item.outcomeReason ? redact(item.outcomeReason) : undefined;
206
+ if (item.verificationReceipt) {
207
+ item.verificationReceipt.summary = redact(item.verificationReceipt.summary);
208
+ item.verificationReceipt.evidence = item.verificationReceipt.evidence.map(redact);
209
+ item.verificationReceipt.limitations = item.verificationReceipt.limitations.map(redact);
210
+ }
206
211
  for (const artifact of [...item.artifacts, ...item.artifactHistory]) {
207
212
  artifact.kind = redact(artifact.kind);
208
213
  artifact.version = redact(artifact.version);
@@ -89,6 +89,18 @@ export function createBlockingWorkLedger(
89
89
  const hasExplicitIntegrationOwner = resolvedWorkflowTasks.some(
90
90
  (task) => task.integrationOwner === true,
91
91
  );
92
+ let defaultIntegrationOwnerIndex = -1;
93
+ if (!hasExplicitIntegrationOwner) {
94
+ for (let index = resolvedWorkflowTasks.length - 1; index >= 0; index--) {
95
+ if (resolvedWorkflowTasks[index]?.verifierFor === undefined) {
96
+ defaultIntegrationOwnerIndex = index;
97
+ break;
98
+ }
99
+ }
100
+ }
101
+ if (!hasExplicitIntegrationOwner && defaultIntegrationOwnerIndex < 0) {
102
+ throw new Error("Workflow has no non-verifier integration owner candidate");
103
+ }
92
104
  return WorkItemLedger.create({
93
105
  workflowId: params.workflow.id ?? "blocking-workflow",
94
106
  items: resolvedWorkflowTasks.map((task, index) =>
@@ -96,7 +108,7 @@ export function createBlockingWorkLedger(
96
108
  ...task,
97
109
  integrationOwner:
98
110
  task.integrationOwner ??
99
- (!hasExplicitIntegrationOwner && index === resolvedWorkflowTasks.length - 1),
111
+ (!hasExplicitIntegrationOwner && index === defaultIntegrationOwnerIndex),
100
112
  }),
101
113
  ),
102
114
  });