@wichayutdew/pi-workflows 0.3.0 → 1.0.1

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 (144) hide show
  1. package/README.md +55 -39
  2. package/dist/index.js +5771 -4794
  3. package/examples/mr-comments.workflow.yaml +2 -0
  4. package/package.json +1 -1
  5. package/schemas/workflow.schema.json +10 -1
  6. package/src/command-names.ts +7 -1
  7. package/src/commands.ts +126 -52
  8. package/src/config/catalog.ts +162 -0
  9. package/src/config/ceiling.ts +120 -116
  10. package/src/config/command-conflicts.ts +4 -4
  11. package/src/config/diagnostics.ts +28 -0
  12. package/src/config/load-settings.ts +37 -0
  13. package/src/config/load-types.ts +53 -0
  14. package/src/config/load-workflows.ts +135 -0
  15. package/src/config/load.ts +43 -311
  16. package/src/config/types.ts +150 -137
  17. package/src/config/validate.ts +12 -1261
  18. package/src/config/validation/permissions.ts +312 -0
  19. package/src/config/validation/prompt.ts +21 -0
  20. package/src/config/validation/settings.ts +122 -0
  21. package/src/config/validation/shared.ts +129 -0
  22. package/src/config/validation/shortcut.ts +118 -0
  23. package/src/config/validation/step.ts +228 -0
  24. package/src/config/validation/subagent.ts +288 -0
  25. package/src/config/validation/workflow.ts +156 -0
  26. package/src/config/yaml.ts +29 -0
  27. package/src/digest.ts +38 -6
  28. package/src/engine/checkpoint.ts +13 -9
  29. package/src/engine/create-run.ts +43 -0
  30. package/src/engine/gate-transitions.ts +187 -0
  31. package/src/engine/reconciliation-history.ts +88 -0
  32. package/src/engine/resume.ts +19 -7
  33. package/src/engine/run-advance.ts +122 -0
  34. package/src/engine/run-lifecycle.ts +124 -0
  35. package/src/engine/run-reconciliation.ts +116 -0
  36. package/src/engine/run-validation.ts +129 -0
  37. package/src/engine/state-types.ts +66 -0
  38. package/src/engine/state.ts +10 -193
  39. package/src/engine/transition-helpers.ts +28 -0
  40. package/src/engine/transition-types.ts +8 -0
  41. package/src/engine/transitions.ts +17 -471
  42. package/src/harness/action-context.ts +181 -0
  43. package/src/harness/catalog.ts +45 -0
  44. package/src/harness/context-idle.ts +21 -0
  45. package/src/harness/core-actions.ts +299 -0
  46. package/src/harness/delegation-control-actions.ts +250 -0
  47. package/src/harness/delegation-failure.ts +215 -0
  48. package/src/harness/delegation-plan.ts +219 -0
  49. package/src/harness/delegation-recovery-validation.ts +159 -0
  50. package/src/harness/delegation-response-actions.ts +295 -0
  51. package/src/harness/delegation-retry-policy.ts +118 -0
  52. package/src/harness/dependencies.ts +165 -0
  53. package/src/harness/gate-submission-action.ts +154 -0
  54. package/src/harness/lifecycle-actions.ts +154 -0
  55. package/src/harness/pause-actions.ts +120 -0
  56. package/src/harness/plannotator-result-actions.ts +123 -0
  57. package/src/harness/prompt-gate-actions.ts +270 -0
  58. package/src/harness/resume-action.ts +261 -0
  59. package/src/harness/start-actions.ts +183 -0
  60. package/src/harness/status-actions.ts +191 -0
  61. package/src/harness/step-execution-actions.ts +283 -0
  62. package/src/harness/types.ts +76 -0
  63. package/src/harness.ts +236 -2303
  64. package/src/index.ts +73 -11
  65. package/src/integrations/plannotator-requests.ts +127 -0
  66. package/src/integrations/plannotator-responses.ts +141 -0
  67. package/src/integrations/plannotator-types.ts +44 -0
  68. package/src/integrations/plannotator.ts +16 -235
  69. package/src/integrations/prompt-gate.ts +17 -10
  70. package/src/integrations/subagents/child-policy-envelope.ts +119 -0
  71. package/src/integrations/subagents/child-policy-paths.ts +66 -0
  72. package/src/integrations/subagents/child-policy-sections.ts +186 -0
  73. package/src/integrations/subagents/child-policy-types.ts +32 -0
  74. package/src/integrations/subagents/child-policy-validation.ts +163 -0
  75. package/src/integrations/subagents/child-runtime-completion.ts +56 -0
  76. package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
  77. package/src/integrations/subagents/child-runtime-files.ts +156 -0
  78. package/src/integrations/subagents/child-runtime-policy.ts +83 -0
  79. package/src/integrations/subagents/child-runtime-types.ts +30 -0
  80. package/src/integrations/subagents/child-runtime.ts +140 -301
  81. package/src/integrations/subagents/client-delegation.ts +181 -0
  82. package/src/integrations/subagents/client-messages.ts +66 -0
  83. package/src/integrations/subagents/client-types.ts +36 -0
  84. package/src/integrations/subagents/client.ts +127 -219
  85. package/src/integrations/subagents/delegated-result.ts +31 -0
  86. package/src/integrations/subagents/diagnostic-format.ts +40 -0
  87. package/src/integrations/subagents/diagnostic-text.ts +114 -0
  88. package/src/integrations/subagents/diagnostic-types.ts +77 -0
  89. package/src/integrations/subagents/diagnostics.ts +24 -977
  90. package/src/integrations/subagents/failure-correlation.ts +235 -0
  91. package/src/integrations/subagents/failure-transcript.ts +192 -0
  92. package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
  93. package/src/integrations/subagents/protocol-events.ts +27 -0
  94. package/src/integrations/subagents/protocol.ts +30 -375
  95. package/src/integrations/subagents/replay-audit.ts +147 -0
  96. package/src/integrations/subagents/replay-safety.ts +82 -0
  97. package/src/integrations/subagents/session-diagnostics.ts +258 -0
  98. package/src/policy/approved-command-extraction.ts +71 -0
  99. package/src/policy/approved-commands.ts +15 -432
  100. package/src/policy/bash-authorization.ts +86 -0
  101. package/src/policy/bash-read-only.ts +161 -0
  102. package/src/policy/bash-types.ts +20 -0
  103. package/src/policy/bash.ts +4 -346
  104. package/src/policy/completion-batch.ts +21 -16
  105. package/src/policy/immutable-input.ts +5 -2
  106. package/src/policy/mcp-authorization.ts +71 -0
  107. package/src/policy/restricted-command.ts +141 -0
  108. package/src/policy/restricted-git.ts +30 -0
  109. package/src/policy/reviewed-artifact.ts +97 -0
  110. package/src/policy/reviewed-command-safety.ts +150 -0
  111. package/src/policy/reviewed-command-shape.ts +49 -0
  112. package/src/policy/reviewed-repository-contract.ts +138 -0
  113. package/src/policy/reviewed-repository-cwd.ts +102 -0
  114. package/src/policy/tool-call-authorization.ts +58 -0
  115. package/src/policy/tool-selection.ts +72 -0
  116. package/src/policy/tool-types.ts +14 -0
  117. package/src/policy/tools.ts +11 -150
  118. package/src/preflight.ts +96 -61
  119. package/src/prompt/main-workflow-notice.ts +41 -0
  120. package/src/prompt/retry-task.ts +83 -0
  121. package/src/prompt/step-contract.ts +101 -0
  122. package/src/prompt/step-sections.ts +75 -0
  123. package/src/prompt/step-task.ts +156 -0
  124. package/src/prompt/template.ts +83 -0
  125. package/src/prompt.ts +10 -254
  126. package/src/runtime/completion-tool.ts +3 -0
  127. package/src/runtime/main-step-completion.ts +78 -0
  128. package/src/runtime/main-step-lifecycle.ts +37 -0
  129. package/src/runtime/main-step-policy.ts +84 -0
  130. package/src/runtime/main-step-runtime-types.ts +64 -0
  131. package/src/runtime/main-step-runtime.ts +129 -200
  132. package/src/runtime/main-step-state.ts +85 -0
  133. package/src/runtime/serial-task-queue.ts +48 -13
  134. package/src/runtime/step-result.ts +48 -27
  135. package/src/workflow-list.ts +15 -6
  136. package/src/workflow-status/format-status.ts +52 -0
  137. package/src/workflow-status/formatting.ts +178 -0
  138. package/src/workflow-status/layout.ts +109 -0
  139. package/src/workflow-status/render-board.ts +101 -0
  140. package/src/workflow-status/render-path.ts +123 -0
  141. package/src/workflow-status/render-summary.ts +144 -0
  142. package/src/workflow-status/types.ts +40 -0
  143. package/src/workflow-status/view.ts +273 -0
  144. package/src/workflow-status.ts +20 -820
