@wichayutdew/pi-workflows 0.2.3 → 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 (142) hide show
  1. package/README.md +66 -38
  2. package/dist/index.js +5903 -4782
  3. package/package.json +1 -1
  4. package/src/command-names.ts +7 -1
  5. package/src/commands.ts +126 -52
  6. package/src/config/catalog.ts +162 -0
  7. package/src/config/ceiling.ts +116 -116
  8. package/src/config/command-conflicts.ts +4 -4
  9. package/src/config/diagnostics.ts +28 -0
  10. package/src/config/load-settings.ts +37 -0
  11. package/src/config/load-types.ts +53 -0
  12. package/src/config/load-workflows.ts +135 -0
  13. package/src/config/load.ts +43 -311
  14. package/src/config/types.ts +137 -135
  15. package/src/config/validate.ts +12 -1261
  16. package/src/config/validation/permissions.ts +291 -0
  17. package/src/config/validation/prompt.ts +19 -0
  18. package/src/config/validation/settings.ts +122 -0
  19. package/src/config/validation/shared.ts +129 -0
  20. package/src/config/validation/shortcut.ts +118 -0
  21. package/src/config/validation/step.ts +228 -0
  22. package/src/config/validation/subagent.ts +288 -0
  23. package/src/config/validation/workflow.ts +156 -0
  24. package/src/config/yaml.ts +29 -0
  25. package/src/digest.ts +38 -6
  26. package/src/engine/checkpoint.ts +13 -9
  27. package/src/engine/create-run.ts +42 -0
  28. package/src/engine/gate-transitions.ts +184 -0
  29. package/src/engine/reconciliation-history.ts +88 -0
  30. package/src/engine/resume.ts +19 -7
  31. package/src/engine/run-advance.ts +122 -0
  32. package/src/engine/run-lifecycle.ts +124 -0
  33. package/src/engine/run-reconciliation.ts +116 -0
  34. package/src/engine/run-validation.ts +128 -0
  35. package/src/engine/state-types.ts +64 -0
  36. package/src/engine/state.ts +10 -193
  37. package/src/engine/transition-helpers.ts +28 -0
  38. package/src/engine/transition-types.ts +8 -0
  39. package/src/engine/transitions.ts +17 -471
  40. package/src/harness/action-context.ts +181 -0
  41. package/src/harness/catalog.ts +45 -0
  42. package/src/harness/context-idle.ts +21 -0
  43. package/src/harness/core-actions.ts +299 -0
  44. package/src/harness/delegation-control-actions.ts +250 -0
  45. package/src/harness/delegation-failure.ts +215 -0
  46. package/src/harness/delegation-plan.ts +219 -0
  47. package/src/harness/delegation-recovery-validation.ts +159 -0
  48. package/src/harness/delegation-response-actions.ts +295 -0
  49. package/src/harness/delegation-retry-policy.ts +118 -0
  50. package/src/harness/dependencies.ts +165 -0
  51. package/src/harness/gate-submission-action.ts +154 -0
  52. package/src/harness/lifecycle-actions.ts +154 -0
  53. package/src/harness/pause-actions.ts +120 -0
  54. package/src/harness/plannotator-result-actions.ts +123 -0
  55. package/src/harness/prompt-gate-actions.ts +270 -0
  56. package/src/harness/resume-action.ts +261 -0
  57. package/src/harness/start-actions.ts +183 -0
  58. package/src/harness/status-actions.ts +191 -0
  59. package/src/harness/step-execution-actions.ts +283 -0
  60. package/src/harness/types.ts +76 -0
  61. package/src/harness.ts +237 -2201
  62. package/src/index.ts +73 -11
  63. package/src/integrations/plannotator-requests.ts +127 -0
  64. package/src/integrations/plannotator-responses.ts +141 -0
  65. package/src/integrations/plannotator-types.ts +44 -0
  66. package/src/integrations/plannotator.ts +16 -235
  67. package/src/integrations/prompt-gate.ts +17 -10
  68. package/src/integrations/subagents/child-policy-envelope.ts +119 -0
  69. package/src/integrations/subagents/child-policy-paths.ts +66 -0
  70. package/src/integrations/subagents/child-policy-sections.ts +174 -0
  71. package/src/integrations/subagents/child-policy-types.ts +32 -0
  72. package/src/integrations/subagents/child-policy-validation.ts +163 -0
  73. package/src/integrations/subagents/child-runtime-completion.ts +56 -0
  74. package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
  75. package/src/integrations/subagents/child-runtime-files.ts +156 -0
  76. package/src/integrations/subagents/child-runtime-policy.ts +83 -0
  77. package/src/integrations/subagents/child-runtime-types.ts +30 -0
  78. package/src/integrations/subagents/child-runtime.ts +140 -301
  79. package/src/integrations/subagents/client-delegation.ts +181 -0
  80. package/src/integrations/subagents/client-messages.ts +66 -0
  81. package/src/integrations/subagents/client-types.ts +36 -0
  82. package/src/integrations/subagents/client.ts +127 -219
  83. package/src/integrations/subagents/delegated-result.ts +31 -0
  84. package/src/integrations/subagents/diagnostic-format.ts +40 -0
  85. package/src/integrations/subagents/diagnostic-text.ts +114 -0
  86. package/src/integrations/subagents/diagnostic-types.ts +77 -0
  87. package/src/integrations/subagents/diagnostics.ts +24 -799
  88. package/src/integrations/subagents/failure-correlation.ts +235 -0
  89. package/src/integrations/subagents/failure-transcript.ts +192 -0
  90. package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
  91. package/src/integrations/subagents/protocol-events.ts +27 -0
  92. package/src/integrations/subagents/protocol.ts +30 -375
  93. package/src/integrations/subagents/replay-audit.ts +147 -0
  94. package/src/integrations/subagents/replay-safety.ts +82 -0
  95. package/src/integrations/subagents/session-diagnostics.ts +258 -0
  96. package/src/policy/approved-command-extraction.ts +71 -0
  97. package/src/policy/approved-commands.ts +15 -432
  98. package/src/policy/bash-authorization.ts +86 -0
  99. package/src/policy/bash-read-only.ts +161 -0
  100. package/src/policy/bash-types.ts +20 -0
  101. package/src/policy/bash.ts +4 -346
  102. package/src/policy/completion-batch.ts +21 -16
  103. package/src/policy/immutable-input.ts +5 -2
  104. package/src/policy/mcp-authorization.ts +71 -0
  105. package/src/policy/restricted-command.ts +141 -0
  106. package/src/policy/restricted-git.ts +30 -0
  107. package/src/policy/reviewed-artifact.ts +97 -0
  108. package/src/policy/reviewed-command-safety.ts +150 -0
  109. package/src/policy/reviewed-command-shape.ts +49 -0
  110. package/src/policy/reviewed-repository-contract.ts +138 -0
  111. package/src/policy/reviewed-repository-cwd.ts +102 -0
  112. package/src/policy/tool-call-authorization.ts +58 -0
  113. package/src/policy/tool-selection.ts +72 -0
  114. package/src/policy/tool-types.ts +14 -0
  115. package/src/policy/tools.ts +11 -150
  116. package/src/preflight.ts +96 -61
  117. package/src/prompt/main-workflow-notice.ts +41 -0
  118. package/src/prompt/retry-task.ts +83 -0
  119. package/src/prompt/step-contract.ts +101 -0
  120. package/src/prompt/step-sections.ts +75 -0
  121. package/src/prompt/step-task.ts +156 -0
  122. package/src/prompt/template.ts +81 -0
  123. package/src/prompt.ts +10 -245
  124. package/src/runtime/completion-tool.ts +3 -0
  125. package/src/runtime/main-step-completion.ts +78 -0
  126. package/src/runtime/main-step-lifecycle.ts +37 -0
  127. package/src/runtime/main-step-policy.ts +84 -0
  128. package/src/runtime/main-step-runtime-types.ts +64 -0
  129. package/src/runtime/main-step-runtime.ts +129 -200
  130. package/src/runtime/main-step-state.ts +85 -0
  131. package/src/runtime/serial-task-queue.ts +48 -13
  132. package/src/runtime/step-result.ts +48 -27
  133. package/src/workflow-list.ts +15 -6
  134. package/src/workflow-status/format-status.ts +52 -0
  135. package/src/workflow-status/formatting.ts +178 -0
  136. package/src/workflow-status/layout.ts +109 -0
  137. package/src/workflow-status/render-board.ts +101 -0
  138. package/src/workflow-status/render-path.ts +123 -0
  139. package/src/workflow-status/render-summary.ts +144 -0
  140. package/src/workflow-status/types.ts +40 -0
  141. package/src/workflow-status/view.ts +273 -0
  142. package/src/workflow-status.ts +20 -820
