@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.
package/README.md CHANGED
@@ -413,11 +413,58 @@ Run an explicit dependency workflow:
413
413
  }
414
414
  ```
415
415
 
416
+ A verification-gated implementation declares one distinct verifier:
417
+
418
+ ```json
419
+ {
420
+ "workflow": {
421
+ "tasks": [
422
+ {
423
+ "id": "implementation",
424
+ "agent": "worker",
425
+ "task": "Implement the contracted change.",
426
+ "resultFormat": "structured-v2",
427
+ "contract": {
428
+ "version": "pi-subagents:delegation:v2",
429
+ "level": "full",
430
+ "taskId": "implementation",
431
+ "objective": "Implement the contracted change",
432
+ "admission": {
433
+ "contextPressure": "medium",
434
+ "independentWorkItems": 1,
435
+ "coupling": "dense",
436
+ "verificationRequired": true,
437
+ "verificationAvailable": true,
438
+ "budgetAllowsChildren": true,
439
+ "requirementsComplete": true
440
+ }
441
+ }
442
+ },
443
+ {
444
+ "id": "verification",
445
+ "agent": "reviewer",
446
+ "task": "Independently verify the staged result.",
447
+ "dependsOn": ["implementation"],
448
+ "verifierFor": "implementation",
449
+ "resultFormat": "structured-v2"
450
+ }
451
+ ]
452
+ }
453
+ }
454
+ ```
455
+
416
456
  Cycles, missing dependencies, conflicting integration owners, recursive workflow grandchildren, and unsafe retry or hedge policies fail before child launch.
417
457
  Workflow scheduling starts at most two mutating tasks concurrently, while declared read-only work may use the existing four-child ceiling.
418
458
  Set `workflow.honorAdmission: true` only when explicit contract admission metadata should be allowed to decline parent-owned or insufficient-evidence work before launch; admission never silently widens the requested architecture.
419
459
  Workflow result details include the final ledger, scheduling decisions, artifact versions, task generations, attempts, hedge use, accepted plan identity, and bounded capability-grant metadata.
420
- Explicit workflow transitions are also atomically persisted as mode-0600, private-text-redacted snapshots for current-session `list_workflows` and `get_workflow` inspection; in-flight tasks inspect as `interrupted`, and no prior side effect is automatically resumed.
460
+ A task that explicitly requires independent verification must have exactly one direct-dependent `verifierFor` task using a different agent, and both tasks must request `structured-v2`.
461
+ The producer stops in `awaiting-verification`, its own passing verification claims remain untrusted, and ordinary downstream tasks stay blocked until the executor records an accepted verifier receipt.
462
+ The verifier runs alone in a fresh subprocess context against one bounded Git-visible tree identity and must encode `verification-accepted`, `verification-rework`, or `verification-rejected` through the documented `structured-v2` status and reason fields.
463
+ Dirty-tree identity covers at most 1 MiB across separately framed staged and unstaged binary diffs plus bounded non-ignored untracked paths and bytes; submodules, unsupported states, and changing trees fail closed.
464
+ A rework or rejection preserves bounded evidence but does not replay the producer automatically.
465
+ This acceptance gate does not isolate operating-system effects and does not make shared-workspace mutation into manager-controlled patch integration.
466
+ Explicit workflow transitions are also atomically persisted as mode-0600, private-text-redacted snapshots for current-session `list_workflows` and `get_workflow` inspection; running and awaiting-verification tasks inspect as `interrupted`, and no prior side effect is automatically resumed.
467
+ When a v1 ledger is restored, legacy self-reported verification flags and artifact trust are cleared because they have no executor receipt.
421
468
 
422
469
  ## 🔁 Stateful agents
423
470
 
@@ -890,6 +937,8 @@ packages/pi-subagents/
890
937
  │ ├── execution-plan.ts # Executor-owned authority and resource resolution
891
938
  │ ├── work-item-ledger.ts # Persistent dependency and artifact state machine
892
939
  │ ├── work-item-persistence.ts # Atomic redacted workflow state and inspection
940
+ │ ├── workflow-verification.ts # Executor-owned independent-verifier receipts
941
+ │ ├── workflow-tree-identity.ts # Bounded exact Git-visible tree identities
893
942
  │ ├── integration-controller.ts # Fail-closed canonical integration admission
894
943
  │ ├── adaptive-scheduler.ts # Dependency, capacity, budget, and conflict scheduling
895
944
  │ ├── semantic-snapshot.ts # Privacy-safe continuation compatibility checks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "Pi extension for delegating work to specialized isolated subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,7 +9,8 @@ export type SchedulingReason =
9
9
  | "state-not-ready"
10
10
  | "budget-exhausted"
11
11
  | "capacity-exhausted"
12
- | "scope-conflict";
12
+ | "scope-conflict"
13
+ | "verification-barrier";
13
14
 
14
15
  export interface SchedulingDecisionItem {
15
16
  id: string;
@@ -57,6 +58,33 @@ export class AdaptiveScheduler {
57
58
  ),
58
59
  );
59
60
  const effectiveConcurrency = options.remainingBudgetMs > 0 ? availableSlots : 0;
61
+ const readyVerifier = ready.find((item) => item.verifierFor !== undefined);
62
+ if (readyVerifier) {
63
+ const verifierMayStart = effectiveConcurrency > 0 && options.activeCount === 0;
64
+ return {
65
+ policy: ADAPTIVE_SCHEDULER_POLICY,
66
+ workflowId: snapshot.workflowId,
67
+ workflowGeneration: snapshot.generation,
68
+ effectiveConcurrency: verifierMayStart ? 1 : 0,
69
+ selected: verifierMayStart ? [readyVerifier.id] : [],
70
+ decisions: snapshot.items
71
+ .map((item) => ({
72
+ id: item.id,
73
+ reason:
74
+ item.id === readyVerifier.id
75
+ ? verifierMayStart
76
+ ? ("selected" as const)
77
+ : ("capacity-exhausted" as const)
78
+ : item.state === "ready"
79
+ ? ("verification-barrier" as const)
80
+ : item.state === "pending"
81
+ ? ("dependency-not-ready" as const)
82
+ : ("state-not-ready" as const),
83
+ criticalPathDepth: depth.get(item.id) ?? 0,
84
+ }))
85
+ .sort((left, right) => left.id.localeCompare(right.id)),
86
+ };
87
+ }
60
88
  const selected: string[] = [];
61
89
  let mutatingCount = options.activeMutatingCount ?? 0;
62
90
  const maxMutatingConcurrency = options.maxMutatingConcurrency ?? 2;
package/src/execution.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  startSubagentStatus,
22
22
  } from "./blocking-status.js";
23
23
  import { issueCapabilityGrant } from "./capability-grant.js";
24
+ import { redactPrivateText } from "./context.js";
24
25
  import {
25
26
  assertDelegationTargetAllowed,
26
27
  type ResolvedSubagentTarget,
@@ -69,13 +70,21 @@ import {
69
70
  import { isRetryableResult, runHedgedAttempt, supervisionDelay } from "./supervision.js";
70
71
  import { TimeoutProgressJournal, TURN_TERMINATION_VERSION } from "./timeout-checkpoint.js";
71
72
  import type { TurnLimits } from "./turn-budget.js";
72
- import { requiresIndependentVerification } from "./verification-policy.js";
73
+ import {
74
+ requiresIndependentVerification,
75
+ validateWorkflowVerificationGraph,
76
+ } from "./verification-policy.js";
73
77
  import type { WorkItemLedger } from "./work-item-ledger.js";
74
78
  import {
75
79
  createSessionWorkItemPersistence,
76
80
  type WorkItemPersistence,
77
81
  } from "./work-item-persistence.js";
78
82
  import { createBlockingWorkLedger, resolveWorkflowTasks } from "./workflow-planning.js";
83
+ import { captureWorkflowTreeIdentity, sameWorkflowTreeIdentity } from "./workflow-tree-identity.js";
84
+ import {
85
+ createWorkflowVerificationReceipt,
86
+ workflowVerificationInstruction,
87
+ } from "./workflow-verification.js";
79
88
 
80
89
  export const FALLBACK_TIMEOUT_MS = 10 * 60 * 1000;
81
90
 
@@ -173,6 +182,7 @@ export async function executeSubagent(
173
182
  Number(hasSingle);
174
183
  let workLedger: WorkItemLedger | undefined;
175
184
  const workflowScheduling: ReturnType<AdaptiveScheduler["decide"]>[] = [];
185
+ const verificationTargetIds = new Set<string>();
176
186
 
177
187
  const makeDetails =
178
188
  (mode: "single" | "parallel" | "chain" | "workflow" | "panel") =>
@@ -191,6 +201,38 @@ export async function executeSubagent(
191
201
  metrics: calculateOrchestrationMetrics(workflow, metricResults),
192
202
  };
193
203
  };
204
+ const workflowFailureResult = (
205
+ agentName: string,
206
+ task: string,
207
+ reasonCode: string,
208
+ message: string,
209
+ thinkingLevel?: SubagentThinkingLevel,
210
+ ): SingleResult => ({
211
+ agent: agentName,
212
+ agentSource: agents.find((agent) => agent.name === agentName)?.source ?? "unknown",
213
+ task,
214
+ exitCode: 1,
215
+ messages: [],
216
+ stderr: message,
217
+ errorMessage: message,
218
+ usage: {
219
+ input: 0,
220
+ output: 0,
221
+ cacheRead: 0,
222
+ cacheWrite: 0,
223
+ cost: 0,
224
+ contextTokens: 0,
225
+ turns: 0,
226
+ },
227
+ thinkingLevel,
228
+ finalOutput: "",
229
+ outcome: {
230
+ status: "failed",
231
+ reasonCode,
232
+ recoveryActions: ["revalidate"],
233
+ retryable: false,
234
+ },
235
+ });
194
236
  const exhaustedResult = (
195
237
  agentName: string,
196
238
  task: string,
@@ -292,6 +334,7 @@ export async function executeSubagent(
292
334
  }
293
335
  if (hasWorkflow && params.workflow) {
294
336
  for (const task of resolvedWorkflowTasks) {
337
+ if (task.verifierFor) verificationTargetIds.add(task.verifierFor);
295
338
  const contract = normalizeDelegationContract(task.contract);
296
339
  if (params.workflow.honorAdmission) {
297
340
  const admission = contract?.admission;
@@ -322,17 +365,7 @@ export async function executeSubagent(
322
365
  requiredCapabilities: task.requiredCapabilities ?? [],
323
366
  })
324
367
  ) {
325
- const verifier = resolvedWorkflowTasks.find(
326
- (candidate) =>
327
- candidate.verifierFor === task.id &&
328
- candidate.dependsOn?.includes(task.id) &&
329
- candidate.agent !== task.agent,
330
- );
331
- if (!verifier) {
332
- throw new Error(
333
- `Workflow task ${task.id} requires a distinct dependent verifier before launch`,
334
- );
335
- }
368
+ verificationTargetIds.add(task.id);
336
369
  }
337
370
  if (!task.retryPolicy && !task.hedgeAfterMs) continue;
338
371
  const policy = contract?.sideEffectPolicy;
@@ -345,6 +378,13 @@ export async function executeSubagent(
345
378
  );
346
379
  }
347
380
  }
381
+ validateWorkflowVerificationGraph(
382
+ resolvedWorkflowTasks.map((task) => ({
383
+ ...task,
384
+ resultFormat: task.resultFormat ?? params.resultFormat,
385
+ })),
386
+ verificationTargetIds,
387
+ );
348
388
  }
349
389
  workLedger = createBlockingWorkLedger(params, resolvedWorkflowTasks, aggregator);
350
390
  let workflowPersistence: WorkItemPersistence | undefined;
@@ -512,6 +552,19 @@ export async function executeSubagent(
512
552
  );
513
553
  }
514
554
 
555
+ const artifactsFromResult = (result: SingleResult) => {
556
+ const structured =
557
+ result.structuredResult?.version === "pi-subagents:result:v2"
558
+ ? result.structuredResult
559
+ : undefined;
560
+ return (structured?.artifacts ?? []).map((artifact) => ({
561
+ id: artifact.id,
562
+ kind: artifact.kind,
563
+ version: artifact.version ?? artifact.digest ?? "unversioned",
564
+ digest: artifact.digest,
565
+ verified: false,
566
+ }));
567
+ };
515
568
  const startWorkItem = (id: string, agentName: string) => {
516
569
  if (workLedger?.get(id)?.state === "ready") {
517
570
  return workLedger.start(id, `agent:${agentName}`);
@@ -548,24 +601,10 @@ export async function executeSubagent(
548
601
  );
549
602
  return;
550
603
  }
551
- const structured =
552
- result.structuredResult?.version === "pi-subagents:result:v2"
553
- ? result.structuredResult
554
- : undefined;
555
604
  workLedger.complete(id, {
556
605
  taskGeneration,
557
606
  executionPlanId: result.executionPlan?.id,
558
- artifacts: (structured?.artifacts ?? []).map((artifact) => ({
559
- id: artifact.id,
560
- kind: artifact.kind,
561
- version: artifact.version ?? artifact.digest ?? "unversioned",
562
- digest: artifact.digest,
563
- verified:
564
- structured?.verification.some((verification) => verification.status === "passed") ??
565
- false,
566
- })),
567
- verificationAccepted:
568
- structured?.verification.some((verification) => verification.status === "passed") ?? false,
607
+ artifacts: artifactsFromResult(result),
569
608
  });
570
609
  };
571
610
 
@@ -659,7 +698,9 @@ export async function executeSubagent(
659
698
  const deadline = orchestrationDeadline;
660
699
  const cancelWorkflowGeneration = () => {
661
700
  for (const item of workLedger.snapshot().items) {
662
- if (item.state === "running") workLedger.invalidate(item.id, "parent-aborted");
701
+ if (item.state === "running" || item.state === "awaiting-verification") {
702
+ workLedger.invalidate(item.id, "parent-aborted");
703
+ }
663
704
  }
664
705
  };
665
706
  signal?.addEventListener("abort", cancelWorkflowGeneration, { once: true });
@@ -691,17 +732,73 @@ export async function executeSubagent(
691
732
  const dependencies = (task.dependsOn ?? [])
692
733
  .map((dependency) => resultsById.get(dependency))
693
734
  .filter((result): result is SingleResult => result !== undefined);
694
- const dependencyContext = dependencies.length
695
- ? `\n\nDependency results:\n${buildFanInContext(dependencies)}`
696
- : "";
735
+ const verifierDependency = task.verifierFor
736
+ ? resultsById.get(task.verifierFor)
737
+ : undefined;
738
+ const verifierStructuredResult =
739
+ verifierDependency?.structuredResult?.version === "pi-subagents:result:v2"
740
+ ? verifierDependency.structuredResult
741
+ : undefined;
742
+ const dependencyContext = task.verifierFor
743
+ ? verifierStructuredResult
744
+ ? `\n\nStaged target result:\n${redactPrivateText(
745
+ JSON.stringify(verifierStructuredResult),
746
+ )}`
747
+ : ""
748
+ : dependencies.length
749
+ ? `\n\nDependency results:\n${buildFanInContext(dependencies)}`
750
+ : "";
697
751
  const displayTask = task.task;
698
- const taskWithContext = truncateUtf8(
699
- `${task.task}${dependencyContext}`,
700
- DEFAULT_MAX_CONTEXT_BYTES,
701
- ).text;
702
- const prepared = prepareTask(taskWithContext, task);
703
752
  const target = workflowTargets[index];
704
753
  const thinkingLevel = resolveThinkingLevel(task.agent, task.thinkingLevel);
754
+ let verifierTreeIdentity:
755
+ | Awaited<ReturnType<typeof captureWorkflowTreeIdentity>>
756
+ | undefined;
757
+ let verifierPreflightError: string | undefined;
758
+ let verifierPreflightCode: string | undefined;
759
+ if (task.verifierFor) {
760
+ const staged = workLedger.get(task.verifierFor);
761
+ if (!staged?.stagedTreeIdentity) {
762
+ verifierPreflightCode = "verification-tree-unavailable";
763
+ verifierPreflightError = `Verification target ${task.verifierFor} has no staged tree identity`;
764
+ } else {
765
+ try {
766
+ verifierTreeIdentity = await captureWorkflowTreeIdentity(target.cwd, { signal });
767
+ if (!sameWorkflowTreeIdentity(staged.stagedTreeIdentity, verifierTreeIdentity)) {
768
+ verifierPreflightCode = "verification-tree-mismatch";
769
+ verifierPreflightError =
770
+ "Workflow verification tree changed before verifier launch";
771
+ }
772
+ } catch (error) {
773
+ if (signal?.aborted) throw error;
774
+ verifierPreflightCode = "verification-tree-unavailable";
775
+ verifierPreflightError = error instanceof Error ? error.message : String(error);
776
+ }
777
+ }
778
+ }
779
+ const verificationTargetTask = task.verifierFor
780
+ ? taskById.get(task.verifierFor)?.task
781
+ : undefined;
782
+ const verificationTargetContract = normalizeDelegationContract(
783
+ verificationTargetTask?.contract,
784
+ );
785
+ const verificationSuffix =
786
+ task.verifierFor && verifierTreeIdentity
787
+ ? `\n\n${workflowVerificationInstruction(task.verifierFor, verifierTreeIdentity, {
788
+ acceptanceCriteria: [
789
+ ...(verificationTargetTask?.acceptanceCriteria ?? []),
790
+ ...(verificationTargetContract?.acceptanceCriteria ?? []),
791
+ ],
792
+ requiredEvidence: verificationTargetContract?.requiredEvidence ?? [],
793
+ })}`
794
+ : "";
795
+ const baseTask = `${task.task}${dependencyContext}`;
796
+ const baseBudget = Math.max(
797
+ 0,
798
+ DEFAULT_MAX_CONTEXT_BYTES - Buffer.byteLength(verificationSuffix, "utf8"),
799
+ );
800
+ const taskWithContext = `${truncateUtf8(baseTask, baseBudget).text}${verificationSuffix}`;
801
+ const prepared = prepareTask(taskWithContext, task);
705
802
  const startedItem = startWorkItem(workItemId, task.agent);
706
803
  const acceptedTaskGeneration = startedItem?.taskGeneration ?? 0;
707
804
  await persistWorkLedger();
@@ -737,9 +834,20 @@ export async function executeSubagent(
737
834
  );
738
835
  };
739
836
  const maxAttempts = task.retryPolicy?.maxAttempts ?? 1;
740
- let result: SingleResult | undefined;
837
+ let result: SingleResult | undefined = verifierPreflightError
838
+ ? attachTarget(
839
+ workflowFailureResult(
840
+ task.agent,
841
+ displayTask,
842
+ verifierPreflightCode ?? "verification-tree-unavailable",
843
+ verifierPreflightError,
844
+ thinkingLevel,
845
+ ),
846
+ target,
847
+ )
848
+ : undefined;
741
849
  let hedged = false;
742
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
850
+ for (let attempt = 1; !verifierPreflightError && attempt <= maxAttempts; attempt++) {
743
851
  if (attempt > 1 && deadline !== undefined && Date.now() >= deadline) break;
744
852
  const attempted = await runHedgedAttempt(runAttempt, signal, task.hedgeAfterMs);
745
853
  hedged ||= attempted.hedged;
@@ -765,7 +873,123 @@ export async function executeSubagent(
765
873
  };
766
874
  }
767
875
  resultsById.set(workItemId, result);
768
- settleWorkItem(workItemId, result, acceptedTaskGeneration);
876
+ if (result.outcome?.status === "stale") {
877
+ // Cancellation or replacement already rotated and invalidated this generation.
878
+ } else if (task.verifierFor) {
879
+ const staged = workLedger.get(task.verifierFor);
880
+ const structured =
881
+ result.structuredResult?.version === "pi-subagents:result:v2"
882
+ ? result.structuredResult
883
+ : undefined;
884
+ let failureReason: string | undefined = verifierPreflightError;
885
+ let failureCode: string | undefined = verifierPreflightCode;
886
+ let postVerifierIdentity = verifierTreeIdentity;
887
+ if (!failureReason) {
888
+ try {
889
+ postVerifierIdentity = await captureWorkflowTreeIdentity(target.cwd, { signal });
890
+ if (
891
+ !verifierTreeIdentity ||
892
+ !staged?.stagedTreeIdentity ||
893
+ !sameWorkflowTreeIdentity(verifierTreeIdentity, postVerifierIdentity) ||
894
+ !sameWorkflowTreeIdentity(staged.stagedTreeIdentity, postVerifierIdentity)
895
+ ) {
896
+ failureCode = "verification-tree-mismatch";
897
+ failureReason = "Workflow verification tree changed during verifier execution";
898
+ }
899
+ } catch (error) {
900
+ if (signal?.aborted) throw error;
901
+ failureCode = "verification-tree-unavailable";
902
+ failureReason = error instanceof Error ? error.message : String(error);
903
+ }
904
+ }
905
+ if (
906
+ !failureReason &&
907
+ structured &&
908
+ staged?.acceptedExecutionPlanId &&
909
+ result.executionPlan?.id &&
910
+ postVerifierIdentity
911
+ ) {
912
+ try {
913
+ const receipt = createWorkflowVerificationReceipt(structured, {
914
+ targetTaskId: staged.id,
915
+ targetTaskGeneration: staged.taskGeneration,
916
+ targetExecutionPlanId: staged.acceptedExecutionPlanId,
917
+ verifierTaskId: workItemId,
918
+ verifierTaskGeneration: acceptedTaskGeneration,
919
+ verifierExecutionPlanId: result.executionPlan.id,
920
+ treeIdentity: postVerifierIdentity,
921
+ sourceTruncated: result.truncated === true,
922
+ });
923
+ workLedger.completeVerification(workItemId, {
924
+ taskGeneration: acceptedTaskGeneration,
925
+ executionPlanId: result.executionPlan.id,
926
+ receipt,
927
+ });
928
+ if (receipt.decision !== "accept") {
929
+ const targetResult = resultsById.get(staged.id);
930
+ if (targetResult) {
931
+ targetResult.outcome = {
932
+ status: receipt.decision === "rework" ? "blocked" : "failed",
933
+ reasonCode:
934
+ receipt.decision === "rework"
935
+ ? "verification-rework"
936
+ : "verification-rejected",
937
+ recoveryActions:
938
+ receipt.decision === "rework" ? ["replan", "verify"] : ["stop"],
939
+ retryable: false,
940
+ };
941
+ }
942
+ }
943
+ } catch (error) {
944
+ failureCode = "verification-receipt-invalid";
945
+ failureReason = error instanceof Error ? error.message : String(error);
946
+ }
947
+ } else if (!failureReason) {
948
+ failureCode = "verification-receipt-invalid";
949
+ failureReason = "Workflow verifier did not return a current structured-v2 result";
950
+ }
951
+ if (failureReason) {
952
+ const reasonCode = failureCode ?? "verification-receipt-invalid";
953
+ result.outcome = {
954
+ status:
955
+ reasonCode === "verification-receipt-invalid" ? "contract-invalid" : "failed",
956
+ reasonCode,
957
+ recoveryActions:
958
+ reasonCode === "verification-receipt-invalid"
959
+ ? ["repair-contract"]
960
+ : ["revalidate"],
961
+ retryable: false,
962
+ };
963
+ result.errorMessage = failureReason;
964
+ workLedger.failVerification(workItemId, reasonCode);
965
+ }
966
+ } else if (verificationTargetIds.has(workItemId) && !isResultError(result)) {
967
+ try {
968
+ const treeIdentity = await captureWorkflowTreeIdentity(target.cwd, { signal });
969
+ if (!result.executionPlan?.id) {
970
+ throw new Error("Verification-required producer has no accepted ExecutionPlan");
971
+ }
972
+ workLedger.stageForVerification(workItemId, {
973
+ taskGeneration: acceptedTaskGeneration,
974
+ executionPlanId: result.executionPlan.id,
975
+ artifacts: artifactsFromResult(result),
976
+ treeIdentity,
977
+ });
978
+ } catch (error) {
979
+ if (signal?.aborted) throw error;
980
+ const message = error instanceof Error ? error.message : String(error);
981
+ result.outcome = {
982
+ status: "failed",
983
+ reasonCode: "verification-tree-unavailable",
984
+ recoveryActions: ["revalidate"],
985
+ retryable: false,
986
+ };
987
+ result.errorMessage = message;
988
+ settleWorkItem(workItemId, result, acceptedTaskGeneration);
989
+ }
990
+ } else {
991
+ settleWorkItem(workItemId, result, acceptedTaskGeneration);
992
+ }
769
993
  await persistWorkLedger();
770
994
  return result;
771
995
  },
@@ -774,11 +998,19 @@ export async function executeSubagent(
774
998
  if (batch.length === 0 || signal?.aborted) break;
775
999
  }
776
1000
  for (const item of workLedger.snapshot().items) {
777
- if (item.state !== "pending" && item.state !== "ready") continue;
1001
+ if (
1002
+ item.state !== "pending" &&
1003
+ item.state !== "ready" &&
1004
+ item.state !== "awaiting-verification"
1005
+ ) {
1006
+ continue;
1007
+ }
778
1008
  if (signal?.aborted) {
779
1009
  workLedger.settle(item.id, "interrupted", "parent-aborted");
780
1010
  } else if (deadline !== undefined && Date.now() >= deadline) {
781
1011
  workLedger.settle(item.id, "blocked", "budget-exhausted");
1012
+ } else if (item.state === "awaiting-verification") {
1013
+ workLedger.settle(item.id, "blocked", "verification-not-completed");
782
1014
  } else {
783
1015
  const dependencyBlocked = item.dependencies.some(
784
1016
  (dependency) => workLedger.get(dependency)?.state !== "completed",
@@ -793,8 +1025,26 @@ export async function executeSubagent(
793
1025
  await persistWorkLedger();
794
1026
  const results = resolvedWorkflowTasks.map((task) => {
795
1027
  const completed = resultsById.get(task.id);
796
- if (completed) return completed;
797
1028
  const item = workLedger.get(task.id);
1029
+ if (completed) {
1030
+ if (item && item.state !== "completed" && !isResultError(completed)) {
1031
+ completed.outcome = {
1032
+ status:
1033
+ item.state === "needs-input"
1034
+ ? "needs-input"
1035
+ : item.state === "interrupted"
1036
+ ? "interrupted"
1037
+ : item.state === "failed"
1038
+ ? "failed"
1039
+ : "blocked",
1040
+ reasonCode: item.outcomeReason ?? "verification-not-accepted",
1041
+ recoveryActions:
1042
+ item.outcomeReason === "verification-rework" ? ["replan", "verify"] : ["stop"],
1043
+ retryable: false,
1044
+ };
1045
+ }
1046
+ return completed;
1047
+ }
798
1048
  const outcomeStatus =
799
1049
  item?.state === "interrupted"
800
1050
  ? "interrupted"
package/src/inspect.ts CHANGED
@@ -537,6 +537,31 @@ function projectWorkflow(workflow: WorkItemLedgerSnapshot): Record<string, unkno
537
537
  verified: artifact.verified,
538
538
  })),
539
539
  verificationAccepted: item.verificationAccepted,
540
+ stagedTreeIdentity: item.stagedTreeIdentity
541
+ ? {
542
+ version: item.stagedTreeIdentity.version,
543
+ kind: item.stagedTreeIdentity.kind,
544
+ digest: item.stagedTreeIdentity.digest,
545
+ }
546
+ : undefined,
547
+ verificationReceipt: item.verificationReceipt
548
+ ? {
549
+ version: item.verificationReceipt.version,
550
+ decision: item.verificationReceipt.decision,
551
+ targetTaskId: boundedPrivateText(item.verificationReceipt.targetTaskId, 256),
552
+ targetTaskGeneration: item.verificationReceipt.targetTaskGeneration,
553
+ targetExecutionPlanId: item.verificationReceipt.targetExecutionPlanId,
554
+ verifierTaskId: boundedPrivateText(item.verificationReceipt.verifierTaskId, 256),
555
+ verifierTaskGeneration: item.verificationReceipt.verifierTaskGeneration,
556
+ verifierExecutionPlanId: item.verificationReceipt.verifierExecutionPlanId,
557
+ treeIdentity: item.verificationReceipt.treeIdentity,
558
+ summary: boundedPrivateText(item.verificationReceipt.summary, 8 * 1024),
559
+ evidenceCount: item.verificationReceipt.evidence.length,
560
+ limitationCount: item.verificationReceipt.limitations.length,
561
+ createdAt: item.verificationReceipt.createdAt,
562
+ truncated: item.verificationReceipt.truncated,
563
+ }
564
+ : undefined,
540
565
  outcomeReason: item.outcomeReason
541
566
  ? boundedPrivateText(item.outcomeReason, 2 * 1024)
542
567
  : undefined,
@@ -14,6 +14,12 @@ export interface OrchestrationMetrics {
14
14
  requestedTools: number;
15
15
  effectiveRequestedTools: number;
16
16
  permissionPrecision: number;
17
+ workerReportedVerification: number;
18
+ executorAcceptedVerification: number;
19
+ verificationRework: number;
20
+ verificationRejected: number;
21
+ verificationInvalid: number;
22
+ verificationTreeMismatch: number;
17
23
  panelValidReviews?: number;
18
24
  panelFailedReviews?: number;
19
25
  panelBlockingObjections?: number;
@@ -65,6 +71,31 @@ export function calculateOrchestrationMetrics(
65
71
  requestedTools,
66
72
  effectiveRequestedTools,
67
73
  permissionPrecision: requestedTools === 0 ? 1 : effectiveRequestedTools / requestedTools,
74
+ workerReportedVerification: results.filter(
75
+ (result, index) =>
76
+ !items[index]?.verifierFor &&
77
+ result.structuredResult?.version === "pi-subagents:result:v2" &&
78
+ result.structuredResult.verification.some(
79
+ (verification) => verification.status === "passed",
80
+ ),
81
+ ).length,
82
+ executorAcceptedVerification: items.filter((item) => item.verificationAccepted).length,
83
+ verificationRework: items.filter(
84
+ (item) => !item.verifierFor && item.verificationReceipt?.decision === "rework",
85
+ ).length,
86
+ verificationRejected: items.filter(
87
+ (item) => !item.verifierFor && item.verificationReceipt?.decision === "reject",
88
+ ).length,
89
+ verificationInvalid: items.filter(
90
+ (item) => !item.verifierFor && item.outcomeReason === "verification-receipt-invalid",
91
+ ).length,
92
+ verificationTreeMismatch: items.filter(
93
+ (item) =>
94
+ !item.verifierFor &&
95
+ ["verification-tree-mismatch", "verification-tree-unavailable"].includes(
96
+ item.outcomeReason ?? "",
97
+ ),
98
+ ).length,
68
99
  ...(panel
69
100
  ? {
70
101
  panelValidReviews: panel.validReviewCount,
@@ -389,7 +389,6 @@ export async function executePanel(input: PanelExecutionInput): Promise<PanelToo
389
389
  },
390
390
  ]
391
391
  : [],
392
- verificationAccepted: false,
393
392
  });
394
393
  }
395
394
  completed += 1;
@@ -554,7 +553,6 @@ export async function executePanel(input: PanelExecutionInput): Promise<PanelToo
554
553
  workLedger.complete("synthesis", {
555
554
  taskGeneration: synthesisWork.taskGeneration,
556
555
  executionPlanId: synthesisResult.executionPlan?.id,
557
- verificationAccepted: false,
558
556
  });
559
557
  }
560
558
  await persistWork();
package/src/params.ts CHANGED
@@ -106,7 +106,14 @@ const WorkflowTaskItem = Type.Object({
106
106
  ownershipKeys: Type.Optional(Type.Array(Type.String({ maxLength: 256 }), { maxItems: 50 })),
107
107
  acceptanceCriteria: Type.Optional(Type.Array(Type.String({ maxLength: 4096 }), { maxItems: 50 })),
108
108
  integrationOwner: Type.Optional(Type.Boolean()),
109
- verifierFor: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
109
+ verifierFor: Type.Optional(
110
+ Type.String({
111
+ minLength: 1,
112
+ maxLength: 256,
113
+ description:
114
+ "Target task ID for one distinct direct-dependent structured-v2 verifier. The executor gates target acceptance on a current exact-tree receipt.",
115
+ }),
116
+ ),
110
117
  cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
111
118
  timeoutMs: Type.Optional(TimeoutMs),
112
119
  ...TurnLimitFields,