@wichayutdew/pi-workflows 0.2.3 → 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 +66 -38
- package/dist/index.js +5903 -4782
- 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 +237 -2201
- 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 -799
- 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 -245
- 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,154 @@
|
|
|
1
|
+
import type { LoadedWorkflow } from '../config/types.ts';
|
|
2
|
+
import type { WorkflowRun } from '../engine/state.ts';
|
|
3
|
+
import {
|
|
4
|
+
attachGateReviewId,
|
|
5
|
+
beginGate,
|
|
6
|
+
failGate,
|
|
7
|
+
failRun,
|
|
8
|
+
resolveGate,
|
|
9
|
+
} from '../engine/transitions.ts';
|
|
10
|
+
import { reviewedCommandShapeError } from '../policy/approved-commands.ts';
|
|
11
|
+
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
12
|
+
|
|
13
|
+
type HarnessActionContext = Pick<
|
|
14
|
+
FullHarnessActionContext,
|
|
15
|
+
| 'dependencies'
|
|
16
|
+
| 'isSessionActive'
|
|
17
|
+
| 'latestContext'
|
|
18
|
+
| 'launchPromptReview'
|
|
19
|
+
| 'persist'
|
|
20
|
+
| 'pi'
|
|
21
|
+
| 'restoreBaselineTools'
|
|
22
|
+
| 'run'
|
|
23
|
+
| 'sessionEpoch'
|
|
24
|
+
| 'settleAfterTransition'
|
|
25
|
+
| 'updateStatus'
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
export type GateSubmissionAction = {
|
|
29
|
+
submitGate: (
|
|
30
|
+
this: HarnessActionContext,
|
|
31
|
+
workflow: LoadedWorkflow,
|
|
32
|
+
originalRun: WorkflowRun,
|
|
33
|
+
outcome: string,
|
|
34
|
+
artifact: string,
|
|
35
|
+
) => Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function isCurrentGateRequest(
|
|
39
|
+
run: WorkflowRun | undefined,
|
|
40
|
+
originalRun: WorkflowRun,
|
|
41
|
+
requestId: string,
|
|
42
|
+
): run is WorkflowRun {
|
|
43
|
+
return (
|
|
44
|
+
run !== undefined &&
|
|
45
|
+
run.runId === originalRun.runId &&
|
|
46
|
+
run.currentStepId === originalRun.currentStepId &&
|
|
47
|
+
run.pendingGate?.requestId === requestId &&
|
|
48
|
+
run.pendingGate.reviewId === undefined &&
|
|
49
|
+
(run.status === 'awaiting-gate' || run.status === 'paused')
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function submitGate(
|
|
54
|
+
this: HarnessActionContext,
|
|
55
|
+
workflow: LoadedWorkflow,
|
|
56
|
+
originalRun: WorkflowRun,
|
|
57
|
+
outcome: string,
|
|
58
|
+
artifact: string,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
const requestSessionEpoch = this.sessionEpoch;
|
|
61
|
+
const requestId =
|
|
62
|
+
`${originalRun.runId}:${originalRun.currentStepId}:` +
|
|
63
|
+
this.dependencies.createRequestId();
|
|
64
|
+
const step = workflow.definition.steps[originalRun.currentStepId];
|
|
65
|
+
if (!step?.gate) throw new Error('Current step has no gate');
|
|
66
|
+
|
|
67
|
+
const commandShapeError = reviewedCommandShapeError(artifact);
|
|
68
|
+
if (commandShapeError) {
|
|
69
|
+
const awaitingReview = beginGate(
|
|
70
|
+
workflow,
|
|
71
|
+
originalRun,
|
|
72
|
+
outcome,
|
|
73
|
+
artifact,
|
|
74
|
+
requestId,
|
|
75
|
+
this.dependencies.now(),
|
|
76
|
+
);
|
|
77
|
+
this.run = resolveGate(
|
|
78
|
+
workflow,
|
|
79
|
+
awaitingReview,
|
|
80
|
+
{
|
|
81
|
+
approved: false,
|
|
82
|
+
feedback: commandShapeError,
|
|
83
|
+
resolvedAt: this.dependencies.now(),
|
|
84
|
+
},
|
|
85
|
+
this.dependencies.now(),
|
|
86
|
+
);
|
|
87
|
+
this.latestContext?.ui.notify(
|
|
88
|
+
`Plan contract needs repair before review: ${commandShapeError}`,
|
|
89
|
+
'warning',
|
|
90
|
+
);
|
|
91
|
+
this.settleAfterTransition(workflow);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.run = beginGate(
|
|
96
|
+
workflow,
|
|
97
|
+
originalRun,
|
|
98
|
+
outcome,
|
|
99
|
+
artifact,
|
|
100
|
+
requestId,
|
|
101
|
+
this.dependencies.now(),
|
|
102
|
+
);
|
|
103
|
+
this.persist();
|
|
104
|
+
this.restoreBaselineTools();
|
|
105
|
+
this.updateStatus();
|
|
106
|
+
|
|
107
|
+
if (step.gate.provider === 'prompt') {
|
|
108
|
+
this.launchPromptReview(workflow, this.run, this.latestContext);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const response = await this.dependencies.requestPlannotatorReview(
|
|
113
|
+
this.pi.events,
|
|
114
|
+
requestId,
|
|
115
|
+
artifact,
|
|
116
|
+
`pi-workflows:${workflow.definition.id}:${originalRun.currentStepId}`,
|
|
117
|
+
step.gate.timeoutMs,
|
|
118
|
+
);
|
|
119
|
+
const currentRun = this.run;
|
|
120
|
+
if (
|
|
121
|
+
!this.isSessionActive ||
|
|
122
|
+
this.sessionEpoch !== requestSessionEpoch ||
|
|
123
|
+
!isCurrentGateRequest(currentRun, originalRun, requestId)
|
|
124
|
+
) {
|
|
125
|
+
throw new Error('Gate request was superseded by a workflow state change');
|
|
126
|
+
}
|
|
127
|
+
if (response.status !== 'handled') {
|
|
128
|
+
const reason = response.error ?? 'Plannotator is unavailable';
|
|
129
|
+
const gateFailed = failGate(currentRun, reason, this.dependencies.now());
|
|
130
|
+
this.run = failRun(gateFailed, reason, this.dependencies.now());
|
|
131
|
+
this.persist();
|
|
132
|
+
this.restoreBaselineTools();
|
|
133
|
+
this.updateStatus();
|
|
134
|
+
throw new Error(reason);
|
|
135
|
+
}
|
|
136
|
+
this.run = attachGateReviewId(
|
|
137
|
+
currentRun,
|
|
138
|
+
response.result.reviewId,
|
|
139
|
+
this.dependencies.now(),
|
|
140
|
+
);
|
|
141
|
+
this.persist();
|
|
142
|
+
this.updateStatus();
|
|
143
|
+
this.latestContext?.ui.notify(
|
|
144
|
+
`Submitted "${originalRun.currentStepId}" for Plannotator review ${response.result.reviewId}`,
|
|
145
|
+
'info',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Returns the gate-submission action for harness composition.
|
|
151
|
+
*/
|
|
152
|
+
export function createGateSubmissionAction(): GateSubmissionAction {
|
|
153
|
+
return { submitGate };
|
|
154
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { failRun } from '../engine/transitions.ts';
|
|
2
|
+
import { buildMainWorkflowNotice } from '../prompt.ts';
|
|
3
|
+
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
4
|
+
import { parseAvailableSkills } from './catalog.ts';
|
|
5
|
+
import { waitForEventContextIdle } from './context-idle.ts';
|
|
6
|
+
|
|
7
|
+
type HarnessActionContext = Pick<
|
|
8
|
+
FullHarnessActionContext,
|
|
9
|
+
| 'cancelActiveDelegation'
|
|
10
|
+
| 'cancelPromptReview'
|
|
11
|
+
| 'captureSkills'
|
|
12
|
+
| 'catalog'
|
|
13
|
+
| 'dependencies'
|
|
14
|
+
| 'enqueueMutation'
|
|
15
|
+
| 'isSessionActive'
|
|
16
|
+
| 'latestContext'
|
|
17
|
+
| 'mainSteps'
|
|
18
|
+
| 'persist'
|
|
19
|
+
| 'pi'
|
|
20
|
+
| 'registeredWorkflowCommands'
|
|
21
|
+
| 'reloadCatalog'
|
|
22
|
+
| 'restoreBaselineTools'
|
|
23
|
+
| 'restoreFromSession'
|
|
24
|
+
| 'run'
|
|
25
|
+
| 'sessionEpoch'
|
|
26
|
+
| 'startNow'
|
|
27
|
+
| 'statusShortcutLabel'
|
|
28
|
+
| 'stopStatusRefresh'
|
|
29
|
+
| 'updateStatus'
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
export type LifecycleActions = {
|
|
33
|
+
registerMultilineCommandInput: (this: HarnessActionContext) => void;
|
|
34
|
+
registerLifecycle: (this: HarnessActionContext) => void;
|
|
35
|
+
registerPolicy: (this: HarnessActionContext) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function registerMultilineCommandInput(this: HarnessActionContext): void {
|
|
39
|
+
this.pi.on('input', async (event, context) => {
|
|
40
|
+
if (
|
|
41
|
+
event.source === 'extension' ||
|
|
42
|
+
event.images?.length ||
|
|
43
|
+
!event.text.startsWith('/')
|
|
44
|
+
) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const newlineIndex = event.text.indexOf('\n');
|
|
48
|
+
if (newlineIndex === -1) return;
|
|
49
|
+
const command = event.text.slice(1, newlineIndex).replace(/\r$/, '');
|
|
50
|
+
if (!this.registeredWorkflowCommands.has(command)) return;
|
|
51
|
+
const workflow = [...this.catalog.workflows.values()].find(
|
|
52
|
+
(candidate) => candidate.definition.command === command,
|
|
53
|
+
);
|
|
54
|
+
if (!workflow) return;
|
|
55
|
+
|
|
56
|
+
const input = event.text.slice(newlineIndex + 1);
|
|
57
|
+
const skills = parseAvailableSkills(context.getSystemPrompt());
|
|
58
|
+
try {
|
|
59
|
+
await this.enqueueMutation(context, (sessionEpoch) =>
|
|
60
|
+
this.startNow(
|
|
61
|
+
workflow.definition.id,
|
|
62
|
+
input,
|
|
63
|
+
{
|
|
64
|
+
context,
|
|
65
|
+
skills: () => skills,
|
|
66
|
+
waitForIdle: () =>
|
|
67
|
+
waitForEventContextIdle(context, this.dependencies),
|
|
68
|
+
},
|
|
69
|
+
sessionEpoch,
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
context.ui.notify(
|
|
74
|
+
`Cannot start workflow: ${error instanceof Error ? error.message : String(error)}`,
|
|
75
|
+
'error',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return { action: 'handled' as const };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function registerLifecycle(this: HarnessActionContext): void {
|
|
83
|
+
this.pi.on('session_start', async (_event, context) => {
|
|
84
|
+
this.sessionEpoch += 1;
|
|
85
|
+
this.isSessionActive = false;
|
|
86
|
+
this.cancelPromptReview();
|
|
87
|
+
this.mainSteps.deactivate();
|
|
88
|
+
await this.cancelActiveDelegation('Pi session changed');
|
|
89
|
+
if (this.run) this.restoreBaselineTools();
|
|
90
|
+
this.run = undefined;
|
|
91
|
+
this.latestContext = context;
|
|
92
|
+
if (!(await this.reloadCatalog(context, false))) return;
|
|
93
|
+
this.restoreFromSession(context);
|
|
94
|
+
this.isSessionActive = true;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this.pi.on('session_tree', async (_event, context) => {
|
|
98
|
+
this.sessionEpoch += 1;
|
|
99
|
+
this.isSessionActive = false;
|
|
100
|
+
this.cancelPromptReview();
|
|
101
|
+
this.mainSteps.deactivate();
|
|
102
|
+
await this.cancelActiveDelegation('Pi session tree changed');
|
|
103
|
+
this.latestContext = context;
|
|
104
|
+
if (!(await this.reloadCatalog(context, false))) return;
|
|
105
|
+
this.restoreFromSession(context);
|
|
106
|
+
this.isSessionActive = true;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this.pi.on('session_shutdown', async () => {
|
|
110
|
+
this.sessionEpoch += 1;
|
|
111
|
+
this.isSessionActive = false;
|
|
112
|
+
this.cancelPromptReview();
|
|
113
|
+
this.mainSteps.deactivate();
|
|
114
|
+
await this.cancelActiveDelegation('Pi session shut down');
|
|
115
|
+
if (this.run) this.restoreBaselineTools();
|
|
116
|
+
this.run = undefined;
|
|
117
|
+
this.latestContext = undefined;
|
|
118
|
+
this.stopStatusRefresh();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function registerPolicy(this: HarnessActionContext): void {
|
|
123
|
+
this.pi.on('before_agent_start', (event, context) => {
|
|
124
|
+
this.latestContext = context;
|
|
125
|
+
this.captureSkills(event.systemPromptOptions.skills);
|
|
126
|
+
if (!this.run || this.run.status !== 'running') return;
|
|
127
|
+
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
128
|
+
if (!workflow) {
|
|
129
|
+
this.run = failRun(
|
|
130
|
+
this.run,
|
|
131
|
+
'Workflow configuration disappeared; reload or restore it',
|
|
132
|
+
this.dependencies.now(),
|
|
133
|
+
);
|
|
134
|
+
this.persist();
|
|
135
|
+
this.restoreBaselineTools();
|
|
136
|
+
this.updateStatus();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
systemPrompt: `${event.systemPrompt}\n\n${buildMainWorkflowNotice(workflow, this.run, this.statusShortcutLabel)}`,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Returns Pi lifecycle and input registration actions for composition.
|
|
147
|
+
*/
|
|
148
|
+
export function createLifecycleActions(): LifecycleActions {
|
|
149
|
+
return {
|
|
150
|
+
registerMultilineCommandInput,
|
|
151
|
+
registerLifecycle,
|
|
152
|
+
registerPolicy,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { abortRun, pauseRun } from '../engine/transitions.ts';
|
|
3
|
+
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
4
|
+
|
|
5
|
+
type HarnessActionContext = Pick<
|
|
6
|
+
FullHarnessActionContext,
|
|
7
|
+
| 'cancelActiveDelegation'
|
|
8
|
+
| 'cancelPromptReview'
|
|
9
|
+
| 'dependencies'
|
|
10
|
+
| 'isolateMainSessionTools'
|
|
11
|
+
| 'mainSteps'
|
|
12
|
+
| 'persist'
|
|
13
|
+
| 'restoreBaselineTools'
|
|
14
|
+
| 'run'
|
|
15
|
+
| 'updateStatus'
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
export type PauseActions = {
|
|
19
|
+
pauseNow: (
|
|
20
|
+
this: HarnessActionContext,
|
|
21
|
+
reason: string,
|
|
22
|
+
context: ExtensionCommandContext,
|
|
23
|
+
) => Promise<void>;
|
|
24
|
+
abortNow: (
|
|
25
|
+
this: HarnessActionContext,
|
|
26
|
+
reason: string,
|
|
27
|
+
context: ExtensionCommandContext,
|
|
28
|
+
) => Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function pauseNow(
|
|
32
|
+
this: HarnessActionContext,
|
|
33
|
+
reason: string,
|
|
34
|
+
context: ExtensionCommandContext,
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
if (
|
|
37
|
+
!this.run ||
|
|
38
|
+
this.run.status === 'completed' ||
|
|
39
|
+
this.run.status === 'aborted'
|
|
40
|
+
) {
|
|
41
|
+
context.ui.notify('No active workflow to pause', 'warning');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (this.run.status === 'paused') {
|
|
45
|
+
context.ui.notify(
|
|
46
|
+
`Workflow is already paused${this.run.pauseReason ? `: ${this.run.pauseReason}` : ''}`,
|
|
47
|
+
'info',
|
|
48
|
+
);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.cancelPromptReview();
|
|
52
|
+
const isMainStepSuspended = this.mainSteps.suspend();
|
|
53
|
+
const isCancellationConfirmed = await this.cancelActiveDelegation(
|
|
54
|
+
'Workflow paused by user',
|
|
55
|
+
);
|
|
56
|
+
if (!context.isIdle()) {
|
|
57
|
+
context.abort();
|
|
58
|
+
if (isMainStepSuspended) await context.waitForIdle();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.run = pauseRun(this.run, reason, this.dependencies.now());
|
|
62
|
+
this.persist();
|
|
63
|
+
if (isCancellationConfirmed) {
|
|
64
|
+
this.restoreBaselineTools();
|
|
65
|
+
} else {
|
|
66
|
+
this.isolateMainSessionTools();
|
|
67
|
+
}
|
|
68
|
+
this.updateStatus();
|
|
69
|
+
context.ui.notify(
|
|
70
|
+
isCancellationConfirmed
|
|
71
|
+
? `Paused "${this.run.workflowId}" at step "${this.run.currentStepId}". Fix the issue, then run /workflow-resume.`
|
|
72
|
+
: `Pause recorded at "${this.run.currentStepId}", but child cancellation is not confirmed. Main tools remain isolated until it exits.`,
|
|
73
|
+
isCancellationConfirmed ? 'info' : 'warning',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function abortNow(
|
|
78
|
+
this: HarnessActionContext,
|
|
79
|
+
reason: string,
|
|
80
|
+
context: ExtensionCommandContext,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
if (
|
|
83
|
+
!this.run ||
|
|
84
|
+
this.run.status === 'completed' ||
|
|
85
|
+
this.run.status === 'aborted'
|
|
86
|
+
) {
|
|
87
|
+
context.ui.notify('No active workflow to abort', 'warning');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.cancelPromptReview();
|
|
91
|
+
const isMainStepSuspended = this.mainSteps.suspend();
|
|
92
|
+
const isCancellationConfirmed = await this.cancelActiveDelegation(
|
|
93
|
+
'Workflow aborted by user',
|
|
94
|
+
);
|
|
95
|
+
if (!context.isIdle()) {
|
|
96
|
+
context.abort();
|
|
97
|
+
if (isMainStepSuspended) await context.waitForIdle();
|
|
98
|
+
}
|
|
99
|
+
this.run = abortRun(this.run, reason, this.dependencies.now());
|
|
100
|
+
this.persist();
|
|
101
|
+
if (isCancellationConfirmed) {
|
|
102
|
+
this.restoreBaselineTools();
|
|
103
|
+
} else {
|
|
104
|
+
this.isolateMainSessionTools();
|
|
105
|
+
}
|
|
106
|
+
this.updateStatus();
|
|
107
|
+
context.ui.notify(
|
|
108
|
+
isCancellationConfirmed
|
|
109
|
+
? `Aborted workflow "${this.run.workflowId}"`
|
|
110
|
+
: `Workflow "${this.run.workflowId}" is aborted, but its child has not confirmed cancellation; main tools remain isolated`,
|
|
111
|
+
isCancellationConfirmed ? 'info' : 'warning',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns pause and abort actions for harness composition.
|
|
117
|
+
*/
|
|
118
|
+
export function createPauseActions(): PauseActions {
|
|
119
|
+
return { pauseNow, abortNow };
|
|
120
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { GateResolution } from '../engine/state.ts';
|
|
2
|
+
import {
|
|
3
|
+
failRun,
|
|
4
|
+
resolveGate,
|
|
5
|
+
storeGateResolution,
|
|
6
|
+
} from '../engine/transitions.ts';
|
|
7
|
+
import {
|
|
8
|
+
parsePlannotatorResult,
|
|
9
|
+
PLANNOTATOR_RESULT_CHANNEL,
|
|
10
|
+
} from '../integrations/plannotator.ts';
|
|
11
|
+
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
12
|
+
|
|
13
|
+
type HarnessActionContext = Pick<
|
|
14
|
+
FullHarnessActionContext,
|
|
15
|
+
| 'catalog'
|
|
16
|
+
| 'dependencies'
|
|
17
|
+
| 'handlePlannotatorResult'
|
|
18
|
+
| 'isSessionActive'
|
|
19
|
+
| 'latestContext'
|
|
20
|
+
| 'mutationQueue'
|
|
21
|
+
| 'persist'
|
|
22
|
+
| 'pi'
|
|
23
|
+
| 'restoreBaselineTools'
|
|
24
|
+
| 'run'
|
|
25
|
+
| 'settleAfterTransition'
|
|
26
|
+
| 'updateStatus'
|
|
27
|
+
>;
|
|
28
|
+
|
|
29
|
+
export type PlannotatorResultActions = {
|
|
30
|
+
registerPlannotatorResults: (this: HarnessActionContext) => void;
|
|
31
|
+
handlePlannotatorResult: (
|
|
32
|
+
this: HarnessActionContext,
|
|
33
|
+
data: unknown,
|
|
34
|
+
) => Promise<void>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function registerPlannotatorResults(this: HarnessActionContext): void {
|
|
38
|
+
this.pi.events.on(PLANNOTATOR_RESULT_CHANNEL, (data) => {
|
|
39
|
+
void this.mutationQueue
|
|
40
|
+
.run(() => this.handlePlannotatorResult(data))
|
|
41
|
+
.catch((error: unknown) => {
|
|
42
|
+
this.latestContext?.ui.notify(
|
|
43
|
+
`Cannot apply Plannotator result: ${
|
|
44
|
+
error instanceof Error ? error.message : String(error)
|
|
45
|
+
}`,
|
|
46
|
+
'error',
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function handlePlannotatorResult(
|
|
53
|
+
this: HarnessActionContext,
|
|
54
|
+
data: unknown,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
if (
|
|
57
|
+
!this.isSessionActive ||
|
|
58
|
+
this.run?.pendingGate?.provider !== 'plannotator' ||
|
|
59
|
+
!this.run.pendingGate.reviewId
|
|
60
|
+
) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const result = parsePlannotatorResult(data);
|
|
64
|
+
if (!result || result.reviewId !== this.run.pendingGate.reviewId) return;
|
|
65
|
+
|
|
66
|
+
const resolution: GateResolution = {
|
|
67
|
+
approved: result.approved,
|
|
68
|
+
feedback: result.feedback,
|
|
69
|
+
resolvedAt: this.dependencies.now(),
|
|
70
|
+
};
|
|
71
|
+
if (this.run.status === 'paused') {
|
|
72
|
+
this.run = storeGateResolution(
|
|
73
|
+
this.run,
|
|
74
|
+
resolution,
|
|
75
|
+
this.dependencies.now(),
|
|
76
|
+
);
|
|
77
|
+
this.persist();
|
|
78
|
+
this.latestContext?.ui.notify(
|
|
79
|
+
`Review ${result.reviewId} finished while paused. Run /workflow-resume to apply it.`,
|
|
80
|
+
'info',
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (this.run.status !== 'awaiting-gate') return;
|
|
85
|
+
|
|
86
|
+
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
87
|
+
if (!workflow) {
|
|
88
|
+
this.run = failRun(
|
|
89
|
+
this.run,
|
|
90
|
+
'Gate result arrived, but workflow configuration is unavailable',
|
|
91
|
+
this.dependencies.now(),
|
|
92
|
+
);
|
|
93
|
+
this.persist();
|
|
94
|
+
this.restoreBaselineTools();
|
|
95
|
+
this.updateStatus();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
this.run = resolveGate(
|
|
100
|
+
workflow,
|
|
101
|
+
this.run,
|
|
102
|
+
resolution,
|
|
103
|
+
this.dependencies.now(),
|
|
104
|
+
);
|
|
105
|
+
this.settleAfterTransition(workflow);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.run = failRun(
|
|
108
|
+
this.run,
|
|
109
|
+
`Cannot apply gate result: ${error instanceof Error ? error.message : String(error)}`,
|
|
110
|
+
this.dependencies.now(),
|
|
111
|
+
);
|
|
112
|
+
this.persist();
|
|
113
|
+
this.restoreBaselineTools();
|
|
114
|
+
this.updateStatus();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Returns Plannotator result-channel actions for harness composition.
|
|
120
|
+
*/
|
|
121
|
+
export function createPlannotatorResultActions(): PlannotatorResultActions {
|
|
122
|
+
return { registerPlannotatorResults, handlePlannotatorResult };
|
|
123
|
+
}
|