@@ -0,0 +1,215 @@
1
+ import type { WorkflowStepResult } from '../runtime/step-result.ts';
2
+ import {
3
+ failedToolName,
4
+ formatToolFailureDiagnostic,
5
+ } from '../integrations/subagents/diagnostics.ts';
6
+ import type {
7
+ ChildStepPolicy,
8
+ SubagentDelegationResponse,
9
+ } from '../integrations/subagents/protocol.ts';
10
+ import type { WorkflowHarnessDependencies } from './dependencies.ts';
11
+ import {
12
+ completionMatchesResult,
13
+ recoveredProjectionError,
14
+ } from './delegation-recovery-validation.ts';
15
+ import {
16
+ boundedFailureField,
17
+ delegationFailureFingerprint,
18
+ isRetryableTerminalFailure,
19
+ isSafeToRetryDelegation,
20
+ rejectedRecoveryReason,
21
+ } from './delegation-retry-policy.ts';
22
+ import type {
23
+ ActiveDelegation,
24
+ DelegationFailureDetails,
25
+ DelegationRecoveryBlocker,
26
+ } from './types.ts';
27
+
28
+ export type DelegationFailureActions = {
29
+ delegationFailureFingerprint: (failure: DelegationFailureDetails) => string;
30
+ hasContradictoryCompletion: (response: SubagentDelegationResponse) => boolean;
31
+ isRetryableTerminalFailure: (failure: DelegationFailureDetails) => boolean;
32
+ isSafeToRetryDelegation: (
33
+ policy: ChildStepPolicy,
34
+ isReplayExplicitlyAuthorized: boolean,
35
+ replayAudit: DelegationFailureDetails['replayAudit'],
36
+ ) => boolean;
37
+ rejectedRecoveryReason: (
38
+ failure: DelegationFailureDetails,
39
+ error: unknown,
40
+ ) => string;
41
+ completionMatchesResult: (
42
+ diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
43
+ result: WorkflowStepResult,
44
+ policy: ChildStepPolicy,
45
+ ) => boolean;
46
+ recoveredProjectionError: (
47
+ active: ActiveDelegation,
48
+ response: SubagentDelegationResponse,
49
+ diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
50
+ ) => string | undefined;
51
+ describeDelegationFailure: (
52
+ active: ActiveDelegation,
53
+ response: SubagentDelegationResponse,
54
+ ) => Promise<DelegationFailureDetails>;
55
+ };
56
+
57
+ function nonEmptyTerminalError(
58
+ response: SubagentDelegationResponse,
59
+ ): string | undefined {
60
+ return [response.error, response.execution?.error].find(
61
+ (error): error is string =>
62
+ typeof error === 'string' && error.trim().length > 0,
63
+ );
64
+ }
65
+
66
+ function nonzeroTerminalExitCode(
67
+ response: SubagentDelegationResponse,
68
+ ): number | undefined {
69
+ return [response.exitCode, response.execution?.exitCode].find(
70
+ (exitCode): exitCode is number =>
71
+ typeof exitCode === 'number' &&
72
+ Number.isSafeInteger(exitCode) &&
73
+ exitCode !== 0,
74
+ );
75
+ }
76
+
77
+ function hasContradictoryCompletion(
78
+ response: SubagentDelegationResponse,
79
+ ): boolean {
80
+ return (
81
+ response.status === 'completed' &&
82
+ (nonEmptyTerminalError(response) !== undefined ||
83
+ nonzeroTerminalExitCode(response) !== undefined)
84
+ );
85
+ }
86
+
87
+ function recoveryBlocker(
88
+ response: SubagentDelegationResponse,
89
+ ): DelegationRecoveryBlocker | undefined {
90
+ const execution = response.execution;
91
+ const fileMutation = response.effects?.fileMutation;
92
+ if (fileMutation?.attempted === true || fileMutation?.status === 'observed') {
93
+ return 'reported-mutation';
94
+ }
95
+ if (execution?.detached === true || execution?.status === 'detached') {
96
+ return 'detached';
97
+ }
98
+ if (execution?.stopped === true || execution?.status === 'stopped') {
99
+ return 'stopped';
100
+ }
101
+ if (
102
+ response.status === 'interrupted' ||
103
+ execution?.interrupted === true ||
104
+ execution?.status === 'paused'
105
+ ) {
106
+ return 'interrupted';
107
+ }
108
+ if (response.status === 'cancelled') return 'cancelled';
109
+ if (execution?.timedOut === true && response.status !== 'timed_out') {
110
+ return 'inconsistent-timeout';
111
+ }
112
+ return undefined;
113
+ }
114
+
115
+ function validateReplayAudit(
116
+ response: SubagentDelegationResponse,
117
+ replayAudit: DelegationFailureDetails['replayAudit'],
118
+ ): DelegationFailureDetails['replayAudit'] {
119
+ if (!replayAudit) return undefined;
120
+ if (
121
+ response.toolCount !== undefined &&
122
+ response.toolCount !== replayAudit.toolCount
123
+ ) {
124
+ return { ...replayAudit, replaySafe: false };
125
+ }
126
+ return replayAudit;
127
+ }
128
+
129
+ /**
130
+ * Creates pure delegation-failure evaluators plus the injected diagnostic
131
+ * reader used at the transcript I/O boundary.
132
+ */
133
+ export function createDelegationFailureActions(
134
+ dependencies: Pick<
135
+ WorkflowHarnessDependencies,
136
+ 'readDelegationReplayAudit' | 'readToolFailureDiagnostic'
137
+ >,
138
+ ): DelegationFailureActions {
139
+ const describeDelegationFailure = async (
140
+ active: ActiveDelegation,
141
+ response: SubagentDelegationResponse,
142
+ ): Promise<DelegationFailureDetails> => {
143
+ const blocker = recoveryBlocker(response);
144
+ const terminalError = nonEmptyTerminalError(response);
145
+ const error =
146
+ terminalError ?? 'The subagent returned no terminal error details.';
147
+ const isResponseIdentityValid =
148
+ response.childIndex === 0 &&
149
+ (response.agent === undefined || response.agent === active.agent);
150
+ const identity =
151
+ isResponseIdentityValid && response.runId !== undefined
152
+ ? { runId: response.runId, childIndex: 0 }
153
+ : undefined;
154
+ const [diagnostic, replayAudit] = await Promise.all([
155
+ dependencies.readToolFailureDiagnostic(
156
+ response.sessionFile,
157
+ active.trustedSessionRoot,
158
+ identity,
159
+ failedToolName(terminalError),
160
+ terminalError,
161
+ ),
162
+ dependencies.readDelegationReplayAudit(
163
+ response.sessionFile,
164
+ active.trustedSessionRoot,
165
+ identity,
166
+ {
167
+ task: active.transcriptTask,
168
+ bashPermission: active.policy.permissions.bash,
169
+ approvedBashCommands: active.policy.approvedBashCommands ?? [],
170
+ },
171
+ ),
172
+ ]);
173
+ const validatedReplayAudit = validateReplayAudit(response, replayAudit);
174
+ const exitCode =
175
+ nonzeroTerminalExitCode(response) ??
176
+ response.exitCode ??
177
+ response.execution?.exitCode;
178
+ const reason = [
179
+ hasContradictoryCompletion(response)
180
+ ? `Subagent "${active.agent}" reported terminal failure signals with completed status.`
181
+ : `Subagent "${active.agent}" ${response.status.replaceAll('_', ' ')}.`,
182
+ ...(diagnostic ? formatToolFailureDiagnostic(diagnostic) : []),
183
+ ...(exitCode !== undefined ? [`Subagent exit code: ${exitCode}`] : []),
184
+ ...(blocker
185
+ ? [`Automatic recovery blocked by: ${blocker.replaceAll('-', ' ')}`]
186
+ : []),
187
+ `Terminal error: ${boundedFailureField(error)}`,
188
+ ...(diagnostic && response.sessionFile
189
+ ? [
190
+ `Diagnostic session: ${boundedFailureField(response.sessionFile.replaceAll(/\s+/g, ' '))}`,
191
+ ]
192
+ : []),
193
+ ].join('\n');
194
+ return {
195
+ reason,
196
+ status: response.status,
197
+ ...(terminalError ? { error: terminalError } : {}),
198
+ ...(exitCode !== undefined ? { exitCode } : {}),
199
+ ...(blocker ? { recoveryBlocker: blocker } : {}),
200
+ ...(diagnostic ? { diagnostic } : {}),
201
+ ...(validatedReplayAudit ? { replayAudit: validatedReplayAudit } : {}),
202
+ };
203
+ };
204
+
205
+ return {
206
+ delegationFailureFingerprint,
207
+ hasContradictoryCompletion,
208
+ isRetryableTerminalFailure,
209
+ isSafeToRetryDelegation,
210
+ rejectedRecoveryReason,
211
+ completionMatchesResult,
212
+ recoveredProjectionError,
213
+ describeDelegationFailure,
214
+ };
215
+ }
@@ -0,0 +1,219 @@
1
+ import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
2
+ import type { LoadedWorkflow, WorkflowStep } from '../config/types.ts';
3
+ import type { WorkflowRun } from '../engine/state.ts';
4
+ import { allowedOutcomes } from '../engine/transitions.ts';
5
+ import { digest } from '../digest.ts';
6
+ import { deriveSubagentSessionRoot } from '../integrations/subagents/diagnostics.ts';
7
+ import {
8
+ encodeChildPolicy,
9
+ type ChildStepPolicy,
10
+ type SubagentDelegationRequest,
11
+ } from '../integrations/subagents/protocol.ts';
12
+ import {
13
+ narrowApprovedBashCommands,
14
+ resolveReviewedRepositoryCwd,
15
+ } from '../policy/approved-commands.ts';
16
+ import { automaticRecoveryTask, buildDelegatedStepTask } from '../prompt.ts';
17
+ import { WORKFLOW_COMPLETION_PARAMETERS } from '../runtime/completion-tool.ts';
18
+ import { MAX_DELEGATION_RECOVERY_ATTEMPTS } from './delegation-retry-policy.ts';
19
+ import type { WorkflowHarnessDependencies } from './dependencies.ts';
20
+ import type { ActiveDelegation, DelegationRecovery } from './types.ts';
21
+
22
+ type DelegationPlanInput = {
23
+ workflow: LoadedWorkflow;
24
+ run: WorkflowRun;
25
+ step: WorkflowStep;
26
+ sessionEpoch: number;
27
+ latestContext: ExtensionContext | undefined;
28
+ recovery: DelegationRecovery | undefined;
29
+ };
30
+
31
+ export type DelegationPlan =
32
+ | {
33
+ kind: 'invalid';
34
+ reason: string;
35
+ }
36
+ | {
37
+ kind: 'ready';
38
+ active: ActiveDelegation;
39
+ request: SubagentDelegationRequest;
40
+ };
41
+
42
+ function delegationTranscriptBinding(
43
+ requestId: string,
44
+ policyDigest: string,
45
+ ): string {
46
+ return `<pi-workflows-delegation-binding-v1>${requestId}:${policyDigest}</pi-workflows-delegation-binding-v1>`;
47
+ }
48
+
49
+ function approvedCommands(
50
+ run: WorkflowRun,
51
+ step: WorkflowStep,
52
+ ): ReadonlyArray<string> {
53
+ return narrowApprovedBashCommands(
54
+ run.reviewedArtifact ?? '',
55
+ run.stepHandoff ?? '',
56
+ step.permissions.bash.approvedSources ?? [],
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Creates one immutable subagent request from workflow state and injected
62
+ * identity/workspace effects.
63
+ */
64
+ export function createDelegationPlan(
65
+ input: DelegationPlanInput,
66
+ dependencies: Pick<
67
+ WorkflowHarnessDependencies,
68
+ 'createDelegationWorkspace' | 'createRequestId' | 'currentWorkingDirectory'
69
+ >,
70
+ ): DelegationPlan {
71
+ const { workflow, run, step, latestContext, recovery } = input;
72
+ const subagent = step.subagent;
73
+ if (!subagent) {
74
+ return {
75
+ kind: 'invalid',
76
+ reason: `Step "${run.currentStepId}" has no subagent configuration`,
77
+ };
78
+ }
79
+
80
+ const reviewedRepository = resolveReviewedRepositoryCwd(
81
+ run.reviewedArtifact ?? '',
82
+ );
83
+ if (reviewedRepository.kind === 'invalid') {
84
+ return { kind: 'invalid', reason: reviewedRepository.reason };
85
+ }
86
+
87
+ const delegationCwd =
88
+ reviewedRepository.kind === 'resolved'
89
+ ? reviewedRepository.cwd
90
+ : (latestContext?.cwd ?? dependencies.currentWorkingDirectory());
91
+ const requestId =
92
+ `${run.runId}:${run.currentStepId}:` + dependencies.createRequestId();
93
+ const workspace = dependencies.createDelegationWorkspace();
94
+ const outcomes = allowedOutcomes(workflow, run);
95
+ const outcomeSet = new Set(outcomes);
96
+ const approvedBashCommands = approvedCommands(run, step);
97
+ const repositoryPolicy =
98
+ reviewedRepository.kind === 'resolved'
99
+ ? {
100
+ repositoryCwd: reviewedRepository.repositoryCwd,
101
+ ...(reviewedRepository.bootstrapping
102
+ ? { bootstrapCwd: reviewedRepository.cwd }
103
+ : {}),
104
+ }
105
+ : {};
106
+ const policyDigest = digest({
107
+ version: 1,
108
+ requestId,
109
+ agent: subagent.agent,
110
+ runId: run.runId,
111
+ stepId: run.currentStepId,
112
+ stepDigest: run.currentStepDigest,
113
+ capabilityPath: workspace.capabilityPath,
114
+ resultPath: workspace.resultPath,
115
+ approvedBashCommands,
116
+ ...repositoryPolicy,
117
+ });
118
+ const policy: ChildStepPolicy = {
119
+ version: 1,
120
+ requestId,
121
+ agent: subagent.agent,
122
+ workflowId: workflow.definition.id,
123
+ runId: run.runId,
124
+ stepId: run.currentStepId,
125
+ stepTitle: step.title,
126
+ policyDigest,
127
+ capabilityPath: workspace.capabilityPath,
128
+ capabilityToken: workspace.capabilityToken,
129
+ resultPath: workspace.resultPath,
130
+ permissions: structuredClone(step.permissions),
131
+ ...(approvedBashCommands.length > 0 ? { approvedBashCommands } : {}),
132
+ ...repositoryPolicy,
133
+ outcomes,
134
+ pauseOutcomes: Object.entries(step.transitions)
135
+ .filter(
136
+ ([outcome, target]) => target === '$pause' && outcomeSet.has(outcome),
137
+ )
138
+ .map(([outcome]) => outcome),
139
+ summaryMaxChars: workflow.definition.summaryMaxChars,
140
+ ...(step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {}),
141
+ };
142
+ const trustedSessionRoot = deriveSubagentSessionRoot(
143
+ latestContext?.sessionManager.getSessionFile(),
144
+ );
145
+ const transcriptTask = [
146
+ buildDelegatedStepTask(workflow, run, ''),
147
+ delegationTranscriptBinding(requestId, policyDigest),
148
+ ...(recovery
149
+ ? [
150
+ automaticRecoveryTask(
151
+ recovery.failures.map(({ reason }) => reason),
152
+ recovery.attempt,
153
+ MAX_DELEGATION_RECOVERY_ATTEMPTS,
154
+ ),
155
+ ]
156
+ : []),
157
+ ].join('\n\n');
158
+ const active: ActiveDelegation = {
159
+ requestId,
160
+ runId: run.runId,
161
+ stepId: run.currentStepId,
162
+ stepDigest: run.currentStepDigest,
163
+ sessionEpoch: input.sessionEpoch,
164
+ resultDirectory: workspace.resultDirectory,
165
+ policy,
166
+ transcriptTask,
167
+ agent: subagent.agent,
168
+ ...(trustedSessionRoot ? { trustedSessionRoot } : {}),
169
+ broadRecoveryAuthorized: subagent.retryToolFailures,
170
+ recoveryAttemptCount: recovery?.attempt ?? 0,
171
+ recoveryFailures: recovery?.failures ?? [],
172
+ };
173
+ const request: SubagentDelegationRequest = {
174
+ version: 1,
175
+ requestId,
176
+ agent: subagent.agent,
177
+ task: `${encodeChildPolicy(policy)}\n\n${transcriptTask}`,
178
+ // Workflow steps are isolation boundaries. Parent/sibling transcripts are
179
+ // replaced by the explicit workflow handoff.
180
+ context: 'fresh',
181
+ cwd: delegationCwd,
182
+ timeoutMs: subagent.timeoutMs,
183
+ skill:
184
+ step.permissions.skills.length > 0 ? [...step.permissions.skills] : false,
185
+ output: false,
186
+ // TypeBox schemas are structurally JSON objects, but the upstream contract
187
+ // exposes a generic record.
188
+ outputSchema: WORKFLOW_COMPLETION_PARAMETERS as unknown as Record<
189
+ string,
190
+ unknown
191
+ >,
192
+ agentContract: { version: 1 },
193
+ artifacts: subagent.artifacts,
194
+ ...(subagent.model ? { model: subagent.model } : {}),
195
+ ...(subagent.turnBudget
196
+ ? { turnBudget: structuredClone(subagent.turnBudget) }
197
+ : {}),
198
+ ...(subagent.toolBudget
199
+ ? {
200
+ toolBudget: {
201
+ hard: subagent.toolBudget.hard,
202
+ ...(subagent.toolBudget.soft === undefined
203
+ ? {}
204
+ : { soft: subagent.toolBudget.soft }),
205
+ ...(subagent.toolBudget.block === undefined
206
+ ? {}
207
+ : {
208
+ block:
209
+ subagent.toolBudget.block === '*'
210
+ ? '*'
211
+ : [...subagent.toolBudget.block],
212
+ }),
213
+ },
214
+ }
215
+ : {}),
216
+ };
217
+
218
+ return { kind: 'ready', active, request };
219
+ }
@@ -0,0 +1,159 @@
1
+ import { parseDelegatedStepResult } from '../integrations/subagents/protocol.ts';
2
+ import type {
3
+ ChildStepPolicy,
4
+ SubagentDelegationResponse,
5
+ } from '../integrations/subagents/protocol.ts';
6
+ import type { WorkflowStepResult } from '../runtime/step-result.ts';
7
+ import type { ActiveDelegation, DelegationFailureDetails } from './types.ts';
8
+
9
+ /**
10
+ * Verifies that transcript completion data exactly matches a recovered result.
11
+ *
12
+ * @param diagnostic - Correlated tool-failure diagnostic.
13
+ * @param result - Result recovered from the delegated artifact.
14
+ * @param policy - Child policy used to validate the result.
15
+ * @returns Whether the transcript and recovered artifact agree.
16
+ */
17
+ export function completionMatchesResult(
18
+ diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
19
+ result: WorkflowStepResult,
20
+ policy: ChildStepPolicy,
21
+ ): boolean {
22
+ const value = diagnostic.completionValue;
23
+ if (!value) return false;
24
+ const expectedKeys = [
25
+ 'outcome',
26
+ 'summary',
27
+ ...(result.artifact === undefined ? [] : ['artifact']),
28
+ ].sort();
29
+ const actualKeys = Object.keys(value).sort();
30
+ if (
31
+ actualKeys.length !== expectedKeys.length ||
32
+ !actualKeys.every((key, index) => key === expectedKeys[index])
33
+ ) {
34
+ return false;
35
+ }
36
+ try {
37
+ const completion = parseDelegatedStepResult(
38
+ {
39
+ ...value,
40
+ version: 1,
41
+ policyDigest: policy.policyDigest,
42
+ },
43
+ policy,
44
+ );
45
+ return (
46
+ completion.outcome === result.outcome &&
47
+ completion.summary === result.summary &&
48
+ completion.artifact === result.artifact
49
+ );
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Finds contradictory or unsafe fields in a recovered terminal projection.
57
+ *
58
+ * @param active - Active delegation identity.
59
+ * @param response - Terminal response received from the child.
60
+ * @param diagnostic - Transcript evidence correlated to the failure.
61
+ * @returns A rejection reason, or undefined for a valid projection.
62
+ */
63
+ export function recoveredProjectionError(
64
+ active: ActiveDelegation,
65
+ response: SubagentDelegationResponse,
66
+ diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
67
+ ): string | undefined {
68
+ if (response.agent !== active.agent) {
69
+ return `terminal agent identity is ${JSON.stringify(response.agent)}; expected ${JSON.stringify(active.agent)}`;
70
+ }
71
+ if (response.childIndex !== 0) {
72
+ return `terminal child index is ${JSON.stringify(response.childIndex)}; expected 0`;
73
+ }
74
+ if (
75
+ typeof response.exitCode !== 'number' ||
76
+ !Number.isSafeInteger(response.exitCode) ||
77
+ response.exitCode <= 0
78
+ ) {
79
+ return `terminal exit code is ${JSON.stringify(response.exitCode)}; expected a positive safe integer`;
80
+ }
81
+ const execution = response.execution;
82
+ if (!execution) return 'terminal response has no execution projection';
83
+ if (execution.status !== 'failed' || execution.success) {
84
+ return `execution projection is ${JSON.stringify({
85
+ status: execution.status,
86
+ success: execution.success,
87
+ })}; expected failed/false`;
88
+ }
89
+ if (execution.exitCode !== response.exitCode) {
90
+ return `execution exit code ${JSON.stringify(execution.exitCode)} does not match terminal exit code ${JSON.stringify(response.exitCode)}`;
91
+ }
92
+ if (
93
+ typeof response.error !== 'string' ||
94
+ !response.error ||
95
+ typeof execution.error !== 'string' ||
96
+ execution.error !== response.error
97
+ ) {
98
+ return 'terminal and execution errors are missing or do not match exactly';
99
+ }
100
+ const warnings = response.warnings as unknown;
101
+ if (
102
+ warnings !== undefined &&
103
+ (!Array.isArray(warnings) ||
104
+ warnings.some(
105
+ (warning) => typeof warning !== 'string' || warning.trim().length > 0,
106
+ ))
107
+ ) {
108
+ return `terminal response contains warning evidence: ${JSON.stringify(warnings)}`;
109
+ }
110
+ if (
111
+ diagnostic.transcriptToolCount !== undefined &&
112
+ response.toolCount !== undefined &&
113
+ response.toolCount !== diagnostic.transcriptToolCount
114
+ ) {
115
+ return `terminal tool count ${response.toolCount} does not match transcript tool count ${diagnostic.transcriptToolCount}`;
116
+ }
117
+ if (
118
+ diagnostic.transcriptTurnCount !== undefined &&
119
+ response.turns !== undefined &&
120
+ response.turns !== diagnostic.transcriptTurnCount
121
+ ) {
122
+ return `terminal turn count ${response.turns} does not match transcript turn count ${diagnostic.transcriptTurnCount}`;
123
+ }
124
+ const toolFailure = response.error.match(
125
+ /^\s*([a-z][\w-]*) failed\s*\(exit\s+(\d+)\)\s*:/i,
126
+ );
127
+ if (!toolFailure) {
128
+ return 'terminal error is not a recognized "<tool> failed (exit N): <detail>" failure';
129
+ }
130
+ const terminalTool = toolFailure[1];
131
+ const terminalExitCodeText = toolFailure[2];
132
+ if (!terminalTool || !terminalExitCodeText) {
133
+ return 'terminal error does not contain a complete tool failure projection';
134
+ }
135
+ const terminalExitCode = Number(terminalExitCodeText);
136
+ if (
137
+ terminalTool.toLowerCase() !== diagnostic.tool.toLowerCase() ||
138
+ terminalExitCode !== response.exitCode
139
+ ) {
140
+ return `terminal tool/exit ${JSON.stringify({
141
+ tool: terminalTool,
142
+ exitCode: terminalExitCode,
143
+ })} does not match the correlated failure ${JSON.stringify({
144
+ tool: diagnostic.tool,
145
+ exitCode: response.exitCode,
146
+ })}`;
147
+ }
148
+ const unsafeFlag = (
149
+ [
150
+ ['interrupted', execution.interrupted],
151
+ ['timedOut', execution.timedOut],
152
+ ['stopped', execution.stopped],
153
+ ['detached', execution.detached],
154
+ ] as const
155
+ ).find(([, isEnabled]) => isEnabled === true)?.[0];
156
+ return unsafeFlag
157
+ ? `execution projection reports ${unsafeFlag}=true`
158
+ : undefined;
159
+ }