@wichayutdew/pi-workflows 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +53 -39
  2. package/dist/index.js +5754 -4794
  3. package/package.json +1 -1
  4. package/src/command-names.ts +7 -1
  5. package/src/commands.ts +126 -52
  6. package/src/config/catalog.ts +162 -0
  7. package/src/config/ceiling.ts +116 -116
  8. package/src/config/command-conflicts.ts +4 -4
  9. package/src/config/diagnostics.ts +28 -0
  10. package/src/config/load-settings.ts +37 -0
  11. package/src/config/load-types.ts +53 -0
  12. package/src/config/load-workflows.ts +135 -0
  13. package/src/config/load.ts +43 -311
  14. package/src/config/types.ts +137 -135
  15. package/src/config/validate.ts +12 -1261
  16. package/src/config/validation/permissions.ts +291 -0
  17. package/src/config/validation/prompt.ts +19 -0
  18. package/src/config/validation/settings.ts +122 -0
  19. package/src/config/validation/shared.ts +129 -0
  20. package/src/config/validation/shortcut.ts +118 -0
  21. package/src/config/validation/step.ts +228 -0
  22. package/src/config/validation/subagent.ts +288 -0
  23. package/src/config/validation/workflow.ts +156 -0
  24. package/src/config/yaml.ts +29 -0
  25. package/src/digest.ts +38 -6
  26. package/src/engine/checkpoint.ts +13 -9
  27. package/src/engine/create-run.ts +42 -0
  28. package/src/engine/gate-transitions.ts +184 -0
  29. package/src/engine/reconciliation-history.ts +88 -0
  30. package/src/engine/resume.ts +19 -7
  31. package/src/engine/run-advance.ts +122 -0
  32. package/src/engine/run-lifecycle.ts +124 -0
  33. package/src/engine/run-reconciliation.ts +116 -0
  34. package/src/engine/run-validation.ts +128 -0
  35. package/src/engine/state-types.ts +64 -0
  36. package/src/engine/state.ts +10 -193
  37. package/src/engine/transition-helpers.ts +28 -0
  38. package/src/engine/transition-types.ts +8 -0
  39. package/src/engine/transitions.ts +17 -471
  40. package/src/harness/action-context.ts +181 -0
  41. package/src/harness/catalog.ts +45 -0
  42. package/src/harness/context-idle.ts +21 -0
  43. package/src/harness/core-actions.ts +299 -0
  44. package/src/harness/delegation-control-actions.ts +250 -0
  45. package/src/harness/delegation-failure.ts +215 -0
  46. package/src/harness/delegation-plan.ts +219 -0
  47. package/src/harness/delegation-recovery-validation.ts +159 -0
  48. package/src/harness/delegation-response-actions.ts +295 -0
  49. package/src/harness/delegation-retry-policy.ts +118 -0
  50. package/src/harness/dependencies.ts +165 -0
  51. package/src/harness/gate-submission-action.ts +154 -0
  52. package/src/harness/lifecycle-actions.ts +154 -0
  53. package/src/harness/pause-actions.ts +120 -0
  54. package/src/harness/plannotator-result-actions.ts +123 -0
  55. package/src/harness/prompt-gate-actions.ts +270 -0
  56. package/src/harness/resume-action.ts +261 -0
  57. package/src/harness/start-actions.ts +183 -0
  58. package/src/harness/status-actions.ts +191 -0
  59. package/src/harness/step-execution-actions.ts +283 -0
  60. package/src/harness/types.ts +76 -0
  61. package/src/harness.ts +236 -2303
  62. package/src/index.ts +73 -11
  63. package/src/integrations/plannotator-requests.ts +127 -0
  64. package/src/integrations/plannotator-responses.ts +141 -0
  65. package/src/integrations/plannotator-types.ts +44 -0
  66. package/src/integrations/plannotator.ts +16 -235
  67. package/src/integrations/prompt-gate.ts +17 -10
  68. package/src/integrations/subagents/child-policy-envelope.ts +119 -0
  69. package/src/integrations/subagents/child-policy-paths.ts +66 -0
  70. package/src/integrations/subagents/child-policy-sections.ts +174 -0
  71. package/src/integrations/subagents/child-policy-types.ts +32 -0
  72. package/src/integrations/subagents/child-policy-validation.ts +163 -0
  73. package/src/integrations/subagents/child-runtime-completion.ts +56 -0
  74. package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
  75. package/src/integrations/subagents/child-runtime-files.ts +156 -0
  76. package/src/integrations/subagents/child-runtime-policy.ts +83 -0
  77. package/src/integrations/subagents/child-runtime-types.ts +30 -0
  78. package/src/integrations/subagents/child-runtime.ts +140 -301
  79. package/src/integrations/subagents/client-delegation.ts +181 -0
  80. package/src/integrations/subagents/client-messages.ts +66 -0
  81. package/src/integrations/subagents/client-types.ts +36 -0
  82. package/src/integrations/subagents/client.ts +127 -219
  83. package/src/integrations/subagents/delegated-result.ts +31 -0
  84. package/src/integrations/subagents/diagnostic-format.ts +40 -0
  85. package/src/integrations/subagents/diagnostic-text.ts +114 -0
  86. package/src/integrations/subagents/diagnostic-types.ts +77 -0
  87. package/src/integrations/subagents/diagnostics.ts +24 -977
  88. package/src/integrations/subagents/failure-correlation.ts +235 -0
  89. package/src/integrations/subagents/failure-transcript.ts +192 -0
  90. package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
  91. package/src/integrations/subagents/protocol-events.ts +27 -0
  92. package/src/integrations/subagents/protocol.ts +30 -375
  93. package/src/integrations/subagents/replay-audit.ts +147 -0
  94. package/src/integrations/subagents/replay-safety.ts +82 -0
  95. package/src/integrations/subagents/session-diagnostics.ts +258 -0
  96. package/src/policy/approved-command-extraction.ts +71 -0
  97. package/src/policy/approved-commands.ts +15 -432
  98. package/src/policy/bash-authorization.ts +86 -0
  99. package/src/policy/bash-read-only.ts +161 -0
  100. package/src/policy/bash-types.ts +20 -0
  101. package/src/policy/bash.ts +4 -346
  102. package/src/policy/completion-batch.ts +21 -16
  103. package/src/policy/immutable-input.ts +5 -2
  104. package/src/policy/mcp-authorization.ts +71 -0
  105. package/src/policy/restricted-command.ts +141 -0
  106. package/src/policy/restricted-git.ts +30 -0
  107. package/src/policy/reviewed-artifact.ts +97 -0
  108. package/src/policy/reviewed-command-safety.ts +150 -0
  109. package/src/policy/reviewed-command-shape.ts +49 -0
  110. package/src/policy/reviewed-repository-contract.ts +138 -0
  111. package/src/policy/reviewed-repository-cwd.ts +102 -0
  112. package/src/policy/tool-call-authorization.ts +58 -0
  113. package/src/policy/tool-selection.ts +72 -0
  114. package/src/policy/tool-types.ts +14 -0
  115. package/src/policy/tools.ts +11 -150
  116. package/src/preflight.ts +96 -61
  117. package/src/prompt/main-workflow-notice.ts +41 -0
  118. package/src/prompt/retry-task.ts +83 -0
  119. package/src/prompt/step-contract.ts +101 -0
  120. package/src/prompt/step-sections.ts +75 -0
  121. package/src/prompt/step-task.ts +156 -0
  122. package/src/prompt/template.ts +81 -0
  123. package/src/prompt.ts +10 -254
  124. package/src/runtime/completion-tool.ts +3 -0
  125. package/src/runtime/main-step-completion.ts +78 -0
  126. package/src/runtime/main-step-lifecycle.ts +37 -0
  127. package/src/runtime/main-step-policy.ts +84 -0
  128. package/src/runtime/main-step-runtime-types.ts +64 -0
  129. package/src/runtime/main-step-runtime.ts +129 -200
  130. package/src/runtime/main-step-state.ts +85 -0
  131. package/src/runtime/serial-task-queue.ts +48 -13
  132. package/src/runtime/step-result.ts +48 -27
  133. package/src/workflow-list.ts +15 -6
  134. package/src/workflow-status/format-status.ts +52 -0
  135. package/src/workflow-status/formatting.ts +178 -0
  136. package/src/workflow-status/layout.ts +109 -0
  137. package/src/workflow-status/render-board.ts +101 -0
  138. package/src/workflow-status/render-path.ts +123 -0
  139. package/src/workflow-status/render-summary.ts +144 -0
  140. package/src/workflow-status/types.ts +40 -0
  141. package/src/workflow-status/view.ts +273 -0
  142. package/src/workflow-status.ts +20 -820
@@ -0,0 +1,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
+ }