@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.
- package/README.md +53 -39
- package/dist/index.js +5754 -4794
- package/package.json +1 -1
- package/src/command-names.ts +7 -1
- package/src/commands.ts +126 -52
- package/src/config/catalog.ts +162 -0
- package/src/config/ceiling.ts +116 -116
- package/src/config/command-conflicts.ts +4 -4
- package/src/config/diagnostics.ts +28 -0
- package/src/config/load-settings.ts +37 -0
- package/src/config/load-types.ts +53 -0
- package/src/config/load-workflows.ts +135 -0
- package/src/config/load.ts +43 -311
- package/src/config/types.ts +137 -135
- package/src/config/validate.ts +12 -1261
- package/src/config/validation/permissions.ts +291 -0
- package/src/config/validation/prompt.ts +19 -0
- package/src/config/validation/settings.ts +122 -0
- package/src/config/validation/shared.ts +129 -0
- package/src/config/validation/shortcut.ts +118 -0
- package/src/config/validation/step.ts +228 -0
- package/src/config/validation/subagent.ts +288 -0
- package/src/config/validation/workflow.ts +156 -0
- package/src/config/yaml.ts +29 -0
- package/src/digest.ts +38 -6
- package/src/engine/checkpoint.ts +13 -9
- package/src/engine/create-run.ts +42 -0
- package/src/engine/gate-transitions.ts +184 -0
- package/src/engine/reconciliation-history.ts +88 -0
- package/src/engine/resume.ts +19 -7
- package/src/engine/run-advance.ts +122 -0
- package/src/engine/run-lifecycle.ts +124 -0
- package/src/engine/run-reconciliation.ts +116 -0
- package/src/engine/run-validation.ts +128 -0
- package/src/engine/state-types.ts +64 -0
- package/src/engine/state.ts +10 -193
- package/src/engine/transition-helpers.ts +28 -0
- package/src/engine/transition-types.ts +8 -0
- package/src/engine/transitions.ts +17 -471
- package/src/harness/action-context.ts +181 -0
- package/src/harness/catalog.ts +45 -0
- package/src/harness/context-idle.ts +21 -0
- package/src/harness/core-actions.ts +299 -0
- package/src/harness/delegation-control-actions.ts +250 -0
- package/src/harness/delegation-failure.ts +215 -0
- package/src/harness/delegation-plan.ts +219 -0
- package/src/harness/delegation-recovery-validation.ts +159 -0
- package/src/harness/delegation-response-actions.ts +295 -0
- package/src/harness/delegation-retry-policy.ts +118 -0
- package/src/harness/dependencies.ts +165 -0
- package/src/harness/gate-submission-action.ts +154 -0
- package/src/harness/lifecycle-actions.ts +154 -0
- package/src/harness/pause-actions.ts +120 -0
- package/src/harness/plannotator-result-actions.ts +123 -0
- package/src/harness/prompt-gate-actions.ts +270 -0
- package/src/harness/resume-action.ts +261 -0
- package/src/harness/start-actions.ts +183 -0
- package/src/harness/status-actions.ts +191 -0
- package/src/harness/step-execution-actions.ts +283 -0
- package/src/harness/types.ts +76 -0
- package/src/harness.ts +236 -2303
- package/src/index.ts +73 -11
- package/src/integrations/plannotator-requests.ts +127 -0
- package/src/integrations/plannotator-responses.ts +141 -0
- package/src/integrations/plannotator-types.ts +44 -0
- package/src/integrations/plannotator.ts +16 -235
- package/src/integrations/prompt-gate.ts +17 -10
- package/src/integrations/subagents/child-policy-envelope.ts +119 -0
- package/src/integrations/subagents/child-policy-paths.ts +66 -0
- package/src/integrations/subagents/child-policy-sections.ts +174 -0
- package/src/integrations/subagents/child-policy-types.ts +32 -0
- package/src/integrations/subagents/child-policy-validation.ts +163 -0
- package/src/integrations/subagents/child-runtime-completion.ts +56 -0
- package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
- package/src/integrations/subagents/child-runtime-files.ts +156 -0
- package/src/integrations/subagents/child-runtime-policy.ts +83 -0
- package/src/integrations/subagents/child-runtime-types.ts +30 -0
- package/src/integrations/subagents/child-runtime.ts +140 -301
- package/src/integrations/subagents/client-delegation.ts +181 -0
- package/src/integrations/subagents/client-messages.ts +66 -0
- package/src/integrations/subagents/client-types.ts +36 -0
- package/src/integrations/subagents/client.ts +127 -219
- package/src/integrations/subagents/delegated-result.ts +31 -0
- package/src/integrations/subagents/diagnostic-format.ts +40 -0
- package/src/integrations/subagents/diagnostic-text.ts +114 -0
- package/src/integrations/subagents/diagnostic-types.ts +77 -0
- package/src/integrations/subagents/diagnostics.ts +24 -977
- package/src/integrations/subagents/failure-correlation.ts +235 -0
- package/src/integrations/subagents/failure-transcript.ts +192 -0
- package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
- package/src/integrations/subagents/protocol-events.ts +27 -0
- package/src/integrations/subagents/protocol.ts +30 -375
- package/src/integrations/subagents/replay-audit.ts +147 -0
- package/src/integrations/subagents/replay-safety.ts +82 -0
- package/src/integrations/subagents/session-diagnostics.ts +258 -0
- package/src/policy/approved-command-extraction.ts +71 -0
- package/src/policy/approved-commands.ts +15 -432
- package/src/policy/bash-authorization.ts +86 -0
- package/src/policy/bash-read-only.ts +161 -0
- package/src/policy/bash-types.ts +20 -0
- package/src/policy/bash.ts +4 -346
- package/src/policy/completion-batch.ts +21 -16
- package/src/policy/immutable-input.ts +5 -2
- package/src/policy/mcp-authorization.ts +71 -0
- package/src/policy/restricted-command.ts +141 -0
- package/src/policy/restricted-git.ts +30 -0
- package/src/policy/reviewed-artifact.ts +97 -0
- package/src/policy/reviewed-command-safety.ts +150 -0
- package/src/policy/reviewed-command-shape.ts +49 -0
- package/src/policy/reviewed-repository-contract.ts +138 -0
- package/src/policy/reviewed-repository-cwd.ts +102 -0
- package/src/policy/tool-call-authorization.ts +58 -0
- package/src/policy/tool-selection.ts +72 -0
- package/src/policy/tool-types.ts +14 -0
- package/src/policy/tools.ts +11 -150
- package/src/preflight.ts +96 -61
- package/src/prompt/main-workflow-notice.ts +41 -0
- package/src/prompt/retry-task.ts +83 -0
- package/src/prompt/step-contract.ts +101 -0
- package/src/prompt/step-sections.ts +75 -0
- package/src/prompt/step-task.ts +156 -0
- package/src/prompt/template.ts +81 -0
- package/src/prompt.ts +10 -254
- package/src/runtime/completion-tool.ts +3 -0
- package/src/runtime/main-step-completion.ts +78 -0
- package/src/runtime/main-step-lifecycle.ts +37 -0
- package/src/runtime/main-step-policy.ts +84 -0
- package/src/runtime/main-step-runtime-types.ts +64 -0
- package/src/runtime/main-step-runtime.ts +129 -200
- package/src/runtime/main-step-state.ts +85 -0
- package/src/runtime/serial-task-queue.ts +48 -13
- package/src/runtime/step-result.ts +48 -27
- package/src/workflow-list.ts +15 -6
- package/src/workflow-status/format-status.ts +52 -0
- package/src/workflow-status/formatting.ts +178 -0
- package/src/workflow-status/layout.ts +109 -0
- package/src/workflow-status/render-board.ts +101 -0
- package/src/workflow-status/render-path.ts +123 -0
- package/src/workflow-status/render-summary.ts +144 -0
- package/src/workflow-status/types.ts +40 -0
- package/src/workflow-status/view.ts +273 -0
- 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
|
+
}
|