@wichayutdew/pi-workflows 1.0.0 → 2.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 (131) hide show
  1. package/README.md +312 -221
  2. package/agents/step.md +4 -11
  3. package/dist/index.js +2556 -961
  4. package/examples/mr-comments.workflow.yaml +18 -8
  5. package/examples/prompts/mr-comments/implement.md +13 -9
  6. package/examples/prompts/mr-comments/plan.md +44 -15
  7. package/examples/prompts/mr-comments/verify.md +8 -7
  8. package/examples/settings.yaml +7 -1
  9. package/examples/starter-kit/mr-comment.workflow.yaml +149 -0
  10. package/examples/starter-kit/mr-review.workflow.yaml +130 -0
  11. package/examples/starter-kit/steps/mr-comment/fetch.md +33 -0
  12. package/examples/starter-kit/steps/mr-comment/implement.md +38 -0
  13. package/examples/starter-kit/steps/mr-comment/plan.md +62 -0
  14. package/examples/starter-kit/steps/mr-comment/publish.md +36 -0
  15. package/examples/starter-kit/steps/mr-comment/verify.md +35 -0
  16. package/examples/starter-kit/steps/mr-review/fetch.md +25 -0
  17. package/examples/starter-kit/steps/mr-review/publish.md +30 -0
  18. package/examples/starter-kit/steps/mr-review/review.md +69 -0
  19. package/examples/starter-kit/steps/mr-review/verify.md +24 -0
  20. package/examples/starter-kit/steps/shared/prepare-workspace.md +57 -0
  21. package/examples/starter-kit/steps/ticket/implement.md +35 -0
  22. package/examples/starter-kit/steps/ticket/plan.md +40 -0
  23. package/examples/starter-kit/steps/ticket/verify.md +29 -0
  24. package/examples/starter-kit/steps/work/implement.md +34 -0
  25. package/examples/starter-kit/steps/work/plan.md +38 -0
  26. package/examples/starter-kit/steps/work/verify.md +30 -0
  27. package/examples/starter-kit/ticket.workflow.yaml +100 -0
  28. package/examples/starter-kit/work.workflow.yaml +97 -0
  29. package/package.json +3 -3
  30. package/schemas/workflow.schema.json +78 -35
  31. package/src/command-names.ts +1 -0
  32. package/src/commands.ts +33 -4
  33. package/src/config/ceiling.ts +6 -8
  34. package/src/config/load-workflows.ts +12 -1
  35. package/src/config/step-digests.ts +19 -0
  36. package/src/config/types.ts +17 -11
  37. package/src/config/validation/permissions.ts +5 -45
  38. package/src/config/validation/prompt.ts +3 -0
  39. package/src/config/validation/shared.ts +0 -2
  40. package/src/config/validation/step.ts +106 -2
  41. package/src/config/validation/workflow.ts +76 -0
  42. package/src/engine/create-run.ts +4 -0
  43. package/src/engine/gate-transitions.ts +58 -6
  44. package/src/engine/reconciliation-history.ts +61 -24
  45. package/src/engine/run-advance.ts +37 -4
  46. package/src/engine/run-lifecycle.ts +11 -0
  47. package/src/engine/run-reconciliation.ts +51 -21
  48. package/src/engine/run-validation.ts +236 -0
  49. package/src/engine/run-workflow-validation.ts +191 -0
  50. package/src/engine/state-types.ts +109 -4
  51. package/src/engine/state.ts +15 -1
  52. package/src/engine/step-trace.ts +495 -0
  53. package/src/engine/transitions.ts +1 -0
  54. package/src/harness/action-context.ts +31 -6
  55. package/src/harness/core-actions.ts +30 -1
  56. package/src/harness/delegation-control-actions.ts +27 -2
  57. package/src/harness/delegation-failure.ts +42 -9
  58. package/src/harness/delegation-plan.ts +96 -40
  59. package/src/harness/delegation-recovery-validation.ts +3 -1
  60. package/src/harness/delegation-response-actions.ts +73 -3
  61. package/src/harness/delegation-retry-policy.ts +5 -3
  62. package/src/harness/dependencies.ts +11 -2
  63. package/src/harness/gate-submission-action.ts +5 -31
  64. package/src/harness/lifecycle-actions.ts +4 -5
  65. package/src/harness/pause-actions.ts +10 -1
  66. package/src/harness/plannotator-result-actions.ts +20 -11
  67. package/src/harness/prompt-gate-actions.ts +40 -2
  68. package/src/harness/resume-action.ts +75 -1
  69. package/src/harness/start-actions.ts +77 -3
  70. package/src/harness/status-actions.ts +7 -17
  71. package/src/harness/step-effects.ts +61 -0
  72. package/src/harness/step-execution-actions.ts +181 -25
  73. package/src/harness/step-reporting.ts +168 -0
  74. package/src/harness/types.ts +1 -0
  75. package/src/harness/workspace-directory.ts +67 -0
  76. package/src/harness.ts +37 -9
  77. package/src/integrations/plannotator-requests.ts +40 -5
  78. package/src/integrations/subagents/child-policy-sections.ts +71 -72
  79. package/src/integrations/subagents/child-policy-types.ts +7 -7
  80. package/src/integrations/subagents/child-policy-validation.ts +10 -4
  81. package/src/integrations/subagents/child-runtime-completion.ts +1 -0
  82. package/src/integrations/subagents/child-runtime-files.ts +29 -86
  83. package/src/integrations/subagents/child-runtime-policy.ts +8 -33
  84. package/src/integrations/subagents/child-runtime.ts +3 -15
  85. package/src/integrations/subagents/delegated-result.ts +1 -0
  86. package/src/integrations/subagents/diagnostic-format.ts +5 -0
  87. package/src/integrations/subagents/diagnostic-types.ts +7 -1
  88. package/src/integrations/subagents/diagnostics.ts +2 -0
  89. package/src/integrations/subagents/failure-correlation.ts +52 -2
  90. package/src/integrations/subagents/failure-transcript.ts +60 -1
  91. package/src/integrations/subagents/replay-audit.ts +0 -1
  92. package/src/integrations/subagents/replay-safety.ts +12 -27
  93. package/src/integrations/subagents/session-diagnostics.ts +102 -3
  94. package/src/policy/bash-authorization.ts +2 -21
  95. package/src/policy/bash-types.ts +0 -5
  96. package/src/policy/bash.ts +1 -2
  97. package/src/policy/tool-call-authorization.ts +1 -7
  98. package/src/preflight.ts +0 -5
  99. package/src/prompt/retry-task.ts +11 -3
  100. package/src/prompt/step-contract.ts +10 -56
  101. package/src/prompt/step-sections.ts +8 -10
  102. package/src/prompt/step-task.ts +41 -10
  103. package/src/prompt/template.ts +5 -2
  104. package/src/runtime/completion-tool.ts +14 -0
  105. package/src/runtime/main-step-completion.ts +3 -0
  106. package/src/runtime/main-step-policy.ts +2 -1
  107. package/src/runtime/main-step-runtime-types.ts +9 -1
  108. package/src/runtime/main-step-runtime.ts +2 -0
  109. package/src/runtime/main-step-state.ts +6 -0
  110. package/src/runtime/main-step-trace.ts +45 -0
  111. package/src/runtime/step-result.ts +57 -0
  112. package/src/step-log.ts +178 -0
  113. package/src/workflow-doctor.ts +254 -0
  114. package/src/workflow-status/format-status.ts +32 -3
  115. package/src/workflow-status/render-board.ts +7 -4
  116. package/src/workflow-status/render-path.ts +51 -24
  117. package/src/workflow-status/render-step-detail.ts +379 -0
  118. package/src/workflow-status/render-summary.ts +22 -0
  119. package/src/workflow-status/transcript-reader.ts +231 -0
  120. package/src/workflow-status/types.ts +8 -0
  121. package/src/workflow-status/view.ts +161 -14
  122. package/src/workflow-status.ts +6 -0
  123. package/src/policy/approved-command-extraction.ts +0 -71
  124. package/src/policy/approved-commands.ts +0 -15
  125. package/src/policy/bash-read-only.ts +0 -161
  126. package/src/policy/restricted-git.ts +0 -30
  127. package/src/policy/reviewed-artifact.ts +0 -97
  128. package/src/policy/reviewed-command-safety.ts +0 -150
  129. package/src/policy/reviewed-command-shape.ts +0 -49
  130. package/src/policy/reviewed-repository-contract.ts +0 -138
  131. package/src/policy/reviewed-repository-cwd.ts +0 -102
