@wichayutdew/pi-workflows 1.0.1 → 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 -223
  2. package/agents/step.md +4 -11
  3. package/dist/index.js +2554 -976
  4. package/examples/mr-comments.workflow.yaml +16 -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 -44
  31. package/src/command-names.ts +1 -0
  32. package/src/commands.ts +33 -4
  33. package/src/config/ceiling.ts +6 -12
  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 -22
  37. package/src/config/validation/permissions.ts +5 -66
  38. package/src/config/validation/prompt.ts +1 -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 +3 -0
  43. package/src/engine/gate-transitions.ts +58 -9
  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 +235 -0
  49. package/src/engine/run-workflow-validation.ts +191 -0
  50. package/src/engine/state-types.ts +108 -5
  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 -84
  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 +3 -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
@@ -22,6 +22,7 @@ import type { WorkflowStepResult } from '../runtime/step-result.ts';
22
22
  import type { WorkflowStatusSnapshot } from '../workflow-status.ts';
23
23
  import type { DelegationFailureActions } from './delegation-failure.ts';
24
24
  import type { WorkflowHarnessDependencies } from './dependencies.ts';
25
+ import type { SettledStepReport } from './step-reporting.ts';
25
26
  import type {
26
27
  ActiveDelegation,
27
28
  ActivePromptReview,
@@ -61,6 +62,10 @@ export type HarnessActionContext = {
61
62
  legacyProgressWidgetContext: ExtensionContext | undefined;
62
63
  workflowIds: () => Array<string>;
63
64
  list: (context: ExtensionCommandContext) => Promise<void>;
65
+ doctor: (
66
+ workflowId: string,
67
+ context: ExtensionCommandContext,
68
+ ) => Promise<void>;
64
69
  start: (
65
70
  workflowId: string,
66
71
  input: string,
@@ -74,8 +79,11 @@ export type HarnessActionContext = {
74
79
  ) => Promise<void>;
75
80
  pause: (reason: string, context: ExtensionCommandContext) => Promise<void>;
76
81
  pauseNow: (reason: string, context: ExtensionCommandContext) => Promise<void>;
77
- resume: (context: ExtensionCommandContext) => Promise<void>;
78
- resumeNow: (context: ExtensionCommandContext) => Promise<void>;
82
+ resume: (input: string, context: ExtensionCommandContext) => Promise<void>;
83
+ resumeNow: (
84
+ context: ExtensionCommandContext,
85
+ input?: string,
86
+ ) => Promise<void>;
79
87
  abort: (reason: string, context: ExtensionCommandContext) => Promise<void>;
80
88
  abortNow: (reason: string, context: ExtensionCommandContext) => Promise<void>;
81
89
  reload: (context: ExtensionCommandContext) => Promise<void>;
@@ -93,6 +101,16 @@ export type HarnessActionContext = {
93
101
  run: WorkflowRun,
94
102
  step: WorkflowStep,
95
103
  ) => void;
104
+ queueMainStepLog: (
105
+ identity: MainStepIdentity,
106
+ lines: ReadonlyArray<string>,
107
+ context: ExtensionContext,
108
+ ) => Promise<void>;
109
+ recordMainStepLog: (
110
+ identity: MainStepIdentity,
111
+ lines: ReadonlyArray<string>,
112
+ context: ExtensionContext,
113
+ ) => Promise<void>;
96
114
  queueMainStepResult: (
97
115
  identity: MainStepIdentity,
98
116
  result: WorkflowStepResult | undefined,
@@ -123,8 +141,12 @@ export type HarnessActionContext = {
123
141
  failure: DelegationFailureDetails | undefined,
124
142
  reason: string,
125
143
  ) => boolean;
126
- pauseForDelegationFailure: (reason: string) => void;
127
- pauseForExecutionFailure: (label: string, reason: string) => void;
144
+ pauseForDelegationFailure: (reason: string, failureSummary?: string) => void;
145
+ pauseForExecutionFailure: (
146
+ label: string,
147
+ reason: string,
148
+ failureSummary?: string,
149
+ ) => void;
128
150
  retainUnconfirmedDelegation: (
129
151
  active: ActiveDelegation,
130
152
  reason: string,
@@ -134,6 +156,7 @@ export type HarnessActionContext = {
134
156
  workflow: LoadedWorkflow,
135
157
  originalRun: WorkflowRun,
136
158
  outcome: string,
159
+ summary: string,
137
160
  artifact: string,
138
161
  ) => Promise<void>;
139
162
  launchPromptReview: (
@@ -157,7 +180,10 @@ export type HarnessActionContext = {
157
180
  cancelPromptReview: () => void;
158
181
  registerPlannotatorResults: () => void;
159
182
  handlePlannotatorResult: (data: unknown) => Promise<void>;
160
- settleAfterTransition: (workflow: LoadedWorkflow) => void;
183
+ settleAfterTransition: (
184
+ workflow: LoadedWorkflow,
185
+ report: SettledStepReport,
186
+ ) => void;
161
187
  preflight: (workflow: LoadedWorkflow, stepId: string) => Array<string>;
162
188
  isolateMainSessionTools: () => void;
163
189
  restoreBaselineTools: () => void;
@@ -176,6 +202,5 @@ export type HarnessActionContext = {
176
202
  refreshStatusWhileRunning: () => void;
177
203
  stopStatusRefresh: () => void;
178
204
  registerWorkflowStatusShortcut: () => void;
179
- openWorkflowStatus: (context: ExtensionContext) => void;
180
205
  showWorkflowStatus: (context: ExtensionContext) => Promise<void>;
181
206
  };
@@ -7,6 +7,14 @@ import { failRun, pauseRun } from '../engine/transitions.ts';
7
7
  import { preflightStep } from '../preflight.ts';
8
8
  import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
9
9
  import { formatCatalogDiagnostics } from './catalog.ts';
10
+ import {
11
+ conciseStepFailureSummary,
12
+ conciseStepPauseSummary,
13
+ reportFailedStep,
14
+ reportPausedStep,
15
+ reportSettledStep,
16
+ type SettledStepReport,
17
+ } from './step-reporting.ts';
10
18
 
11
19
  const STATE_ENTRY_TYPE = 'pi-workflows-state-v1';
12
20
 
@@ -39,6 +47,7 @@ export type CoreActions = {
39
47
  settleAfterTransition: (
40
48
  this: HarnessActionContext,
41
49
  workflow: LoadedWorkflow,
50
+ report: SettledStepReport,
42
51
  ) => void;
43
52
  preflight: (
44
53
  this: HarnessActionContext,
@@ -71,6 +80,7 @@ export type CoreActions = {
71
80
  function settleAfterTransition(
72
81
  this: HarnessActionContext,
73
82
  workflow: LoadedWorkflow,
83
+ report: SettledStepReport,
74
84
  ): void {
75
85
  if (!this.run) return;
76
86
  if (this.run.status === 'running') {
@@ -85,6 +95,14 @@ function settleAfterTransition(
85
95
  }
86
96
 
87
97
  this.persist();
98
+ reportSettledStep(this.pi, workflow, this.run, report);
99
+ if (
100
+ this.run.status === 'paused' &&
101
+ this.run.failedStepId === this.run.currentStepId &&
102
+ this.run.pauseReason
103
+ ) {
104
+ reportFailedStep(this.pi, workflow, this.run, this.run.pauseReason);
105
+ }
88
106
  if (this.run.status !== 'running') {
89
107
  this.restoreBaselineTools();
90
108
  this.updateStatus();
@@ -94,8 +112,13 @@ function settleAfterTransition(
94
112
  'info',
95
113
  );
96
114
  } else if (this.run.status === 'paused') {
115
+ const reason = this.run.pauseReason ?? 'manual action required';
97
116
  this.latestContext?.ui.notify(
98
- `Workflow paused: ${this.run.pauseReason ?? 'manual action required'}`,
117
+ `Workflow paused: ${
118
+ this.run.failedStepId
119
+ ? conciseStepFailureSummary(reason)
120
+ : conciseStepPauseSummary(reason)
121
+ }`,
99
122
  'warning',
100
123
  );
101
124
  }
@@ -195,6 +218,12 @@ function restoreFromSession(
195
218
  this.dependencies.now(),
196
219
  );
197
220
  this.persist();
221
+ reportPausedStep(
222
+ this.pi,
223
+ this.catalog.workflows.get(this.run.workflowId),
224
+ this.run,
225
+ this.run.pauseReason ?? 'Session was restored',
226
+ );
198
227
  }
199
228
  if (!this.run && previousBaseline) {
200
229
  this.pi.setActiveTools([...previousBaseline]);
@@ -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 {