@wichayutdew/pi-workflows 2.5.0 → 2.6.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 (95) hide show
  1. package/README.md +4 -1
  2. package/dist/index.js +1133 -2409
  3. package/examples/starter-kit/agents/planner.md +7 -0
  4. package/examples/starter-kit/agents/reviewer.md +6 -0
  5. package/examples/starter-kit/agents/scout.md +6 -0
  6. package/examples/starter-kit/agents/worker.md +7 -0
  7. package/examples/starter-kit/agents/workspace-preparer.md +6 -0
  8. package/examples/starter-kit/investigate.workflow.yaml +114 -0
  9. package/examples/starter-kit/mr-comment.workflow.yaml +114 -54
  10. package/examples/starter-kit/mr-review.workflow.yaml +77 -56
  11. package/examples/starter-kit/settings.yaml +3 -0
  12. package/examples/starter-kit/steps/investigate/investigate.md +77 -0
  13. package/examples/starter-kit/steps/investigate/retrieve.md +63 -0
  14. package/examples/starter-kit/steps/investigate/validate.md +46 -0
  15. package/examples/starter-kit/steps/mr-comment/checkout-source.md +67 -0
  16. package/examples/starter-kit/steps/mr-comment/fetch.md +34 -26
  17. package/examples/starter-kit/steps/mr-comment/implement.md +34 -27
  18. package/examples/starter-kit/steps/mr-comment/plan.md +54 -40
  19. package/examples/starter-kit/steps/mr-comment/publish.md +28 -27
  20. package/examples/starter-kit/steps/mr-comment/verify.md +42 -35
  21. package/examples/starter-kit/steps/mr-review/fetch.md +52 -22
  22. package/examples/starter-kit/steps/mr-review/publish-approved.md +49 -0
  23. package/examples/starter-kit/steps/mr-review/review-for-approval.md +142 -0
  24. package/examples/starter-kit/steps/mr-review/verify-published.md +37 -0
  25. package/examples/starter-kit/steps/shared/prepare-workspace.md +91 -73
  26. package/examples/starter-kit/steps/shared/publish-remote.md +45 -0
  27. package/examples/starter-kit/steps/ticket/implement.md +59 -25
  28. package/examples/starter-kit/steps/ticket/plan.md +145 -72
  29. package/examples/starter-kit/steps/ticket/verify.md +80 -45
  30. package/examples/starter-kit/steps/work/implement.md +60 -24
  31. package/examples/starter-kit/steps/work/plan.md +129 -57
  32. package/examples/starter-kit/steps/work/verify.md +58 -22
  33. package/examples/starter-kit/ticket.workflow.yaml +75 -36
  34. package/examples/starter-kit/work.workflow.yaml +63 -33
  35. package/package.json +3 -16
  36. package/schemas/workflow.schema.json +2 -15
  37. package/src/agents/profile.ts +98 -0
  38. package/src/config/ceiling.ts +0 -82
  39. package/src/config/types.ts +6 -53
  40. package/src/config/validation/settings.ts +2 -14
  41. package/src/config/validation/step.ts +12 -8
  42. package/src/config/validation/workflow.ts +1 -10
  43. package/src/engine/run-workflow-validation.ts +0 -3
  44. package/src/engine/state-types.ts +1 -1
  45. package/src/harness/action-context.ts +1 -13
  46. package/src/harness/delegation-control-actions.ts +4 -63
  47. package/src/harness/delegation-plan.ts +26 -80
  48. package/src/harness/delegation-response-actions.ts +22 -135
  49. package/src/harness/dependencies.ts +1 -12
  50. package/src/harness/status-actions.ts +1 -0
  51. package/src/harness/step-execution-actions.ts +2 -9
  52. package/src/harness/types.ts +2 -40
  53. package/src/harness.ts +2 -17
  54. package/src/index.ts +4 -9
  55. package/src/integrations/subagents/child-policy-validation.ts +6 -7
  56. package/src/integrations/subagents/child-runtime-dependencies.ts +1 -1
  57. package/src/integrations/subagents/child-runtime-policy.ts +1 -7
  58. package/src/integrations/subagents/child-runtime.ts +18 -9
  59. package/src/integrations/subagents/client.ts +240 -98
  60. package/src/integrations/subagents/protocol-events.ts +32 -15
  61. package/src/integrations/subagents/protocol.ts +1 -1
  62. package/src/preflight.ts +0 -8
  63. package/src/prompt/main-workflow-notice.ts +8 -15
  64. package/src/prompt/step-task.ts +7 -6
  65. package/src/workflow-status/format-status.ts +5 -1
  66. package/src/workflow-status/render-step-detail.ts +94 -0
  67. package/src/workflow-status/types.ts +1 -0
  68. package/src/workflow-status/view.ts +49 -14
  69. package/agents/step.md +0 -32
  70. package/examples/mr-comments.workflow.yaml +0 -125
  71. package/examples/prompts/mr-comments/implement.md +0 -17
  72. package/examples/prompts/mr-comments/inspect.md +0 -5
  73. package/examples/prompts/mr-comments/plan.md +0 -54
  74. package/examples/prompts/mr-comments/verify.md +0 -9
  75. package/examples/settings.yaml +0 -27
  76. package/examples/starter-kit/steps/mr-review/publish.md +0 -48
  77. package/examples/starter-kit/steps/mr-review/review.md +0 -76
  78. package/examples/starter-kit/steps/mr-review/verify.md +0 -35
  79. package/src/config/validation/subagent.ts +0 -288
  80. package/src/harness/delegation-failure.ts +0 -248
  81. package/src/harness/delegation-recovery-validation.ts +0 -161
  82. package/src/harness/delegation-retry-policy.ts +0 -120
  83. package/src/integrations/subagents/client-delegation.ts +0 -181
  84. package/src/integrations/subagents/client-messages.ts +0 -66
  85. package/src/integrations/subagents/client-types.ts +0 -36
  86. package/src/integrations/subagents/diagnostic-format.ts +0 -45
  87. package/src/integrations/subagents/diagnostic-text.ts +0 -114
  88. package/src/integrations/subagents/diagnostic-types.ts +0 -83
  89. package/src/integrations/subagents/diagnostics.ts +0 -26
  90. package/src/integrations/subagents/failure-correlation.ts +0 -285
  91. package/src/integrations/subagents/failure-transcript.ts +0 -251
  92. package/src/integrations/subagents/hidden-bash-failure.ts +0 -98
  93. package/src/integrations/subagents/replay-audit.ts +0 -146
  94. package/src/integrations/subagents/replay-safety.ts +0 -67
  95. package/src/integrations/subagents/session-diagnostics.ts +0 -357