@@ -0,0 +1,270 @@
1
+ import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
2
+ import type { LoadedWorkflow } from '../config/types.ts';
3
+ import type { GateResolution, WorkflowRun } from '../engine/state.ts';
4
+ import {
5
+ failRun,
6
+ pauseRun,
7
+ resolveGate,
8
+ storeGateResolution,
9
+ } from '../engine/transitions.ts';
10
+ import type { PromptGateReviewResult } from '../integrations/prompt-gate.ts';
11
+ import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
12
+ import type { ActivePromptReview } from './types.ts';
13
+
14
+ type HarnessActionContext = Pick<
15
+ FullHarnessActionContext,
16
+ | 'activePromptReview'
17
+ | 'cancelPromptReview'
18
+ | 'catalog'
19
+ | 'dependencies'
20
+ | 'finishPromptReview'
21
+ | 'isSessionActive'
22
+ | 'latestContext'
23
+ | 'mutationQueue'
24
+ | 'pausePromptGate'
25
+ | 'persist'
26
+ | 'queuePromptReviewFailure'
27
+ | 'queuePromptReviewResult'
28
+ | 'restoreBaselineTools'
29
+ | 'run'
30
+ | 'sessionEpoch'
31
+ | 'settleAfterTransition'
32
+ | 'updateStatus'
33
+ >;
34
+
35
+ export type PromptGateActions = {
36
+ launchPromptReview: (
37
+ this: HarnessActionContext,
38
+ workflow: LoadedWorkflow,
39
+ run: WorkflowRun,
40
+ context: ExtensionContext | undefined,
41
+ ) => void;
42
+ queuePromptReviewResult: (
43
+ this: HarnessActionContext,
44
+ active: ActivePromptReview,
45
+ result: PromptGateReviewResult,
46
+ ) => void;
47
+ queuePromptReviewFailure: (
48
+ this: HarnessActionContext,
49
+ active: ActivePromptReview,
50
+ reason: string,
51
+ ) => void;
52
+ finishPromptReview: (
53
+ this: HarnessActionContext,
54
+ active: ActivePromptReview,
55
+ result: PromptGateReviewResult,
56
+ ) => Promise<void>;
57
+ pausePromptGate: (
58
+ this: HarnessActionContext,
59
+ requestId: string,
60
+ reason: string,
61
+ isFailed: boolean,
62
+ ) => void;
63
+ cancelPromptReview: (this: HarnessActionContext) => void;
64
+ };
65
+
66
+ function launchPromptReview(
67
+ this: HarnessActionContext,
68
+ workflow: LoadedWorkflow,
69
+ run: WorkflowRun,
70
+ context: ExtensionContext | undefined,
71
+ ): void {
72
+ const pendingGate = run.pendingGate;
73
+ if (!pendingGate || pendingGate.provider !== 'prompt') return;
74
+ if (!context?.hasUI) {
75
+ this.pausePromptGate(
76
+ pendingGate.requestId,
77
+ 'Built-in review requires Pi TUI or RPC mode; resume there to continue',
78
+ false,
79
+ );
80
+ return;
81
+ }
82
+ if (this.activePromptReview?.requestId === pendingGate.requestId) return;
83
+ this.cancelPromptReview();
84
+
85
+ const active: ActivePromptReview = {
86
+ requestId: pendingGate.requestId,
87
+ runId: run.runId,
88
+ stepId: pendingGate.stepId,
89
+ sessionEpoch: this.sessionEpoch,
90
+ abortController: this.dependencies.createAbortController(),
91
+ };
92
+ this.activePromptReview = active;
93
+ void this.dependencies
94
+ .requestPromptGateReview(
95
+ context.ui,
96
+ `Review ${workflow.definition.id}:${pendingGate.stepId}`,
97
+ pendingGate.artifact,
98
+ active.abortController.signal,
99
+ )
100
+ .then(
101
+ (result) => {
102
+ this.queuePromptReviewResult(active, result);
103
+ },
104
+ (error: unknown) => {
105
+ this.queuePromptReviewFailure(
106
+ active,
107
+ error instanceof Error ? error.message : String(error),
108
+ );
109
+ },
110
+ );
111
+ }
112
+
113
+ function queuePromptReviewResult(
114
+ this: HarnessActionContext,
115
+ active: ActivePromptReview,
116
+ result: PromptGateReviewResult,
117
+ ): void {
118
+ void this.mutationQueue
119
+ .run(() => this.finishPromptReview(active, result))
120
+ .catch((error: unknown) => {
121
+ this.latestContext?.ui.notify(
122
+ `Cannot apply built-in review: ${error instanceof Error ? error.message : String(error)}`,
123
+ 'error',
124
+ );
125
+ });
126
+ }
127
+
128
+ function queuePromptReviewFailure(
129
+ this: HarnessActionContext,
130
+ active: ActivePromptReview,
131
+ reason: string,
132
+ ): void {
133
+ void this.mutationQueue
134
+ .run(async () => {
135
+ if (this.activePromptReview !== active) return;
136
+ this.activePromptReview = undefined;
137
+ this.pausePromptGate(
138
+ active.requestId,
139
+ `Built-in review failed: ${reason}`,
140
+ true,
141
+ );
142
+ })
143
+ .catch((error: unknown) => {
144
+ this.latestContext?.ui.notify(
145
+ `Cannot pause failed built-in review: ${error instanceof Error ? error.message : String(error)}`,
146
+ 'error',
147
+ );
148
+ });
149
+ }
150
+
151
+ async function finishPromptReview(
152
+ this: HarnessActionContext,
153
+ active: ActivePromptReview,
154
+ result: PromptGateReviewResult,
155
+ ): Promise<void> {
156
+ if (this.activePromptReview !== active) return;
157
+ this.activePromptReview = undefined;
158
+ if (
159
+ !this.isSessionActive ||
160
+ this.sessionEpoch !== active.sessionEpoch ||
161
+ !this.run ||
162
+ this.run.runId !== active.runId ||
163
+ this.run.currentStepId !== active.stepId ||
164
+ this.run.pendingGate?.provider !== 'prompt' ||
165
+ this.run.pendingGate.requestId !== active.requestId
166
+ ) {
167
+ return;
168
+ }
169
+ if (result.status === 'dismissed') {
170
+ this.pausePromptGate(
171
+ active.requestId,
172
+ 'Built-in review was dismissed; resume to reopen it',
173
+ false,
174
+ );
175
+ return;
176
+ }
177
+
178
+ const resolution: GateResolution = {
179
+ approved: result.approved,
180
+ feedback: result.feedback,
181
+ resolvedAt: this.dependencies.now(),
182
+ };
183
+ if (this.run.status === 'paused') {
184
+ this.run = storeGateResolution(
185
+ this.run,
186
+ resolution,
187
+ this.dependencies.now(),
188
+ );
189
+ this.persist();
190
+ this.latestContext?.ui.notify(
191
+ 'Built-in review finished while paused. Run /workflow-resume to apply it.',
192
+ 'info',
193
+ );
194
+ return;
195
+ }
196
+ if (this.run.status !== 'awaiting-gate') return;
197
+
198
+ const workflow = this.catalog.workflows.get(this.run.workflowId);
199
+ if (!workflow) {
200
+ this.pausePromptGate(
201
+ active.requestId,
202
+ 'Built-in review finished, but workflow configuration is unavailable',
203
+ true,
204
+ );
205
+ return;
206
+ }
207
+ try {
208
+ this.run = resolveGate(
209
+ workflow,
210
+ this.run,
211
+ resolution,
212
+ this.dependencies.now(),
213
+ );
214
+ this.settleAfterTransition(workflow);
215
+ } catch (error) {
216
+ this.pausePromptGate(
217
+ active.requestId,
218
+ `Cannot apply built-in review: ${error instanceof Error ? error.message : String(error)}`,
219
+ true,
220
+ );
221
+ }
222
+ }
223
+
224
+ function pausePromptGate(
225
+ this: HarnessActionContext,
226
+ requestId: string,
227
+ reason: string,
228
+ isFailed: boolean,
229
+ ): void {
230
+ if (
231
+ !this.run ||
232
+ this.run.pendingGate?.provider !== 'prompt' ||
233
+ this.run.pendingGate.requestId !== requestId
234
+ ) {
235
+ return;
236
+ }
237
+ if (this.run.status === 'awaiting-gate') {
238
+ this.run = isFailed
239
+ ? failRun(this.run, reason, this.dependencies.now())
240
+ : pauseRun(this.run, reason, this.dependencies.now());
241
+ }
242
+ this.persist();
243
+ this.restoreBaselineTools();
244
+ this.updateStatus();
245
+ this.latestContext?.ui.notify(
246
+ `Workflow paused at "${this.run.currentStepId}": ${reason}`,
247
+ 'warning',
248
+ );
249
+ }
250
+
251
+ function cancelPromptReview(this: HarnessActionContext): void {
252
+ const active = this.activePromptReview;
253
+ if (!active) return;
254
+ this.activePromptReview = undefined;
255
+ active.abortController.abort();
256
+ }
257
+
258
+ /**
259
+ * Returns built-in prompt-gate actions for harness composition.
260
+ */
261
+ export function createPromptGateActions(): PromptGateActions {
262
+ return {
263
+ launchPromptReview,
264
+ queuePromptReviewResult,
265
+ queuePromptReviewFailure,
266
+ finishPromptReview,
267
+ pausePromptGate,
268
+ cancelPromptReview,
269
+ };
270
+ }
@@ -0,0 +1,261 @@
1
+ import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
2
+ import {
3
+ failGate,
4
+ failRun,
5
+ reconcileRun,
6
+ resolveGate,
7
+ resumeRun,
8
+ storeGateResolution,
9
+ } from '../engine/transitions.ts';
10
+ import {
11
+ captureResumeCheckpoint,
12
+ matchesResumeCheckpoint,
13
+ } from '../engine/resume.ts';
14
+ import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
15
+
16
+ type HarnessActionContext = Pick<
17
+ FullHarnessActionContext,
18
+ | 'activeDelegation'
19
+ | 'captureSkills'
20
+ | 'catalog'
21
+ | 'dependencies'
22
+ | 'isolateMainSessionTools'
23
+ | 'launchCurrentStep'
24
+ | 'launchPromptReview'
25
+ | 'persist'
26
+ | 'pi'
27
+ | 'preflight'
28
+ | 'reloadCatalog'
29
+ | 'restoreBaselineTools'
30
+ | 'run'
31
+ | 'sessionEpoch'
32
+ | 'updateStatus'
33
+ >;
34
+
35
+ export type ResumeAction = {
36
+ resumeNow: (
37
+ this: HarnessActionContext,
38
+ context: ExtensionCommandContext,
39
+ ) => Promise<void>;
40
+ };
41
+
42
+ async function resumeNow(
43
+ this: HarnessActionContext,
44
+ context: ExtensionCommandContext,
45
+ ): Promise<void> {
46
+ if (!this.run || this.run.status !== 'paused') {
47
+ context.ui.notify('No paused workflow to resume', 'warning');
48
+ return;
49
+ }
50
+ if (this.activeDelegation) {
51
+ context.ui.notify(
52
+ `Cannot resume while subagent "${this.activeDelegation.agent}" is still cancelling`,
53
+ 'warning',
54
+ );
55
+ return;
56
+ }
57
+ const checkpoint = captureResumeCheckpoint(this.run, this.sessionEpoch);
58
+ if (!context.isIdle()) {
59
+ context.abort();
60
+ await context.waitForIdle();
61
+ }
62
+ if (!matchesResumeCheckpoint(this.run, this.sessionEpoch, checkpoint)) {
63
+ context.ui.notify(
64
+ 'Resume was superseded by another workflow or session change',
65
+ 'warning',
66
+ );
67
+ return;
68
+ }
69
+ this.captureSkills(context.getSystemPromptOptions().skills);
70
+ await this.reloadCatalog(context, false);
71
+ if (!matchesResumeCheckpoint(this.run, this.sessionEpoch, checkpoint)) {
72
+ context.ui.notify(
73
+ 'Resume was superseded by another workflow or session change',
74
+ 'warning',
75
+ );
76
+ return;
77
+ }
78
+
79
+ let workflow = this.catalog.workflows.get(this.run.workflowId);
80
+ if (!workflow) {
81
+ context.ui.notify(
82
+ `Workflow "${this.run.workflowId}" is no longer loaded; restore it or abort`,
83
+ 'error',
84
+ );
85
+ return;
86
+ }
87
+ const reconciled = reconcileRun(this.run, workflow, this.dependencies.now());
88
+ if (!reconciled.run) {
89
+ context.ui.notify(
90
+ reconciled.error ?? 'Cannot reconcile workflow configuration',
91
+ 'error',
92
+ );
93
+ return;
94
+ }
95
+
96
+ let resumed = reconciled.run;
97
+ if (
98
+ resumed.pendingGate?.provider === 'plannotator' &&
99
+ !resumed.pendingGate.reviewId
100
+ ) {
101
+ resumed = failGate(
102
+ resumed,
103
+ 'Gate submission was interrupted before a review id was recorded; submit it again',
104
+ this.dependencies.now(),
105
+ );
106
+ }
107
+ if (
108
+ resumed.pendingGate?.provider === 'plannotator' &&
109
+ resumed.pendingGate.reviewId &&
110
+ !resumed.pendingGate.resolution
111
+ ) {
112
+ const requestedReviewId = resumed.pendingGate.reviewId;
113
+ const gateStep = workflow.definition.steps[resumed.pendingGate.stepId];
114
+ const statusResponse =
115
+ await this.dependencies.requestPlannotatorReviewStatus(
116
+ this.pi.events,
117
+ `${resumed.runId}:review-status:${this.dependencies.createRequestId()}`,
118
+ requestedReviewId,
119
+ gateStep?.gate?.provider === 'plannotator'
120
+ ? gateStep.gate.timeoutMs
121
+ : 5_000,
122
+ );
123
+ if (!matchesResumeCheckpoint(this.run, this.sessionEpoch, checkpoint)) {
124
+ context.ui.notify(
125
+ 'Resume was superseded by another workflow or session change',
126
+ 'warning',
127
+ );
128
+ return;
129
+ }
130
+
131
+ workflow = this.catalog.workflows.get(this.run.workflowId);
132
+ if (!workflow) {
133
+ context.ui.notify(
134
+ `Workflow "${this.run.workflowId}" is no longer loaded; restore it or abort`,
135
+ 'error',
136
+ );
137
+ return;
138
+ }
139
+ const latest = reconcileRun(this.run, workflow, this.dependencies.now());
140
+ if (!latest.run) {
141
+ context.ui.notify(
142
+ latest.error ?? 'Cannot reconcile workflow configuration',
143
+ 'error',
144
+ );
145
+ return;
146
+ }
147
+ resumed = latest.run;
148
+
149
+ if (
150
+ !resumed.pendingGate?.resolution &&
151
+ resumed.pendingGate?.reviewId === requestedReviewId &&
152
+ statusResponse.status !== 'handled'
153
+ ) {
154
+ context.ui.notify(
155
+ statusResponse.error ?? 'Cannot query the pending Plannotator review',
156
+ 'error',
157
+ );
158
+ return;
159
+ }
160
+ if (
161
+ !resumed.pendingGate?.resolution &&
162
+ resumed.pendingGate?.reviewId === requestedReviewId &&
163
+ statusResponse.status === 'handled' &&
164
+ statusResponse.result.status === 'completed'
165
+ ) {
166
+ resumed = storeGateResolution(
167
+ resumed,
168
+ {
169
+ approved: statusResponse.result.approved,
170
+ feedback: statusResponse.result.feedback,
171
+ resolvedAt: this.dependencies.now(),
172
+ },
173
+ this.dependencies.now(),
174
+ );
175
+ } else if (
176
+ !resumed.pendingGate?.resolution &&
177
+ resumed.pendingGate?.reviewId === requestedReviewId &&
178
+ statusResponse.status === 'handled' &&
179
+ statusResponse.result.status === 'missing'
180
+ ) {
181
+ resumed = failGate(
182
+ resumed,
183
+ 'Plannotator no longer has the pending review; submit it again',
184
+ this.dependencies.now(),
185
+ );
186
+ }
187
+ }
188
+
189
+ const storedResolution = resumed.pendingGate?.resolution;
190
+ if (storedResolution) {
191
+ try {
192
+ resumed = resolveGate(
193
+ workflow,
194
+ resumed,
195
+ storedResolution,
196
+ this.dependencies.now(),
197
+ );
198
+ } catch (error) {
199
+ context.ui.notify(
200
+ `Cannot apply stored gate result: ${error instanceof Error ? error.message : String(error)}`,
201
+ 'error',
202
+ );
203
+ return;
204
+ }
205
+ } else {
206
+ resumed = resumeRun(resumed, this.dependencies.now());
207
+ }
208
+ this.run = resumed;
209
+
210
+ if (this.run.status === 'awaiting-gate') {
211
+ this.persist();
212
+ this.restoreBaselineTools();
213
+ this.updateStatus();
214
+ if (this.run.pendingGate?.provider === 'prompt') {
215
+ this.launchPromptReview(workflow, this.run, context);
216
+ context.ui.notify('Workflow resumed with built-in review open', 'info');
217
+ return;
218
+ }
219
+ context.ui.notify(
220
+ `Workflow resumed and is waiting for review ${this.run.pendingGate?.reviewId ?? ''}`.trim(),
221
+ 'info',
222
+ );
223
+ return;
224
+ }
225
+ if (this.run.status !== 'running') {
226
+ this.persist();
227
+ this.restoreBaselineTools();
228
+ this.updateStatus();
229
+ context.ui.notify(`Workflow is now ${this.run.status}`, 'info');
230
+ return;
231
+ }
232
+
233
+ const preflightErrors = this.preflight(workflow, this.run.currentStepId);
234
+ if (preflightErrors.length > 0) {
235
+ this.run = failRun(
236
+ this.run,
237
+ `Step preflight failed: ${preflightErrors.join('; ')}`,
238
+ this.dependencies.now(),
239
+ );
240
+ this.persist();
241
+ this.restoreBaselineTools();
242
+ this.updateStatus();
243
+ context.ui.notify(
244
+ `Cannot resume workflow:\n${preflightErrors.join('\n')}`,
245
+ 'error',
246
+ );
247
+ return;
248
+ }
249
+
250
+ this.persist();
251
+ this.isolateMainSessionTools();
252
+ this.updateStatus();
253
+ this.launchCurrentStep(workflow);
254
+ }
255
+
256
+ /**
257
+ * Returns the resume action for harness composition.
258
+ */
259
+ export function createResumeAction(): ResumeAction {
260
+ return { resumeNow };
261
+ }
@@ -0,0 +1,183 @@
1
+ import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
2
+ import { createRun } from '../engine/state.ts';
3
+ import { formatWorkflowList } from '../workflow-list.ts';
4
+ import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
5
+ import type { WorkflowStartContext } from './types.ts';
6
+
7
+ type HarnessActionContext = Pick<
8
+ FullHarnessActionContext,
9
+ | 'activeDelegation'
10
+ | 'captureSkills'
11
+ | 'catalog'
12
+ | 'dependencies'
13
+ | 'isSessionActive'
14
+ | 'isolateMainSessionTools'
15
+ | 'launchCurrentStep'
16
+ | 'openWorkflowStatus'
17
+ | 'persist'
18
+ | 'pi'
19
+ | 'preflight'
20
+ | 'reloadCatalog'
21
+ | 'run'
22
+ | 'sessionEpoch'
23
+ | 'updateStatus'
24
+ >;
25
+
26
+ type SessionIdentity = Pick<
27
+ FullHarnessActionContext,
28
+ 'isSessionActive' | 'sessionEpoch'
29
+ >;
30
+
31
+ function isCurrentSession(
32
+ session: SessionIdentity,
33
+ sessionEpoch: number,
34
+ ): boolean {
35
+ return session.isSessionActive && session.sessionEpoch === sessionEpoch;
36
+ }
37
+
38
+ export type StartActions = {
39
+ listWorkflows: (
40
+ this: HarnessActionContext,
41
+ context: ExtensionCommandContext,
42
+ ) => Promise<void>;
43
+ startNow: (
44
+ this: HarnessActionContext,
45
+ workflowId: string,
46
+ input: string,
47
+ startContext: WorkflowStartContext,
48
+ sessionEpoch: number,
49
+ ) => Promise<void>;
50
+ reloadNow: (
51
+ this: HarnessActionContext,
52
+ context: ExtensionCommandContext,
53
+ ) => Promise<void>;
54
+ };
55
+
56
+ async function listWorkflows(
57
+ this: HarnessActionContext,
58
+ context: ExtensionCommandContext,
59
+ ): Promise<void> {
60
+ const workflows = [...this.catalog.workflows.values()].sort((left, right) =>
61
+ left.definition.id.localeCompare(right.definition.id),
62
+ );
63
+ if (workflows.length === 0) {
64
+ context.ui.notify(
65
+ `No workflows loaded from ${this.catalog.userDirectory}`,
66
+ this.catalog.diagnostics.length > 0 ? 'warning' : 'info',
67
+ );
68
+ return;
69
+ }
70
+ this.pi.sendMessage({
71
+ customType: 'workflow-list',
72
+ content: formatWorkflowList(
73
+ workflows.map((workflow) => workflow.definition),
74
+ ),
75
+ display: true,
76
+ });
77
+ }
78
+
79
+ async function startNow(
80
+ this: HarnessActionContext,
81
+ workflowId: string,
82
+ input: string,
83
+ startContext: WorkflowStartContext,
84
+ sessionEpoch: number,
85
+ ): Promise<void> {
86
+ const { context } = startContext;
87
+ if (this.activeDelegation) {
88
+ context.ui.notify(
89
+ `Cannot start a workflow while subagent "${this.activeDelegation.agent}" is still cancelling`,
90
+ 'warning',
91
+ );
92
+ return;
93
+ }
94
+ if (
95
+ this.run &&
96
+ this.run.status !== 'completed' &&
97
+ this.run.status !== 'aborted'
98
+ ) {
99
+ context.ui.notify(
100
+ `Workflow "${this.run.workflowId}" is ${this.run.status}; resume or abort it first`,
101
+ 'warning',
102
+ );
103
+ return;
104
+ }
105
+ if (!context.isIdle()) {
106
+ context.abort();
107
+ await startContext.waitForIdle();
108
+ }
109
+ if (!isCurrentSession(this, sessionEpoch)) {
110
+ context.ui.notify(
111
+ 'Workflow start was superseded by a session change',
112
+ 'warning',
113
+ );
114
+ return;
115
+ }
116
+
117
+ this.captureSkills(startContext.skills());
118
+ if (!(await this.reloadCatalog(context, false))) {
119
+ context.ui.notify(
120
+ 'Workflow start was superseded by a newer configuration load',
121
+ 'warning',
122
+ );
123
+ return;
124
+ }
125
+ if (!isCurrentSession(this, sessionEpoch)) {
126
+ context.ui.notify(
127
+ 'Workflow start was superseded by a session change',
128
+ 'warning',
129
+ );
130
+ return;
131
+ }
132
+ const workflow = this.catalog.workflows.get(workflowId);
133
+ if (!workflow) {
134
+ context.ui.notify(`Workflow "${workflowId}" is not loaded`, 'error');
135
+ return;
136
+ }
137
+ const preflightErrors = this.preflight(workflow, workflow.definition.start);
138
+ if (preflightErrors.length > 0) {
139
+ context.ui.notify(
140
+ `Cannot start workflow:\n${preflightErrors.join('\n')}`,
141
+ 'error',
142
+ );
143
+ return;
144
+ }
145
+
146
+ this.run = createRun(
147
+ workflow,
148
+ input.trim(),
149
+ this.pi.getActiveTools(),
150
+ this.dependencies.createRequestId(),
151
+ this.dependencies.now(),
152
+ );
153
+ this.persist();
154
+ this.isolateMainSessionTools();
155
+ this.updateStatus();
156
+ this.openWorkflowStatus(context);
157
+ this.launchCurrentStep(workflow);
158
+ }
159
+
160
+ async function reloadNow(
161
+ this: HarnessActionContext,
162
+ context: ExtensionCommandContext,
163
+ ): Promise<void> {
164
+ if (
165
+ this.run &&
166
+ (this.run.status === 'running' || this.run.status === 'awaiting-gate')
167
+ ) {
168
+ context.ui.notify(
169
+ 'Pause the workflow before reloading its configuration',
170
+ 'warning',
171
+ );
172
+ return;
173
+ }
174
+ this.captureSkills(context.getSystemPromptOptions().skills);
175
+ await this.reloadCatalog(context, true);
176
+ }
177
+
178
+ /**
179
+ * Returns workflow listing, start, and reload actions for harness composition.
180
+ */
181
+ export function createStartActions(): StartActions {
182
+ return { listWorkflows, startNow, reloadNow };
183
+ }