@@ -4,6 +4,10 @@ import {
4
4
  boundedFailureField,
5
5
  MAX_DELEGATION_RECOVERY_ATTEMPTS,
6
6
  } from './delegation-retry-policy.ts';
7
+ import {
8
+ conciseStepFailureSummary,
9
+ reportFailedStep,
10
+ } from './step-reporting.ts';
7
11
  import type { ActiveDelegation, DelegationFailureDetails } from './types.ts';
8
12
 
9
13
  type HarnessActionContext = Pick<
@@ -20,6 +24,7 @@ type HarnessActionContext = Pick<
20
24
  | 'mainSteps'
21
25
  | 'pauseForExecutionFailure'
22
26
  | 'persist'
27
+ | 'pi'
23
28
  | 'restoreBaselineTools'
24
29
  | 'run'
25
30
  | 'sessionEpoch'
@@ -45,11 +50,13 @@ export type DelegationControlActions = {
45
50
  pauseForDelegationFailure: (
46
51
  this: HarnessActionContext,
47
52
  reason: string,
53
+ failureSummary?: string,
48
54
  ) => void;
49
55
  pauseForExecutionFailure: (
50
56
  this: HarnessActionContext,
51
57
  label: string,
52
58
  reason: string,
59
+ failureSummary?: string,
53
60
  ) => void;
54
61
  retainUnconfirmedDelegation: (
55
62
  this: HarnessActionContext,
@@ -162,14 +169,16 @@ function retryDelegationAfterFailure(
162
169
  function pauseForDelegationFailure(
163
170
  this: HarnessActionContext,
164
171
  reason: string,
172
+ failureSummary = reason,
165
173
  ): void {
166
- this.pauseForExecutionFailure('Subagent step', reason);
174
+ this.pauseForExecutionFailure('Subagent step', reason, failureSummary);
167
175
  }
168
176
 
169
177
  function pauseForExecutionFailure(
170
178
  this: HarnessActionContext,
171
179
  label: string,
172
180
  reason: string,
181
+ failureSummary = reason,
173
182
  ): void {
174
183
  if (!this.run || this.run.status !== 'running') return;
175
184
  this.mainSteps.deactivate();
@@ -179,6 +188,12 @@ function pauseForExecutionFailure(
179
188
  this.dependencies.now(),
180
189
  );
181
190
  this.persist();
191
+ reportFailedStep(
192
+ this.pi,
193
+ this.catalog.workflows.get(this.run.workflowId),
194
+ this.run,
195
+ failureSummary,
196
+ );
182
197
  if (this.activeDelegation) {
183
198
  this.isolateMainSessionTools();
184
199
  } else {
@@ -186,7 +201,7 @@ function pauseForExecutionFailure(
186
201
  }
187
202
  this.updateStatus();
188
203
  this.latestContext?.ui.notify(
189
- `Workflow paused at "${this.run.currentStepId}": ${reason}`,
204
+ `Workflow paused at "${this.run.currentStepId}": ${conciseStepFailureSummary(failureSummary)}`,
190
205
  'error',
191
206
  );
192
207
  }
@@ -198,6 +213,7 @@ function retainUnconfirmedDelegation(
198
213
  ): void {
199
214
  active.cancelling = true;
200
215
  active.progress = 'cancellation unconfirmed';
216
+ let didFail = false;
201
217
  if (this.run?.status === 'running') {
202
218
  this.run = failRun(
203
219
  this.run,
@@ -205,6 +221,15 @@ function retainUnconfirmedDelegation(
205
221
  this.dependencies.now(),
206
222
  );
207
223
  this.persist();
224
+ didFail = true;
225
+ }
226
+ if (didFail && this.run) {
227
+ reportFailedStep(
228
+ this.pi,
229
+ this.catalog.workflows.get(this.run.workflowId),
230
+ this.run,
231
+ reason,
232
+ );
208
233
  }
209
234
  this.isolateMainSessionTools();
210
235
  this.updateStatus();
@@ -2,6 +2,7 @@ import type { WorkflowStepResult } from '../runtime/step-result.ts';
2
2
  import {
3
3
  failedToolName,
4
4
  formatToolFailureDiagnostic,
5
+ type CompletedDelegationTranscriptAudit,
5
6
  } from '../integrations/subagents/diagnostics.ts';
6
7
  import type {
7
8
  ChildStepPolicy,
@@ -28,6 +29,10 @@ import type {
28
29
  export type DelegationFailureActions = {
29
30
  delegationFailureFingerprint: (failure: DelegationFailureDetails) => string;
30
31
  hasContradictoryCompletion: (response: SubagentDelegationResponse) => boolean;
32
+ completedResponseAudit: (
33
+ active: ActiveDelegation,
34
+ response: SubagentDelegationResponse,
35
+ ) => Promise<CompletedDelegationTranscriptAudit>;
31
36
  isRetryableTerminalFailure: (failure: DelegationFailureDetails) => boolean;
32
37
  isSafeToRetryDelegation: (
33
38
  policy: ChildStepPolicy,
@@ -134,8 +139,42 @@ export function createDelegationFailureActions(
134
139
  dependencies: Pick<
135
140
  WorkflowHarnessDependencies,
136
141
  'readDelegationReplayAudit' | 'readToolFailureDiagnostic'
137
- >,
142
+ > &
143
+ Partial<
144
+ Pick<WorkflowHarnessDependencies, 'auditCompletedDelegationTranscript'>
145
+ >,
138
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
+
139
178
  const describeDelegationFailure = async (
140
179
  active: ActiveDelegation,
141
180
  response: SubagentDelegationResponse,
@@ -144,13 +183,7 @@ export function createDelegationFailureActions(
144
183
  const terminalError = nonEmptyTerminalError(response);
145
184
  const error =
146
185
  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;
186
+ const identity = responseIdentity(active, response);
154
187
  const [diagnostic, replayAudit] = await Promise.all([
155
188
  dependencies.readToolFailureDiagnostic(
156
189
  response.sessionFile,
@@ -166,7 +199,6 @@ export function createDelegationFailureActions(
166
199
  {
167
200
  task: active.transcriptTask,
168
201
  bashPermission: active.policy.permissions.bash,
169
- approvedBashCommands: active.policy.approvedBashCommands ?? [],
170
202
  },
171
203
  ),
172
204
  ]);
@@ -205,6 +237,7 @@ export function createDelegationFailureActions(
205
237
  return {
206
238
  delegationFailureFingerprint,
207
239
  hasContradictoryCompletion,
240
+ completedResponseAudit,
208
241
  isRetryableTerminalFailure,
209
242
  isSafeToRetryDelegation,
210
243
  rejectedRecoveryReason,
@@ -9,10 +9,6 @@ import {
9
9
  type ChildStepPolicy,
10
10
  type SubagentDelegationRequest,
11
11
  } from '../integrations/subagents/protocol.ts';
12
- import {
13
- narrowApprovedBashCommands,
14
- resolveReviewedRepositoryCwd,
15
- } from '../policy/approved-commands.ts';
16
12
  import { automaticRecoveryTask, buildDelegatedStepTask } from '../prompt.ts';
17
13
  import { WORKFLOW_COMPLETION_PARAMETERS } from '../runtime/completion-tool.ts';
18
14
  import { MAX_DELEGATION_RECOVERY_ATTEMPTS } from './delegation-retry-policy.ts';
@@ -46,17 +42,6 @@ function delegationTranscriptBinding(
46
42
  return `<pi-workflows-delegation-binding-v1>${requestId}:${policyDigest}</pi-workflows-delegation-binding-v1>`;
47
43
  }
48
44
 
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
45
  /**
61
46
  * Creates one immutable subagent request from workflow state and injected
62
47
  * identity/workspace effects.
@@ -65,7 +50,9 @@ export function createDelegationPlan(
65
50
  input: DelegationPlanInput,
66
51
  dependencies: Pick<
67
52
  WorkflowHarnessDependencies,
68
- 'createDelegationWorkspace' | 'createRequestId' | 'currentWorkingDirectory'
53
+ | 'createDelegationWorkspace'
54
+ | 'createRequestId'
55
+ | 'resolveWorkspaceDirectory'
69
56
  >,
70
57
  ): DelegationPlan {
71
58
  const { workflow, run, step, latestContext, recovery } = input;
@@ -76,33 +63,100 @@ export function createDelegationPlan(
76
63
  reason: `Step "${run.currentStepId}" has no subagent configuration`,
77
64
  };
78
65
  }
66
+ if (!run.cwd) {
67
+ return {
68
+ kind: 'invalid',
69
+ reason:
70
+ 'Workflow run has no captured working directory; abort it and start a new run',
71
+ };
72
+ }
73
+ if (!run.startCwd || !latestContext?.cwd) {
74
+ return {
75
+ kind: 'invalid',
76
+ reason:
77
+ 'Workflow start directory or current session directory is unavailable; abort it and start a new run',
78
+ };
79
+ }
79
80
 
80
- const reviewedRepository = resolveReviewedRepositoryCwd(
81
- run.reviewedArtifact ?? '',
82
- );
83
- if (reviewedRepository.kind === 'invalid') {
84
- return { kind: 'invalid', reason: reviewedRepository.reason };
81
+ let canonicalStartCwd: string;
82
+ let canonicalSessionCwd: string;
83
+ try {
84
+ canonicalStartCwd = dependencies.resolveWorkspaceDirectory({
85
+ candidateCwd: run.startCwd,
86
+ startCwd: run.startCwd,
87
+ allowedRoots: ['.'],
88
+ });
89
+ canonicalSessionCwd = dependencies.resolveWorkspaceDirectory({
90
+ candidateCwd: latestContext.cwd,
91
+ startCwd: latestContext.cwd,
92
+ allowedRoots: ['.'],
93
+ });
94
+ } catch (error) {
95
+ return {
96
+ kind: 'invalid',
97
+ reason: `Workflow start directory is no longer valid: ${
98
+ error instanceof Error ? error.message : String(error)
99
+ }`,
100
+ };
101
+ }
102
+ if (
103
+ canonicalStartCwd !== run.startCwd ||
104
+ canonicalSessionCwd !== canonicalStartCwd
105
+ ) {
106
+ return {
107
+ kind: 'invalid',
108
+ reason:
109
+ 'Current session cwd does not match the captured workflow start directory',
110
+ };
85
111
  }
86
112
 
87
- const delegationCwd =
88
- reviewedRepository.kind === 'resolved'
89
- ? reviewedRepository.cwd
90
- : (latestContext?.cwd ?? dependencies.currentWorkingDirectory());
113
+ let delegationCwd = run.cwd;
114
+ const bindingEntry = [...run.history]
115
+ .reverse()
116
+ .find((entry) => entry.workspaceCwd !== undefined);
117
+ if (!bindingEntry && run.cwd !== run.startCwd) {
118
+ return {
119
+ kind: 'invalid',
120
+ reason:
121
+ 'Workflow cwd differs from its start directory without a workspace binding',
122
+ };
123
+ }
124
+ if (bindingEntry) {
125
+ const binding = workflow.definition.steps[bindingEntry.stepId]?.workspace;
126
+ if (!binding || !run.startCwd || bindingEntry.workspaceCwd !== run.cwd) {
127
+ return {
128
+ kind: 'invalid',
129
+ reason:
130
+ 'Workflow workspace binding no longer matches its persisted configuration',
131
+ };
132
+ }
133
+ try {
134
+ delegationCwd = dependencies.resolveWorkspaceDirectory({
135
+ candidateCwd: run.cwd,
136
+ startCwd: run.startCwd,
137
+ allowedRoots: binding.allowedRoots,
138
+ });
139
+ } catch (error) {
140
+ return {
141
+ kind: 'invalid',
142
+ reason: `Workflow workspace is no longer valid: ${
143
+ error instanceof Error ? error.message : String(error)
144
+ }`,
145
+ };
146
+ }
147
+ if (delegationCwd !== run.cwd) {
148
+ return {
149
+ kind: 'invalid',
150
+ reason:
151
+ 'Workflow workspace no longer resolves to the bound canonical directory',
152
+ };
153
+ }
154
+ }
91
155
  const requestId =
92
156
  `${run.runId}:${run.currentStepId}:` + dependencies.createRequestId();
93
157
  const workspace = dependencies.createDelegationWorkspace();
94
158
  const outcomes = allowedOutcomes(workflow, run);
95
159
  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
160
  const policyDigest = digest({
107
161
  version: 1,
108
162
  requestId,
@@ -110,10 +164,12 @@ export function createDelegationPlan(
110
164
  runId: run.runId,
111
165
  stepId: run.currentStepId,
112
166
  stepDigest: run.currentStepDigest,
167
+ cwd: delegationCwd,
113
168
  capabilityPath: workspace.capabilityPath,
114
169
  resultPath: workspace.resultPath,
115
- approvedBashCommands,
116
- ...repositoryPolicy,
170
+ permissions: step.permissions,
171
+ outcomes,
172
+ ...(step.workspace ? { workspace: step.workspace } : {}),
117
173
  });
118
174
  const policy: ChildStepPolicy = {
119
175
  version: 1,
@@ -123,13 +179,12 @@ export function createDelegationPlan(
123
179
  runId: run.runId,
124
180
  stepId: run.currentStepId,
125
181
  stepTitle: step.title,
182
+ cwd: delegationCwd,
126
183
  policyDigest,
127
184
  capabilityPath: workspace.capabilityPath,
128
185
  capabilityToken: workspace.capabilityToken,
129
186
  resultPath: workspace.resultPath,
130
187
  permissions: structuredClone(step.permissions),
131
- ...(approvedBashCommands.length > 0 ? { approvedBashCommands } : {}),
132
- ...repositoryPolicy,
133
188
  outcomes,
134
189
  pauseOutcomes: Object.entries(step.transitions)
135
190
  .filter(
@@ -138,9 +193,10 @@ export function createDelegationPlan(
138
193
  .map(([outcome]) => outcome),
139
194
  summaryMaxChars: workflow.definition.summaryMaxChars,
140
195
  ...(step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {}),
196
+ ...(step.workspace ? { workspace: structuredClone(step.workspace) } : {}),
141
197
  };
142
198
  const trustedSessionRoot = deriveSubagentSessionRoot(
143
- latestContext?.sessionManager.getSessionFile(),
199
+ latestContext.sessionManager.getSessionFile(),
144
200
  );
145
201
  const transcriptTask = [
146
202
  buildDelegatedStepTask(workflow, run, ''),
@@ -25,6 +25,7 @@ export function completionMatchesResult(
25
25
  'outcome',
26
26
  'summary',
27
27
  ...(result.artifact === undefined ? [] : ['artifact']),
28
+ ...(result.workspace === undefined ? [] : ['workspace']),
28
29
  ].sort();
29
30
  const actualKeys = Object.keys(value).sort();
30
31
  if (
@@ -45,7 +46,8 @@ export function completionMatchesResult(
45
46
  return (
46
47
  completion.outcome === result.outcome &&
47
48
  completion.summary === result.summary &&
48
- completion.artifact === result.artifact
49
+ completion.artifact === result.artifact &&
50
+ completion.workspace?.cwd === result.workspace?.cwd
49
51
  );
50
52
  } catch {
51
53
  return false;
@@ -4,9 +4,14 @@ import {
4
4
  type SubagentDelegationUpdate,
5
5
  } from '../integrations/subagents/protocol.ts';
6
6
  import { advanceRun } from '../engine/transitions.ts';
7
+ import {
8
+ attachSubagentTranscript,
9
+ recordCurrentStepResult,
10
+ } from '../engine/step-trace.ts';
7
11
  import type { WorkflowStepResult } from '../runtime/step-result.ts';
8
12
  import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
9
13
  import type { ActiveDelegation, DelegationFailureDetails } from './types.ts';
14
+ import { resolveStepEffects } from './step-effects.ts';
10
15
 
11
16
  type HarnessActionContext = Pick<
12
17
  FullHarnessActionContext,
@@ -138,11 +143,51 @@ async function finishDelegation(
138
143
  ) {
139
144
  return;
140
145
  }
146
+ const terminalAt = this.dependencies.now();
147
+ if (
148
+ response.requestId === active.requestId &&
149
+ (response.agent === undefined || response.agent === active.agent) &&
150
+ response.sessionFile &&
151
+ active.trustedSessionRoot &&
152
+ typeof response.runId === 'string' &&
153
+ response.runId &&
154
+ response.childIndex === 0
155
+ ) {
156
+ try {
157
+ this.run = attachSubagentTranscript(
158
+ this.run,
159
+ active.requestId,
160
+ {
161
+ trustedRoot: active.trustedSessionRoot,
162
+ sessionFile: response.sessionFile,
163
+ runId: response.runId,
164
+ childIndex: response.childIndex,
165
+ },
166
+ terminalAt,
167
+ );
168
+ } catch {
169
+ // Status evidence is best-effort and cannot alter terminal handling.
170
+ }
171
+ }
141
172
  const workflow = this.catalog.workflows.get(this.run.workflowId);
142
173
  const step = workflow?.definition.steps[this.run.currentStepId];
143
174
  if (!workflow || !step) {
144
175
  throw new Error('Active workflow configuration is unavailable');
145
176
  }
177
+ if (response.status === 'completed') {
178
+ const transcriptAudit =
179
+ await this.delegationFailures.completedResponseAudit(active, response);
180
+ if (!transcriptAudit.verified) {
181
+ throw new Error(
182
+ `Subagent "${active.agent}" completed without a verifiable terminal transcript: ${transcriptAudit.reason}`,
183
+ );
184
+ }
185
+ if (transcriptAudit.warning) {
186
+ throw new Error(
187
+ `Subagent "${active.agent}" completed with an unresolved post-completion watchdog warning: ${transcriptAudit.warning}`,
188
+ );
189
+ }
190
+ }
146
191
  let recoveredTerminalFailure: DelegationFailureDetails | undefined;
147
192
  if (
148
193
  response.status !== 'completed' ||
@@ -237,6 +282,7 @@ async function finishDelegation(
237
282
  ),
238
283
  );
239
284
  }
285
+ const acceptedAt = terminalAt;
240
286
  if (recoveredTerminalFailure) {
241
287
  const isFalsePositive =
242
288
  recoveredTerminalFailure.diagnostic?.correlation ===
@@ -249,29 +295,53 @@ async function finishDelegation(
249
295
  );
250
296
  }
251
297
  if (step.gate?.submitOutcome === result.outcome) {
298
+ this.run = recordCurrentStepResult(this.run, result, acceptedAt);
252
299
  await this.submitGate(
253
300
  workflow,
254
301
  this.run,
255
302
  result.outcome,
303
+ result.summary,
256
304
  result.artifact ?? '',
257
305
  );
258
306
  return;
259
307
  }
260
308
 
309
+ const effects = resolveStepEffects(
310
+ this.run,
311
+ step,
312
+ result,
313
+ this.dependencies,
314
+ );
315
+ const tracedRun = recordCurrentStepResult(
316
+ this.run,
317
+ result,
318
+ acceptedAt,
319
+ effects.workspaceCwd,
320
+ );
261
321
  this.run = advanceRun(
262
322
  workflow,
263
- this.run,
323
+ tracedRun,
264
324
  result.outcome,
265
325
  result.summary,
266
326
  this.dependencies.now(),
327
+ effects,
267
328
  );
268
- this.settleAfterTransition(workflow);
329
+ this.settleAfterTransition(workflow, {
330
+ stepId: active.stepId,
331
+ outcome: result.outcome,
332
+ summary: result.summary,
333
+ });
269
334
  } catch (error) {
270
335
  const reason = error instanceof Error ? error.message : String(error);
271
336
  cleanupAttempted = true;
272
337
  await this.cleanupDelegation(active);
273
338
  if (!this.retryDelegationAfterFailure(active, terminalFailure, reason)) {
274
- this.pauseForDelegationFailure(reason);
339
+ const failureSummary = terminalFailure?.error
340
+ ? `Subagent ${terminalFailure.status.replaceAll('_', ' ')}: ${terminalFailure.error}`
341
+ : terminalFailure
342
+ ? `Subagent ${terminalFailure.status.replaceAll('_', ' ')}`
343
+ : reason;
344
+ this.pauseForDelegationFailure(reason, failureSummary);
275
345
  }
276
346
  } finally {
277
347
  try {
@@ -32,6 +32,9 @@ export function delegationFailureFingerprint(
32
32
  tool: normalizedFingerprintField(failure.diagnostic.tool),
33
33
  call: normalizedFingerprintField(failure.diagnostic.call),
34
34
  output: normalizedFingerprintField(failure.diagnostic.output),
35
+ postCompletionWarning: normalizedFingerprintField(
36
+ failure.diagnostic.postCompletionWarning,
37
+ ),
35
38
  }
36
39
  : undefined,
37
40
  });
@@ -62,6 +65,7 @@ export function isRetryableTerminalFailure(
62
65
  failure: DelegationFailureDetails,
63
66
  ): boolean {
64
67
  if (failure.recoveryBlocker !== undefined) return false;
68
+ if (failure.diagnostic?.postCompletionWarning !== undefined) return false;
65
69
  if (
66
70
  failure.status === 'timed_out' ||
67
71
  failure.status === 'turn_budget_exhausted' ||
@@ -96,9 +100,7 @@ export function isSafeToRetryDelegation(
96
100
  ): boolean {
97
101
  return (
98
102
  replayAudit?.replaySafe === true &&
99
- (isReplayExplicitlyAuthorized ||
100
- policy.permissions.bash.mode === 'deny' ||
101
- policy.permissions.bash.mode === 'read-only')
103
+ (isReplayExplicitlyAuthorized || policy.permissions.bash.mode === 'deny')
102
104
  );
103
105
  }
104
106
 
@@ -15,6 +15,7 @@ import {
15
15
  type SubagentDelegationClientController,
16
16
  } from '../integrations/subagents/client.ts';
17
17
  import {
18
+ auditCompletedDelegationTranscript,
18
19
  readDelegationReplayAudit,
19
20
  readToolFailureDiagnostic,
20
21
  } from '../integrations/subagents/diagnostics.ts';
@@ -28,6 +29,10 @@ import {
28
29
  } from '../runtime/serial-task-queue.ts';
29
30
  import { showWorkflowStatus } from '../workflow-status.ts';
30
31
  import type { ActiveDelegation } from './types.ts';
32
+ import {
33
+ resolveWorkspaceDirectory,
34
+ type ResolveWorkspaceDirectoryOptions,
35
+ } from './workspace-directory.ts';
31
36
 
32
37
  const MAX_DELEGATED_RESULT_BYTES = 1024 * 1024;
33
38
 
@@ -47,14 +52,17 @@ export type WorkflowHarnessDependencies = {
47
52
  readonly removeDelegationWorkspace: (
48
53
  resultDirectory: string,
49
54
  ) => Promise<void>;
50
- readonly currentWorkingDirectory: () => string;
51
55
  readonly waitForDelay: (delayMs: number) => Promise<void>;
56
+ readonly resolveWorkspaceDirectory: (
57
+ options: ResolveWorkspaceDirectoryOptions,
58
+ ) => string;
52
59
  readonly loadCatalog: typeof loadCatalog;
53
60
  readonly requestPlannotatorReview: typeof requestPlannotatorReview;
54
61
  readonly requestPlannotatorReviewStatus: typeof requestPlannotatorReviewStatus;
55
62
  readonly requestPromptGateReview: typeof requestPromptGateReview;
56
63
  readonly readDelegationReplayAudit: typeof readDelegationReplayAudit;
57
64
  readonly readToolFailureDiagnostic: typeof readToolFailureDiagnostic;
65
+ readonly auditCompletedDelegationTranscript: typeof auditCompletedDelegationTranscript;
58
66
  readonly showWorkflowStatus: typeof showWorkflowStatus;
59
67
  readonly createSubagentClient: (
60
68
  pi: ExtensionAPI,
@@ -132,15 +140,16 @@ const DEFAULT_DEPENDENCIES: WorkflowHarnessDependencies = {
132
140
  readDelegatedResult,
133
141
  removeDelegationWorkspace: async (resultDirectory) =>
134
142
  rm(resultDirectory, { recursive: true, force: true }),
135
- currentWorkingDirectory: () => process.cwd(),
136
143
  waitForDelay: async (delayMs) =>
137
144
  new Promise((resolve) => setTimeout(resolve, delayMs)),
145
+ resolveWorkspaceDirectory,
138
146
  loadCatalog,
139
147
  requestPlannotatorReview,
140
148
  requestPlannotatorReviewStatus,
141
149
  requestPromptGateReview,
142
150
  readDelegationReplayAudit,
143
151
  readToolFailureDiagnostic,
152
+ auditCompletedDelegationTranscript,
144
153
  showWorkflowStatus,
145
154
  createSubagentClient: (pi) => createSubagentDelegationClient(pi.events),
146
155
  createMainStepRuntime: (pi) => createMainStepRuntime({ pi }),