@@ -1,288 +0,0 @@
1
- import {
2
- DEFAULT_STEP_SUBAGENT,
3
- SUBAGENT_RUNTIME_NAME_PATTERN,
4
- type StepSubagent,
5
- type SubagentContext,
6
- type SubagentPermissionCeiling,
7
- type SubagentToolBudget,
8
- type SubagentTurnBudget,
9
- } from '../types.ts';
10
- import {
11
- isJsonObject,
12
- readBoolean,
13
- readInteger,
14
- readString,
15
- readStringList,
16
- rejectUnknownKeys,
17
- RESOURCE_SELECTOR_PATTERN,
18
- TOOL_PATTERN,
19
- type ValidationErrors,
20
- } from './shared.ts';
21
-
22
- function parseSubagentTurnBudget(
23
- value: unknown,
24
- path: string,
25
- errors: ValidationErrors,
26
- ): SubagentTurnBudget | undefined {
27
- if (value === undefined) return undefined;
28
- if (!isJsonObject(value)) {
29
- errors.push(`${path}: expected an object`);
30
- return undefined;
31
- }
32
- rejectUnknownKeys(value, ['maxTurns', 'graceTurns'], path, errors);
33
- const maxTurns = readInteger(value.maxTurns, 0, `${path}.maxTurns`, errors, {
34
- min: 1,
35
- max: 1_000,
36
- });
37
- const graceTurns =
38
- value.graceTurns === undefined
39
- ? undefined
40
- : readInteger(value.graceTurns, 0, `${path}.graceTurns`, errors, {
41
- min: 0,
42
- max: 100,
43
- });
44
- if (maxTurns === 0) return undefined;
45
- return {
46
- maxTurns,
47
- ...(graceTurns !== undefined ? { graceTurns } : {}),
48
- };
49
- }
50
-
51
- function parseSubagentToolBudget(
52
- value: unknown,
53
- path: string,
54
- errors: ValidationErrors,
55
- ): SubagentToolBudget | undefined {
56
- if (value === undefined) return undefined;
57
- if (!isJsonObject(value)) {
58
- errors.push(`${path}: expected an object`);
59
- return undefined;
60
- }
61
- rejectUnknownKeys(value, ['soft', 'hard', 'block'], path, errors);
62
- const hard = readInteger(value.hard, 0, `${path}.hard`, errors, {
63
- min: 1,
64
- max: 100_000,
65
- });
66
- const soft =
67
- value.soft === undefined
68
- ? undefined
69
- : readInteger(value.soft, 0, `${path}.soft`, errors, {
70
- min: 1,
71
- max: 100_000,
72
- });
73
-
74
- let block: Array<string> | '*' | undefined;
75
- if (value.block === '*') {
76
- block = '*';
77
- } else if (value.block !== undefined) {
78
- block = readStringList(value.block, `${path}.block`, errors, TOOL_PATTERN);
79
- if (block.length === 0) {
80
- errors.push(`${path}.block: expected "*" or at least one tool name`);
81
- }
82
- }
83
- if (soft !== undefined && hard > 0 && soft > hard) {
84
- errors.push(`${path}.soft: must not exceed hard`);
85
- }
86
- if (hard === 0) return undefined;
87
- return {
88
- hard,
89
- ...(soft !== undefined ? { soft } : {}),
90
- ...(block !== undefined ? { block } : {}),
91
- };
92
- }
93
-
94
- /** Parse an optional isolated-step subagent configuration. */
95
- export function parseStepSubagent(
96
- value: unknown,
97
- path: string,
98
- errors: ValidationErrors,
99
- ): StepSubagent | undefined {
100
- if (value === undefined) return undefined;
101
- if (typeof value === 'string') {
102
- const agent =
103
- readString(value, path, errors, {
104
- pattern: SUBAGENT_RUNTIME_NAME_PATTERN,
105
- }) ?? DEFAULT_STEP_SUBAGENT.agent;
106
- return { ...DEFAULT_STEP_SUBAGENT, agent };
107
- }
108
- if (!isJsonObject(value)) {
109
- errors.push(`${path}: expected an agent profile name or object`);
110
- return undefined;
111
- }
112
- rejectUnknownKeys(
113
- value,
114
- [
115
- 'agent',
116
- 'context',
117
- 'model',
118
- 'timeoutMs',
119
- 'turnBudget',
120
- 'toolBudget',
121
- 'artifacts',
122
- 'retryToolFailures',
123
- ],
124
- path,
125
- errors,
126
- );
127
-
128
- const agent =
129
- value.agent === undefined
130
- ? DEFAULT_STEP_SUBAGENT.agent
131
- : (readString(value.agent, `${path}.agent`, errors, {
132
- pattern: SUBAGENT_RUNTIME_NAME_PATTERN,
133
- }) ?? DEFAULT_STEP_SUBAGENT.agent);
134
- const contextValue =
135
- value.context === undefined
136
- ? DEFAULT_STEP_SUBAGENT.context
137
- : readString(value.context, `${path}.context`, errors);
138
- const context: SubagentContext = DEFAULT_STEP_SUBAGENT.context;
139
- if (contextValue !== 'fresh') {
140
- errors.push(`${path}.context: expected fresh`);
141
- }
142
- const model =
143
- value.model === undefined
144
- ? undefined
145
- : readString(value.model, `${path}.model`, errors, {
146
- pattern: RESOURCE_SELECTOR_PATTERN,
147
- });
148
- const timeoutMs = readInteger(
149
- value.timeoutMs,
150
- DEFAULT_STEP_SUBAGENT.timeoutMs,
151
- `${path}.timeoutMs`,
152
- errors,
153
- { min: 1_000, max: 86_400_000 },
154
- );
155
- const turnBudget = parseSubagentTurnBudget(
156
- value.turnBudget,
157
- `${path}.turnBudget`,
158
- errors,
159
- );
160
- const toolBudget = parseSubagentToolBudget(
161
- value.toolBudget,
162
- `${path}.toolBudget`,
163
- errors,
164
- );
165
- const artifacts = readBoolean(
166
- value.artifacts,
167
- DEFAULT_STEP_SUBAGENT.artifacts,
168
- `${path}.artifacts`,
169
- errors,
170
- );
171
- const retryToolFailures = readBoolean(
172
- value.retryToolFailures,
173
- DEFAULT_STEP_SUBAGENT.retryToolFailures,
174
- `${path}.retryToolFailures`,
175
- errors,
176
- );
177
- return {
178
- agent,
179
- context,
180
- ...(model ? { model } : {}),
181
- timeoutMs,
182
- ...(turnBudget ? { turnBudget } : {}),
183
- ...(toolBudget ? { toolBudget } : {}),
184
- artifacts,
185
- retryToolFailures,
186
- };
187
- }
188
-
189
- /** Parse the subagent limits inside the user permission ceiling. */
190
- export function parseSubagentPermissionCeiling(
191
- value: unknown,
192
- path: string,
193
- errors: ValidationErrors,
194
- ): SubagentPermissionCeiling | undefined {
195
- if (!isJsonObject(value)) {
196
- errors.push(`${path}: expected an object`);
197
- return undefined;
198
- }
199
- rejectUnknownKeys(
200
- value,
201
- [
202
- 'agents',
203
- 'contexts',
204
- 'models',
205
- 'maxTimeoutMs',
206
- 'maxTurns',
207
- 'maxGraceTurns',
208
- 'maxToolCalls',
209
- 'artifacts',
210
- 'retryToolFailures',
211
- ],
212
- path,
213
- errors,
214
- );
215
- const agents = readStringList(
216
- value.agents,
217
- `${path}.agents`,
218
- errors,
219
- SUBAGENT_RUNTIME_NAME_PATTERN,
220
- );
221
- const contexts = readStringList(
222
- value.contexts,
223
- `${path}.contexts`,
224
- errors,
225
- /^fresh$/,
226
- ).filter((context): context is SubagentContext => context === 'fresh');
227
- const models = readStringList(
228
- value.models,
229
- `${path}.models`,
230
- errors,
231
- RESOURCE_SELECTOR_PATTERN,
232
- );
233
- if (agents.length === 0) {
234
- errors.push(`${path}.agents: at least one subagent is required`);
235
- }
236
- if (contexts.length === 0) {
237
- errors.push(`${path}.contexts: at least one context mode is required`);
238
- }
239
- (
240
- [
241
- 'maxTimeoutMs',
242
- 'maxTurns',
243
- 'maxGraceTurns',
244
- 'maxToolCalls',
245
- 'artifacts',
246
- ] as const
247
- )
248
- .filter((field) => value[field] === undefined)
249
- .forEach((field) => errors.push(`${path}.${field}: required`));
250
-
251
- return {
252
- agents,
253
- contexts,
254
- models,
255
- maxTimeoutMs: readInteger(
256
- value.maxTimeoutMs,
257
- 0,
258
- `${path}.maxTimeoutMs`,
259
- errors,
260
- { min: 1_000, max: 86_400_000 },
261
- ),
262
- maxTurns: readInteger(value.maxTurns, 0, `${path}.maxTurns`, errors, {
263
- min: 1,
264
- max: 1_000,
265
- }),
266
- maxGraceTurns: readInteger(
267
- value.maxGraceTurns,
268
- 0,
269
- `${path}.maxGraceTurns`,
270
- errors,
271
- { min: 0, max: 100 },
272
- ),
273
- maxToolCalls: readInteger(
274
- value.maxToolCalls,
275
- 0,
276
- `${path}.maxToolCalls`,
277
- errors,
278
- { min: 1, max: 100_000 },
279
- ),
280
- artifacts: readBoolean(value.artifacts, false, `${path}.artifacts`, errors),
281
- retryToolFailures: readBoolean(
282
- value.retryToolFailures,
283
- false,
284
- `${path}.retryToolFailures`,
285
- errors,
286
- ),
287
- };
288
- }
@@ -1,248 +0,0 @@
1
- import type { WorkflowStepResult } from '../runtime/step-result.ts';
2
- import {
3
- failedToolName,
4
- formatToolFailureDiagnostic,
5
- type CompletedDelegationTranscriptAudit,
6
- } from '../integrations/subagents/diagnostics.ts';
7
- import type {
8
- ChildStepPolicy,
9
- SubagentDelegationResponse,
10
- } from '../integrations/subagents/protocol.ts';
11
- import type { WorkflowHarnessDependencies } from './dependencies.ts';
12
- import {
13
- completionMatchesResult,
14
- recoveredProjectionError,
15
- } from './delegation-recovery-validation.ts';
16
- import {
17
- boundedFailureField,
18
- delegationFailureFingerprint,
19
- isRetryableTerminalFailure,
20
- isSafeToRetryDelegation,
21
- rejectedRecoveryReason,
22
- } from './delegation-retry-policy.ts';
23
- import type {
24
- ActiveDelegation,
25
- DelegationFailureDetails,
26
- DelegationRecoveryBlocker,
27
- } from './types.ts';
28
-
29
- export type DelegationFailureActions = {
30
- delegationFailureFingerprint: (failure: DelegationFailureDetails) => string;
31
- hasContradictoryCompletion: (response: SubagentDelegationResponse) => boolean;
32
- completedResponseAudit: (
33
- active: ActiveDelegation,
34
- response: SubagentDelegationResponse,
35
- ) => Promise<CompletedDelegationTranscriptAudit>;
36
- isRetryableTerminalFailure: (failure: DelegationFailureDetails) => boolean;
37
- isSafeToRetryDelegation: (
38
- policy: ChildStepPolicy,
39
- isReplayExplicitlyAuthorized: boolean,
40
- replayAudit: DelegationFailureDetails['replayAudit'],
41
- ) => boolean;
42
- rejectedRecoveryReason: (
43
- failure: DelegationFailureDetails,
44
- error: unknown,
45
- ) => string;
46
- completionMatchesResult: (
47
- diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
48
- result: WorkflowStepResult,
49
- policy: ChildStepPolicy,
50
- ) => boolean;
51
- recoveredProjectionError: (
52
- active: ActiveDelegation,
53
- response: SubagentDelegationResponse,
54
- diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
55
- ) => string | undefined;
56
- describeDelegationFailure: (
57
- active: ActiveDelegation,
58
- response: SubagentDelegationResponse,
59
- ) => Promise<DelegationFailureDetails>;
60
- };
61
-
62
- function nonEmptyTerminalError(
63
- response: SubagentDelegationResponse,
64
- ): string | undefined {
65
- return [response.error, response.execution?.error].find(
66
- (error): error is string =>
67
- typeof error === 'string' && error.trim().length > 0,
68
- );
69
- }
70
-
71
- function nonzeroTerminalExitCode(
72
- response: SubagentDelegationResponse,
73
- ): number | undefined {
74
- return [response.exitCode, response.execution?.exitCode].find(
75
- (exitCode): exitCode is number =>
76
- typeof exitCode === 'number' &&
77
- Number.isSafeInteger(exitCode) &&
78
- exitCode !== 0,
79
- );
80
- }
81
-
82
- function hasContradictoryCompletion(
83
- response: SubagentDelegationResponse,
84
- ): boolean {
85
- return (
86
- response.status === 'completed' &&
87
- (nonEmptyTerminalError(response) !== undefined ||
88
- nonzeroTerminalExitCode(response) !== undefined)
89
- );
90
- }
91
-
92
- function recoveryBlocker(
93
- response: SubagentDelegationResponse,
94
- ): DelegationRecoveryBlocker | undefined {
95
- const execution = response.execution;
96
- const fileMutation = response.effects?.fileMutation;
97
- if (fileMutation?.attempted === true || fileMutation?.status === 'observed') {
98
- return 'reported-mutation';
99
- }
100
- if (execution?.detached === true || execution?.status === 'detached') {
101
- return 'detached';
102
- }
103
- if (execution?.stopped === true || execution?.status === 'stopped') {
104
- return 'stopped';
105
- }
106
- if (
107
- response.status === 'interrupted' ||
108
- execution?.interrupted === true ||
109
- execution?.status === 'paused'
110
- ) {
111
- return 'interrupted';
112
- }
113
- if (response.status === 'cancelled') return 'cancelled';
114
- if (execution?.timedOut === true && response.status !== 'timed_out') {
115
- return 'inconsistent-timeout';
116
- }
117
- return undefined;
118
- }
119
-
120
- function validateReplayAudit(
121
- response: SubagentDelegationResponse,
122
- replayAudit: DelegationFailureDetails['replayAudit'],
123
- ): DelegationFailureDetails['replayAudit'] {
124
- if (!replayAudit) return undefined;
125
- if (
126
- response.toolCount !== undefined &&
127
- response.toolCount !== replayAudit.toolCount
128
- ) {
129
- return { ...replayAudit, replaySafe: false };
130
- }
131
- return replayAudit;
132
- }
133
-
134
- /**
135
- * Creates pure delegation-failure evaluators plus the injected diagnostic
136
- * reader used at the transcript I/O boundary.
137
- */
138
- export function createDelegationFailureActions(
139
- dependencies: Pick<
140
- WorkflowHarnessDependencies,
141
- 'readDelegationReplayAudit' | 'readToolFailureDiagnostic'
142
- > &
143
- Partial<
144
- Pick<WorkflowHarnessDependencies, 'auditCompletedDelegationTranscript'>
145
- >,
146
- ): DelegationFailureActions {
147
- const responseIdentity = (
148
- active: ActiveDelegation,
149
- response: SubagentDelegationResponse,
150
- ): { readonly runId: string; readonly childIndex: 0 } | undefined => {
151
- if (
152
- response.childIndex !== 0 ||
153
- typeof response.runId !== 'string' ||
154
- (response.agent !== undefined && response.agent !== active.agent)
155
- ) {
156
- return undefined;
157
- }
158
- return { runId: response.runId, childIndex: 0 };
159
- };
160
-
161
- const completedResponseAudit = async (
162
- active: ActiveDelegation,
163
- response: SubagentDelegationResponse,
164
- ): Promise<CompletedDelegationTranscriptAudit> => {
165
- if (!dependencies.auditCompletedDelegationTranscript) {
166
- return {
167
- verified: false,
168
- reason: 'completed transcript auditing is unavailable',
169
- };
170
- }
171
- return dependencies.auditCompletedDelegationTranscript(
172
- response.sessionFile,
173
- active.trustedSessionRoot,
174
- responseIdentity(active, response),
175
- );
176
- };
177
-
178
- const describeDelegationFailure = async (
179
- active: ActiveDelegation,
180
- response: SubagentDelegationResponse,
181
- ): Promise<DelegationFailureDetails> => {
182
- const blocker = recoveryBlocker(response);
183
- const terminalError = nonEmptyTerminalError(response);
184
- const error =
185
- terminalError ?? 'The subagent returned no terminal error details.';
186
- const identity = responseIdentity(active, response);
187
- const [diagnostic, replayAudit] = await Promise.all([
188
- dependencies.readToolFailureDiagnostic(
189
- response.sessionFile,
190
- active.trustedSessionRoot,
191
- identity,
192
- failedToolName(terminalError),
193
- terminalError,
194
- ),
195
- dependencies.readDelegationReplayAudit(
196
- response.sessionFile,
197
- active.trustedSessionRoot,
198
- identity,
199
- {
200
- task: active.transcriptTask,
201
- bashPermission: active.policy.permissions.bash,
202
- },
203
- ),
204
- ]);
205
- const validatedReplayAudit = validateReplayAudit(response, replayAudit);
206
- const exitCode =
207
- nonzeroTerminalExitCode(response) ??
208
- response.exitCode ??
209
- response.execution?.exitCode;
210
- const reason = [
211
- hasContradictoryCompletion(response)
212
- ? `Subagent "${active.agent}" reported terminal failure signals with completed status.`
213
- : `Subagent "${active.agent}" ${response.status.replaceAll('_', ' ')}.`,
214
- ...(diagnostic ? formatToolFailureDiagnostic(diagnostic) : []),
215
- ...(exitCode !== undefined ? [`Subagent exit code: ${exitCode}`] : []),
216
- ...(blocker
217
- ? [`Automatic recovery blocked by: ${blocker.replaceAll('-', ' ')}`]
218
- : []),
219
- `Terminal error: ${boundedFailureField(error)}`,
220
- ...(diagnostic && response.sessionFile
221
- ? [
222
- `Diagnostic session: ${boundedFailureField(response.sessionFile.replaceAll(/\s+/g, ' '))}`,
223
- ]
224
- : []),
225
- ].join('\n');
226
- return {
227
- reason,
228
- status: response.status,
229
- ...(terminalError ? { error: terminalError } : {}),
230
- ...(exitCode !== undefined ? { exitCode } : {}),
231
- ...(blocker ? { recoveryBlocker: blocker } : {}),
232
- ...(diagnostic ? { diagnostic } : {}),
233
- ...(validatedReplayAudit ? { replayAudit: validatedReplayAudit } : {}),
234
- };
235
- };
236
-
237
- return {
238
- delegationFailureFingerprint,
239
- hasContradictoryCompletion,
240
- completedResponseAudit,
241
- isRetryableTerminalFailure,
242
- isSafeToRetryDelegation,
243
- rejectedRecoveryReason,
244
- completionMatchesResult,
245
- recoveredProjectionError,
246
- describeDelegationFailure,
247
- };
248
- }
@@ -1,161 +0,0 @@
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
- ...(result.workspace === undefined ? [] : ['workspace']),
29
- ].sort();
30
- const actualKeys = Object.keys(value).sort();
31
- if (
32
- actualKeys.length !== expectedKeys.length ||
33
- !actualKeys.every((key, index) => key === expectedKeys[index])
34
- ) {
35
- return false;
36
- }
37
- try {
38
- const completion = parseDelegatedStepResult(
39
- {
40
- ...value,
41
- version: 1,
42
- policyDigest: policy.policyDigest,
43
- },
44
- policy,
45
- );
46
- return (
47
- completion.outcome === result.outcome &&
48
- completion.summary === result.summary &&
49
- completion.artifact === result.artifact &&
50
- completion.workspace?.cwd === result.workspace?.cwd
51
- );
52
- } catch {
53
- return false;
54
- }
55
- }
56
-
57
- /**
58
- * Finds contradictory or unsafe fields in a recovered terminal projection.
59
- *
60
- * @param active - Active delegation identity.
61
- * @param response - Terminal response received from the child.
62
- * @param diagnostic - Transcript evidence correlated to the failure.
63
- * @returns A rejection reason, or undefined for a valid projection.
64
- */
65
- export function recoveredProjectionError(
66
- active: ActiveDelegation,
67
- response: SubagentDelegationResponse,
68
- diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
69
- ): string | undefined {
70
- if (response.agent !== active.agent) {
71
- return `terminal agent identity is ${JSON.stringify(response.agent)}; expected ${JSON.stringify(active.agent)}`;
72
- }
73
- if (response.childIndex !== 0) {
74
- return `terminal child index is ${JSON.stringify(response.childIndex)}; expected 0`;
75
- }
76
- if (
77
- typeof response.exitCode !== 'number' ||
78
- !Number.isSafeInteger(response.exitCode) ||
79
- response.exitCode <= 0
80
- ) {
81
- return `terminal exit code is ${JSON.stringify(response.exitCode)}; expected a positive safe integer`;
82
- }
83
- const execution = response.execution;
84
- if (!execution) return 'terminal response has no execution projection';
85
- if (execution.status !== 'failed' || execution.success) {
86
- return `execution projection is ${JSON.stringify({
87
- status: execution.status,
88
- success: execution.success,
89
- })}; expected failed/false`;
90
- }
91
- if (execution.exitCode !== response.exitCode) {
92
- return `execution exit code ${JSON.stringify(execution.exitCode)} does not match terminal exit code ${JSON.stringify(response.exitCode)}`;
93
- }
94
- if (
95
- typeof response.error !== 'string' ||
96
- !response.error ||
97
- typeof execution.error !== 'string' ||
98
- execution.error !== response.error
99
- ) {
100
- return 'terminal and execution errors are missing or do not match exactly';
101
- }
102
- const warnings = response.warnings as unknown;
103
- if (
104
- warnings !== undefined &&
105
- (!Array.isArray(warnings) ||
106
- warnings.some(
107
- (warning) => typeof warning !== 'string' || warning.trim().length > 0,
108
- ))
109
- ) {
110
- return `terminal response contains warning evidence: ${JSON.stringify(warnings)}`;
111
- }
112
- if (
113
- diagnostic.transcriptToolCount !== undefined &&
114
- response.toolCount !== undefined &&
115
- response.toolCount !== diagnostic.transcriptToolCount
116
- ) {
117
- return `terminal tool count ${response.toolCount} does not match transcript tool count ${diagnostic.transcriptToolCount}`;
118
- }
119
- if (
120
- diagnostic.transcriptTurnCount !== undefined &&
121
- response.turns !== undefined &&
122
- response.turns !== diagnostic.transcriptTurnCount
123
- ) {
124
- return `terminal turn count ${response.turns} does not match transcript turn count ${diagnostic.transcriptTurnCount}`;
125
- }
126
- const toolFailure = response.error.match(
127
- /^\s*([a-z][\w-]*) failed\s*\(exit\s+(\d+)\)\s*:/i,
128
- );
129
- if (!toolFailure) {
130
- return 'terminal error is not a recognized "<tool> failed (exit N): <detail>" failure';
131
- }
132
- const terminalTool = toolFailure[1];
133
- const terminalExitCodeText = toolFailure[2];
134
- if (!terminalTool || !terminalExitCodeText) {
135
- return 'terminal error does not contain a complete tool failure projection';
136
- }
137
- const terminalExitCode = Number(terminalExitCodeText);
138
- if (
139
- terminalTool.toLowerCase() !== diagnostic.tool.toLowerCase() ||
140
- terminalExitCode !== response.exitCode
141
- ) {
142
- return `terminal tool/exit ${JSON.stringify({
143
- tool: terminalTool,
144
- exitCode: terminalExitCode,
145
- })} does not match the correlated failure ${JSON.stringify({
146
- tool: diagnostic.tool,
147
- exitCode: response.exitCode,
148
- })}`;
149
- }
150
- const unsafeFlag = (
151
- [
152
- ['interrupted', execution.interrupted],
153
- ['timedOut', execution.timedOut],
154
- ['stopped', execution.stopped],
155
- ['detached', execution.detached],
156
- ] as const
157
- ).find(([, isEnabled]) => isEnabled === true)?.[0];
158
- return unsafeFlag
159
- ? `execution projection reports ${unsafeFlag}=true`
160
- : undefined;
161
- }