@wichayutdew/pi-workflows 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +312 -223
- package/agents/step.md +4 -11
- package/dist/index.js +2554 -976
- package/examples/mr-comments.workflow.yaml +16 -8
- package/examples/prompts/mr-comments/implement.md +13 -9
- package/examples/prompts/mr-comments/plan.md +44 -15
- package/examples/prompts/mr-comments/verify.md +8 -7
- package/examples/settings.yaml +7 -1
- package/examples/starter-kit/mr-comment.workflow.yaml +149 -0
- package/examples/starter-kit/mr-review.workflow.yaml +130 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +33 -0
- package/examples/starter-kit/steps/mr-comment/implement.md +38 -0
- package/examples/starter-kit/steps/mr-comment/plan.md +62 -0
- package/examples/starter-kit/steps/mr-comment/publish.md +36 -0
- package/examples/starter-kit/steps/mr-comment/verify.md +35 -0
- package/examples/starter-kit/steps/mr-review/fetch.md +25 -0
- package/examples/starter-kit/steps/mr-review/publish.md +30 -0
- package/examples/starter-kit/steps/mr-review/review.md +69 -0
- package/examples/starter-kit/steps/mr-review/verify.md +24 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +57 -0
- package/examples/starter-kit/steps/ticket/implement.md +35 -0
- package/examples/starter-kit/steps/ticket/plan.md +40 -0
- package/examples/starter-kit/steps/ticket/verify.md +29 -0
- package/examples/starter-kit/steps/work/implement.md +34 -0
- package/examples/starter-kit/steps/work/plan.md +38 -0
- package/examples/starter-kit/steps/work/verify.md +30 -0
- package/examples/starter-kit/ticket.workflow.yaml +100 -0
- package/examples/starter-kit/work.workflow.yaml +97 -0
- package/package.json +3 -3
- package/schemas/workflow.schema.json +78 -44
- package/src/command-names.ts +1 -0
- package/src/commands.ts +33 -4
- package/src/config/ceiling.ts +6 -12
- package/src/config/load-workflows.ts +12 -1
- package/src/config/step-digests.ts +19 -0
- package/src/config/types.ts +17 -22
- package/src/config/validation/permissions.ts +5 -66
- package/src/config/validation/prompt.ts +1 -0
- package/src/config/validation/shared.ts +0 -2
- package/src/config/validation/step.ts +106 -2
- package/src/config/validation/workflow.ts +76 -0
- package/src/engine/create-run.ts +3 -0
- package/src/engine/gate-transitions.ts +58 -9
- package/src/engine/reconciliation-history.ts +61 -24
- package/src/engine/run-advance.ts +37 -4
- package/src/engine/run-lifecycle.ts +11 -0
- package/src/engine/run-reconciliation.ts +51 -21
- package/src/engine/run-validation.ts +235 -0
- package/src/engine/run-workflow-validation.ts +191 -0
- package/src/engine/state-types.ts +108 -5
- package/src/engine/state.ts +15 -1
- package/src/engine/step-trace.ts +495 -0
- package/src/engine/transitions.ts +1 -0
- package/src/harness/action-context.ts +31 -6
- package/src/harness/core-actions.ts +30 -1
- package/src/harness/delegation-control-actions.ts +27 -2
- package/src/harness/delegation-failure.ts +42 -9
- package/src/harness/delegation-plan.ts +96 -40
- package/src/harness/delegation-recovery-validation.ts +3 -1
- package/src/harness/delegation-response-actions.ts +73 -3
- package/src/harness/delegation-retry-policy.ts +5 -3
- package/src/harness/dependencies.ts +11 -2
- package/src/harness/gate-submission-action.ts +5 -31
- package/src/harness/lifecycle-actions.ts +4 -5
- package/src/harness/pause-actions.ts +10 -1
- package/src/harness/plannotator-result-actions.ts +20 -11
- package/src/harness/prompt-gate-actions.ts +40 -2
- package/src/harness/resume-action.ts +75 -1
- package/src/harness/start-actions.ts +77 -3
- package/src/harness/status-actions.ts +7 -17
- package/src/harness/step-effects.ts +61 -0
- package/src/harness/step-execution-actions.ts +181 -25
- package/src/harness/step-reporting.ts +168 -0
- package/src/harness/types.ts +1 -0
- package/src/harness/workspace-directory.ts +67 -0
- package/src/harness.ts +37 -9
- package/src/integrations/plannotator-requests.ts +40 -5
- package/src/integrations/subagents/child-policy-sections.ts +71 -84
- package/src/integrations/subagents/child-policy-types.ts +7 -7
- package/src/integrations/subagents/child-policy-validation.ts +10 -4
- package/src/integrations/subagents/child-runtime-completion.ts +1 -0
- package/src/integrations/subagents/child-runtime-files.ts +29 -86
- package/src/integrations/subagents/child-runtime-policy.ts +8 -33
- package/src/integrations/subagents/child-runtime.ts +3 -15
- package/src/integrations/subagents/delegated-result.ts +1 -0
- package/src/integrations/subagents/diagnostic-format.ts +5 -0
- package/src/integrations/subagents/diagnostic-types.ts +7 -1
- package/src/integrations/subagents/diagnostics.ts +2 -0
- package/src/integrations/subagents/failure-correlation.ts +52 -2
- package/src/integrations/subagents/failure-transcript.ts +60 -1
- package/src/integrations/subagents/replay-audit.ts +0 -1
- package/src/integrations/subagents/replay-safety.ts +12 -27
- package/src/integrations/subagents/session-diagnostics.ts +102 -3
- package/src/policy/bash-authorization.ts +2 -21
- package/src/policy/bash-types.ts +0 -5
- package/src/policy/bash.ts +1 -2
- package/src/policy/tool-call-authorization.ts +1 -7
- package/src/preflight.ts +0 -5
- package/src/prompt/retry-task.ts +11 -3
- package/src/prompt/step-contract.ts +10 -56
- package/src/prompt/step-sections.ts +8 -10
- package/src/prompt/step-task.ts +41 -10
- package/src/prompt/template.ts +3 -2
- package/src/runtime/completion-tool.ts +14 -0
- package/src/runtime/main-step-completion.ts +3 -0
- package/src/runtime/main-step-policy.ts +2 -1
- package/src/runtime/main-step-runtime-types.ts +9 -1
- package/src/runtime/main-step-runtime.ts +2 -0
- package/src/runtime/main-step-state.ts +6 -0
- package/src/runtime/main-step-trace.ts +45 -0
- package/src/runtime/step-result.ts +57 -0
- package/src/step-log.ts +178 -0
- package/src/workflow-doctor.ts +254 -0
- package/src/workflow-status/format-status.ts +32 -3
- package/src/workflow-status/render-board.ts +7 -4
- package/src/workflow-status/render-path.ts +51 -24
- package/src/workflow-status/render-step-detail.ts +379 -0
- package/src/workflow-status/render-summary.ts +22 -0
- package/src/workflow-status/transcript-reader.ts +231 -0
- package/src/workflow-status/types.ts +8 -0
- package/src/workflow-status/view.ts +161 -14
- package/src/workflow-status.ts +6 -0
- package/src/policy/approved-command-extraction.ts +0 -71
- package/src/policy/approved-commands.ts +0 -15
- package/src/policy/bash-read-only.ts +0 -161
- package/src/policy/restricted-git.ts +0 -30
- package/src/policy/reviewed-artifact.ts +0 -97
- package/src/policy/reviewed-command-safety.ts +0 -150
- package/src/policy/reviewed-command-shape.ts +0 -49
- package/src/policy/reviewed-repository-contract.ts +0 -138
- package/src/policy/reviewed-repository-cwd.ts +0 -102
|
@@ -32,6 +32,9 @@ export function delegationFailureFingerprint(
|
|
|
32
32
|
tool: normalizedFingerprintField(failure.diagnostic.tool),
|
|
33
33
|
call: normalizedFingerprintField(failure.diagnostic.call),
|
|
34
34
|
output: normalizedFingerprintField(failure.diagnostic.output),
|
|
35
|
+
postCompletionWarning: normalizedFingerprintField(
|
|
36
|
+
failure.diagnostic.postCompletionWarning,
|
|
37
|
+
),
|
|
35
38
|
}
|
|
36
39
|
: undefined,
|
|
37
40
|
});
|
|
@@ -62,6 +65,7 @@ export function isRetryableTerminalFailure(
|
|
|
62
65
|
failure: DelegationFailureDetails,
|
|
63
66
|
): boolean {
|
|
64
67
|
if (failure.recoveryBlocker !== undefined) return false;
|
|
68
|
+
if (failure.diagnostic?.postCompletionWarning !== undefined) return false;
|
|
65
69
|
if (
|
|
66
70
|
failure.status === 'timed_out' ||
|
|
67
71
|
failure.status === 'turn_budget_exhausted' ||
|
|
@@ -96,9 +100,7 @@ export function isSafeToRetryDelegation(
|
|
|
96
100
|
): boolean {
|
|
97
101
|
return (
|
|
98
102
|
replayAudit?.replaySafe === true &&
|
|
99
|
-
(isReplayExplicitlyAuthorized ||
|
|
100
|
-
policy.permissions.bash.mode === 'deny' ||
|
|
101
|
-
policy.permissions.bash.mode === 'read-only')
|
|
103
|
+
(isReplayExplicitlyAuthorized || policy.permissions.bash.mode === 'deny')
|
|
102
104
|
);
|
|
103
105
|
}
|
|
104
106
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type SubagentDelegationClientController,
|
|
16
16
|
} from '../integrations/subagents/client.ts';
|
|
17
17
|
import {
|
|
18
|
+
auditCompletedDelegationTranscript,
|
|
18
19
|
readDelegationReplayAudit,
|
|
19
20
|
readToolFailureDiagnostic,
|
|
20
21
|
} from '../integrations/subagents/diagnostics.ts';
|
|
@@ -28,6 +29,10 @@ import {
|
|
|
28
29
|
} from '../runtime/serial-task-queue.ts';
|
|
29
30
|
import { showWorkflowStatus } from '../workflow-status.ts';
|
|
30
31
|
import type { ActiveDelegation } from './types.ts';
|
|
32
|
+
import {
|
|
33
|
+
resolveWorkspaceDirectory,
|
|
34
|
+
type ResolveWorkspaceDirectoryOptions,
|
|
35
|
+
} from './workspace-directory.ts';
|
|
31
36
|
|
|
32
37
|
const MAX_DELEGATED_RESULT_BYTES = 1024 * 1024;
|
|
33
38
|
|
|
@@ -47,14 +52,17 @@ export type WorkflowHarnessDependencies = {
|
|
|
47
52
|
readonly removeDelegationWorkspace: (
|
|
48
53
|
resultDirectory: string,
|
|
49
54
|
) => Promise<void>;
|
|
50
|
-
readonly currentWorkingDirectory: () => string;
|
|
51
55
|
readonly waitForDelay: (delayMs: number) => Promise<void>;
|
|
56
|
+
readonly resolveWorkspaceDirectory: (
|
|
57
|
+
options: ResolveWorkspaceDirectoryOptions,
|
|
58
|
+
) => string;
|
|
52
59
|
readonly loadCatalog: typeof loadCatalog;
|
|
53
60
|
readonly requestPlannotatorReview: typeof requestPlannotatorReview;
|
|
54
61
|
readonly requestPlannotatorReviewStatus: typeof requestPlannotatorReviewStatus;
|
|
55
62
|
readonly requestPromptGateReview: typeof requestPromptGateReview;
|
|
56
63
|
readonly readDelegationReplayAudit: typeof readDelegationReplayAudit;
|
|
57
64
|
readonly readToolFailureDiagnostic: typeof readToolFailureDiagnostic;
|
|
65
|
+
readonly auditCompletedDelegationTranscript: typeof auditCompletedDelegationTranscript;
|
|
58
66
|
readonly showWorkflowStatus: typeof showWorkflowStatus;
|
|
59
67
|
readonly createSubagentClient: (
|
|
60
68
|
pi: ExtensionAPI,
|
|
@@ -132,15 +140,16 @@ const DEFAULT_DEPENDENCIES: WorkflowHarnessDependencies = {
|
|
|
132
140
|
readDelegatedResult,
|
|
133
141
|
removeDelegationWorkspace: async (resultDirectory) =>
|
|
134
142
|
rm(resultDirectory, { recursive: true, force: true }),
|
|
135
|
-
currentWorkingDirectory: () => process.cwd(),
|
|
136
143
|
waitForDelay: async (delayMs) =>
|
|
137
144
|
new Promise((resolve) => setTimeout(resolve, delayMs)),
|
|
145
|
+
resolveWorkspaceDirectory,
|
|
138
146
|
loadCatalog,
|
|
139
147
|
requestPlannotatorReview,
|
|
140
148
|
requestPlannotatorReviewStatus,
|
|
141
149
|
requestPromptGateReview,
|
|
142
150
|
readDelegationReplayAudit,
|
|
143
151
|
readToolFailureDiagnostic,
|
|
152
|
+
auditCompletedDelegationTranscript,
|
|
144
153
|
showWorkflowStatus,
|
|
145
154
|
createSubagentClient: (pi) => createSubagentDelegationClient(pi.events),
|
|
146
155
|
createMainStepRuntime: (pi) => createMainStepRuntime({ pi }),
|
|
@@ -5,10 +5,9 @@ import {
|
|
|
5
5
|
beginGate,
|
|
6
6
|
failGate,
|
|
7
7
|
failRun,
|
|
8
|
-
resolveGate,
|
|
9
8
|
} from '../engine/transitions.ts';
|
|
10
|
-
import { reviewedCommandShapeError } from '../policy/approved-commands.ts';
|
|
11
9
|
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
10
|
+
import { reportFailedStep } from './step-reporting.ts';
|
|
12
11
|
|
|
13
12
|
type HarnessActionContext = Pick<
|
|
14
13
|
FullHarnessActionContext,
|
|
@@ -21,7 +20,6 @@ type HarnessActionContext = Pick<
|
|
|
21
20
|
| 'restoreBaselineTools'
|
|
22
21
|
| 'run'
|
|
23
22
|
| 'sessionEpoch'
|
|
24
|
-
| 'settleAfterTransition'
|
|
25
23
|
| 'updateStatus'
|
|
26
24
|
>;
|
|
27
25
|
|
|
@@ -31,6 +29,7 @@ export type GateSubmissionAction = {
|
|
|
31
29
|
workflow: LoadedWorkflow,
|
|
32
30
|
originalRun: WorkflowRun,
|
|
33
31
|
outcome: string,
|
|
32
|
+
summary: string,
|
|
34
33
|
artifact: string,
|
|
35
34
|
) => Promise<void>;
|
|
36
35
|
};
|
|
@@ -55,6 +54,7 @@ async function submitGate(
|
|
|
55
54
|
workflow: LoadedWorkflow,
|
|
56
55
|
originalRun: WorkflowRun,
|
|
57
56
|
outcome: string,
|
|
57
|
+
summary: string,
|
|
58
58
|
artifact: string,
|
|
59
59
|
): Promise<void> {
|
|
60
60
|
const requestSessionEpoch = this.sessionEpoch;
|
|
@@ -64,34 +64,6 @@ async function submitGate(
|
|
|
64
64
|
const step = workflow.definition.steps[originalRun.currentStepId];
|
|
65
65
|
if (!step?.gate) throw new Error('Current step has no gate');
|
|
66
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
67
|
this.run = beginGate(
|
|
96
68
|
workflow,
|
|
97
69
|
originalRun,
|
|
@@ -99,6 +71,7 @@ async function submitGate(
|
|
|
99
71
|
artifact,
|
|
100
72
|
requestId,
|
|
101
73
|
this.dependencies.now(),
|
|
74
|
+
summary,
|
|
102
75
|
);
|
|
103
76
|
this.persist();
|
|
104
77
|
this.restoreBaselineTools();
|
|
@@ -129,6 +102,7 @@ async function submitGate(
|
|
|
129
102
|
const gateFailed = failGate(currentRun, reason, this.dependencies.now());
|
|
130
103
|
this.run = failRun(gateFailed, reason, this.dependencies.now());
|
|
131
104
|
this.persist();
|
|
105
|
+
reportFailedStep(this.pi, workflow, this.run, reason);
|
|
132
106
|
this.restoreBaselineTools();
|
|
133
107
|
this.updateStatus();
|
|
134
108
|
throw new Error(reason);
|
|
@@ -3,6 +3,7 @@ import { buildMainWorkflowNotice } from '../prompt.ts';
|
|
|
3
3
|
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
4
4
|
import { parseAvailableSkills } from './catalog.ts';
|
|
5
5
|
import { waitForEventContextIdle } from './context-idle.ts';
|
|
6
|
+
import { reportFailedStep } from './step-reporting.ts';
|
|
6
7
|
|
|
7
8
|
type HarnessActionContext = Pick<
|
|
8
9
|
FullHarnessActionContext,
|
|
@@ -126,12 +127,10 @@ function registerPolicy(this: HarnessActionContext): void {
|
|
|
126
127
|
if (!this.run || this.run.status !== 'running') return;
|
|
127
128
|
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
128
129
|
if (!workflow) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
'Workflow configuration disappeared; reload or restore it',
|
|
132
|
-
this.dependencies.now(),
|
|
133
|
-
);
|
|
130
|
+
const reason = 'Workflow configuration disappeared; reload or restore it';
|
|
131
|
+
this.run = failRun(this.run, reason, this.dependencies.now());
|
|
134
132
|
this.persist();
|
|
133
|
+
reportFailedStep(this.pi, undefined, this.run, reason);
|
|
135
134
|
this.restoreBaselineTools();
|
|
136
135
|
this.updateStatus();
|
|
137
136
|
return;
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { abortRun, pauseRun } from '../engine/transitions.ts';
|
|
3
3
|
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
4
|
+
import { conciseStepPauseSummary, reportPausedStep } from './step-reporting.ts';
|
|
4
5
|
|
|
5
6
|
type HarnessActionContext = Pick<
|
|
6
7
|
FullHarnessActionContext,
|
|
7
8
|
| 'cancelActiveDelegation'
|
|
8
9
|
| 'cancelPromptReview'
|
|
10
|
+
| 'catalog'
|
|
9
11
|
| 'dependencies'
|
|
10
12
|
| 'isolateMainSessionTools'
|
|
11
13
|
| 'mainSteps'
|
|
12
14
|
| 'persist'
|
|
15
|
+
| 'pi'
|
|
13
16
|
| 'restoreBaselineTools'
|
|
14
17
|
| 'run'
|
|
15
18
|
| 'updateStatus'
|
|
@@ -60,6 +63,12 @@ async function pauseNow(
|
|
|
60
63
|
|
|
61
64
|
this.run = pauseRun(this.run, reason, this.dependencies.now());
|
|
62
65
|
this.persist();
|
|
66
|
+
reportPausedStep(
|
|
67
|
+
this.pi,
|
|
68
|
+
this.catalog.workflows.get(this.run.workflowId),
|
|
69
|
+
this.run,
|
|
70
|
+
this.run.pauseReason ?? reason,
|
|
71
|
+
);
|
|
63
72
|
if (isCancellationConfirmed) {
|
|
64
73
|
this.restoreBaselineTools();
|
|
65
74
|
} else {
|
|
@@ -68,7 +77,7 @@ async function pauseNow(
|
|
|
68
77
|
this.updateStatus();
|
|
69
78
|
context.ui.notify(
|
|
70
79
|
isCancellationConfirmed
|
|
71
|
-
? `Paused "${this.run.workflowId}" at step "${this.run.currentStepId}".
|
|
80
|
+
? `Paused "${this.run.workflowId}" at step "${this.run.currentStepId}": ${conciseStepPauseSummary(this.run.pauseReason ?? reason)}`
|
|
72
81
|
: `Pause recorded at "${this.run.currentStepId}", but child cancellation is not confirmed. Main tools remain isolated until it exits.`,
|
|
73
82
|
isCancellationConfirmed ? 'info' : 'warning',
|
|
74
83
|
);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
PLANNOTATOR_RESULT_CHANNEL,
|
|
10
10
|
} from '../integrations/plannotator.ts';
|
|
11
11
|
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
12
|
+
import { reportFailedStep } from './step-reporting.ts';
|
|
12
13
|
|
|
13
14
|
type HarnessActionContext = Pick<
|
|
14
15
|
FullHarnessActionContext,
|
|
@@ -85,31 +86,39 @@ async function handlePlannotatorResult(
|
|
|
85
86
|
|
|
86
87
|
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
87
88
|
if (!workflow) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.dependencies.now(),
|
|
92
|
-
);
|
|
89
|
+
const reason =
|
|
90
|
+
'Gate result arrived, but workflow configuration is unavailable';
|
|
91
|
+
this.run = failRun(this.run, reason, this.dependencies.now());
|
|
93
92
|
this.persist();
|
|
93
|
+
reportFailedStep(this.pi, undefined, this.run, reason);
|
|
94
94
|
this.restoreBaselineTools();
|
|
95
95
|
this.updateStatus();
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
try {
|
|
99
|
+
const stepId = this.run.currentStepId;
|
|
100
|
+
const gate = workflow.definition.steps[stepId]?.gate;
|
|
101
|
+
if (!gate) throw new Error(`gated step "${stepId}" no longer exists`);
|
|
99
102
|
this.run = resolveGate(
|
|
100
103
|
workflow,
|
|
101
104
|
this.run,
|
|
102
105
|
resolution,
|
|
103
106
|
this.dependencies.now(),
|
|
104
107
|
);
|
|
105
|
-
this.settleAfterTransition(workflow
|
|
108
|
+
this.settleAfterTransition(workflow, {
|
|
109
|
+
stepId,
|
|
110
|
+
outcome: resolution.approved
|
|
111
|
+
? gate.approvedOutcome
|
|
112
|
+
: gate.rejectedOutcome,
|
|
113
|
+
summary: this.run.lastSummary,
|
|
114
|
+
});
|
|
106
115
|
} catch (error) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
);
|
|
116
|
+
const reason = `Cannot apply gate result: ${
|
|
117
|
+
error instanceof Error ? error.message : String(error)
|
|
118
|
+
}`;
|
|
119
|
+
this.run = failRun(this.run, reason, this.dependencies.now());
|
|
112
120
|
this.persist();
|
|
121
|
+
reportFailedStep(this.pi, workflow, this.run, reason);
|
|
113
122
|
this.restoreBaselineTools();
|
|
114
123
|
this.updateStatus();
|
|
115
124
|
}
|
|
@@ -9,6 +9,12 @@ import {
|
|
|
9
9
|
} from '../engine/transitions.ts';
|
|
10
10
|
import type { PromptGateReviewResult } from '../integrations/prompt-gate.ts';
|
|
11
11
|
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
12
|
+
import {
|
|
13
|
+
conciseStepFailureSummary,
|
|
14
|
+
conciseStepPauseSummary,
|
|
15
|
+
reportFailedStep,
|
|
16
|
+
reportPausedStep,
|
|
17
|
+
} from './step-reporting.ts';
|
|
12
18
|
import type { ActivePromptReview } from './types.ts';
|
|
13
19
|
|
|
14
20
|
type HarnessActionContext = Pick<
|
|
@@ -23,6 +29,7 @@ type HarnessActionContext = Pick<
|
|
|
23
29
|
| 'mutationQueue'
|
|
24
30
|
| 'pausePromptGate'
|
|
25
31
|
| 'persist'
|
|
32
|
+
| 'pi'
|
|
26
33
|
| 'queuePromptReviewFailure'
|
|
27
34
|
| 'queuePromptReviewResult'
|
|
28
35
|
| 'restoreBaselineTools'
|
|
@@ -205,13 +212,22 @@ async function finishPromptReview(
|
|
|
205
212
|
return;
|
|
206
213
|
}
|
|
207
214
|
try {
|
|
215
|
+
const stepId = this.run.currentStepId;
|
|
216
|
+
const gate = workflow.definition.steps[stepId]?.gate;
|
|
217
|
+
if (!gate) throw new Error(`gated step "${stepId}" no longer exists`);
|
|
208
218
|
this.run = resolveGate(
|
|
209
219
|
workflow,
|
|
210
220
|
this.run,
|
|
211
221
|
resolution,
|
|
212
222
|
this.dependencies.now(),
|
|
213
223
|
);
|
|
214
|
-
this.settleAfterTransition(workflow
|
|
224
|
+
this.settleAfterTransition(workflow, {
|
|
225
|
+
stepId,
|
|
226
|
+
outcome: resolution.approved
|
|
227
|
+
? gate.approvedOutcome
|
|
228
|
+
: gate.rejectedOutcome,
|
|
229
|
+
summary: this.run.lastSummary,
|
|
230
|
+
});
|
|
215
231
|
} catch (error) {
|
|
216
232
|
this.pausePromptGate(
|
|
217
233
|
active.requestId,
|
|
@@ -234,16 +250,38 @@ function pausePromptGate(
|
|
|
234
250
|
) {
|
|
235
251
|
return;
|
|
236
252
|
}
|
|
253
|
+
const wasAwaitingGate = this.run.status === 'awaiting-gate';
|
|
254
|
+
const didFail = wasAwaitingGate && isFailed;
|
|
255
|
+
const didPause = wasAwaitingGate && !isFailed;
|
|
237
256
|
if (this.run.status === 'awaiting-gate') {
|
|
238
257
|
this.run = isFailed
|
|
239
258
|
? failRun(this.run, reason, this.dependencies.now())
|
|
240
259
|
: pauseRun(this.run, reason, this.dependencies.now());
|
|
241
260
|
}
|
|
242
261
|
this.persist();
|
|
262
|
+
if (didFail) {
|
|
263
|
+
reportFailedStep(
|
|
264
|
+
this.pi,
|
|
265
|
+
this.catalog.workflows.get(this.run.workflowId),
|
|
266
|
+
this.run,
|
|
267
|
+
reason,
|
|
268
|
+
);
|
|
269
|
+
} else if (didPause) {
|
|
270
|
+
reportPausedStep(
|
|
271
|
+
this.pi,
|
|
272
|
+
this.catalog.workflows.get(this.run.workflowId),
|
|
273
|
+
this.run,
|
|
274
|
+
reason,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
243
277
|
this.restoreBaselineTools();
|
|
244
278
|
this.updateStatus();
|
|
245
279
|
this.latestContext?.ui.notify(
|
|
246
|
-
`Workflow paused at "${this.run.currentStepId}": ${
|
|
280
|
+
`Workflow paused at "${this.run.currentStepId}": ${
|
|
281
|
+
isFailed
|
|
282
|
+
? conciseStepFailureSummary(reason)
|
|
283
|
+
: conciseStepPauseSummary(reason)
|
|
284
|
+
}`,
|
|
247
285
|
'warning',
|
|
248
286
|
);
|
|
249
287
|
}
|
|
@@ -5,13 +5,20 @@ import {
|
|
|
5
5
|
reconcileRun,
|
|
6
6
|
resolveGate,
|
|
7
7
|
resumeRun,
|
|
8
|
+
setResumeInput,
|
|
8
9
|
storeGateResolution,
|
|
9
10
|
} from '../engine/transitions.ts';
|
|
11
|
+
import { MAX_RESUME_INPUT_CHARS } from '../engine/state.ts';
|
|
10
12
|
import {
|
|
11
13
|
captureResumeCheckpoint,
|
|
12
14
|
matchesResumeCheckpoint,
|
|
13
15
|
} from '../engine/resume.ts';
|
|
16
|
+
import { analyzeWorkflow } from '../workflow-doctor.ts';
|
|
14
17
|
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
18
|
+
import {
|
|
19
|
+
conciseStepFailureSummary,
|
|
20
|
+
reportFailedStep,
|
|
21
|
+
} from './step-reporting.ts';
|
|
15
22
|
|
|
16
23
|
type HarnessActionContext = Pick<
|
|
17
24
|
FullHarnessActionContext,
|
|
@@ -29,6 +36,7 @@ type HarnessActionContext = Pick<
|
|
|
29
36
|
| 'restoreBaselineTools'
|
|
30
37
|
| 'run'
|
|
31
38
|
| 'sessionEpoch'
|
|
39
|
+
| 'settleAfterTransition'
|
|
32
40
|
| 'updateStatus'
|
|
33
41
|
>;
|
|
34
42
|
|
|
@@ -36,13 +44,23 @@ export type ResumeAction = {
|
|
|
36
44
|
resumeNow: (
|
|
37
45
|
this: HarnessActionContext,
|
|
38
46
|
context: ExtensionCommandContext,
|
|
47
|
+
input?: string,
|
|
39
48
|
) => Promise<void>;
|
|
40
49
|
};
|
|
41
50
|
|
|
42
51
|
async function resumeNow(
|
|
43
52
|
this: HarnessActionContext,
|
|
44
53
|
context: ExtensionCommandContext,
|
|
54
|
+
input = '',
|
|
45
55
|
): Promise<void> {
|
|
56
|
+
const resumeInput = input.trim();
|
|
57
|
+
if (resumeInput.length > MAX_RESUME_INPUT_CHARS) {
|
|
58
|
+
context.ui.notify(
|
|
59
|
+
`Resume guidance exceeds ${MAX_RESUME_INPUT_CHARS} characters`,
|
|
60
|
+
'error',
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
46
64
|
if (!this.run || this.run.status !== 'paused') {
|
|
47
65
|
context.ui.notify('No paused workflow to resume', 'warning');
|
|
48
66
|
return;
|
|
@@ -92,6 +110,16 @@ async function resumeNow(
|
|
|
92
110
|
);
|
|
93
111
|
return;
|
|
94
112
|
}
|
|
113
|
+
const livenessErrors = analyzeWorkflow(workflow.definition).issues.filter(
|
|
114
|
+
(issue) => issue.level === 'error',
|
|
115
|
+
);
|
|
116
|
+
if (livenessErrors.length > 0) {
|
|
117
|
+
context.ui.notify(
|
|
118
|
+
`Cannot resume workflow; run /workflow-doctor ${workflow.definition.id}:\n${livenessErrors.map((issue) => issue.message).join('\n')}`,
|
|
119
|
+
'error',
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
95
123
|
|
|
96
124
|
let resumed = reconciled.run;
|
|
97
125
|
if (
|
|
@@ -144,6 +172,16 @@ async function resumeNow(
|
|
|
144
172
|
);
|
|
145
173
|
return;
|
|
146
174
|
}
|
|
175
|
+
const latestLivenessErrors = analyzeWorkflow(
|
|
176
|
+
workflow.definition,
|
|
177
|
+
).issues.filter((issue) => issue.level === 'error');
|
|
178
|
+
if (latestLivenessErrors.length > 0) {
|
|
179
|
+
context.ui.notify(
|
|
180
|
+
`Cannot resume workflow; run /workflow-doctor ${workflow.definition.id}:\n${latestLivenessErrors.map((issue) => issue.message).join('\n')}`,
|
|
181
|
+
'error',
|
|
182
|
+
);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
147
185
|
resumed = latest.run;
|
|
148
186
|
|
|
149
187
|
if (
|
|
@@ -187,14 +225,29 @@ async function resumeNow(
|
|
|
187
225
|
}
|
|
188
226
|
|
|
189
227
|
const storedResolution = resumed.pendingGate?.resolution;
|
|
228
|
+
let settledGate:
|
|
229
|
+
| {
|
|
230
|
+
readonly stepId: string;
|
|
231
|
+
readonly outcome: string;
|
|
232
|
+
}
|
|
233
|
+
| undefined;
|
|
190
234
|
if (storedResolution) {
|
|
191
235
|
try {
|
|
236
|
+
const stepId = resumed.pendingGate?.stepId ?? resumed.currentStepId;
|
|
237
|
+
const gate = workflow.definition.steps[stepId]?.gate;
|
|
238
|
+
if (!gate) throw new Error(`gated step "${stepId}" no longer exists`);
|
|
192
239
|
resumed = resolveGate(
|
|
193
240
|
workflow,
|
|
194
241
|
resumed,
|
|
195
242
|
storedResolution,
|
|
196
243
|
this.dependencies.now(),
|
|
197
244
|
);
|
|
245
|
+
settledGate = {
|
|
246
|
+
stepId,
|
|
247
|
+
outcome: storedResolution.approved
|
|
248
|
+
? gate.approvedOutcome
|
|
249
|
+
: gate.rejectedOutcome,
|
|
250
|
+
};
|
|
198
251
|
} catch (error) {
|
|
199
252
|
context.ui.notify(
|
|
200
253
|
`Cannot apply stored gate result: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -205,8 +258,21 @@ async function resumeNow(
|
|
|
205
258
|
} else {
|
|
206
259
|
resumed = resumeRun(resumed, this.dependencies.now());
|
|
207
260
|
}
|
|
261
|
+
resumed = setResumeInput(
|
|
262
|
+
resumed,
|
|
263
|
+
resumed.status === 'running' ? resumeInput : '',
|
|
264
|
+
this.dependencies.now(),
|
|
265
|
+
);
|
|
208
266
|
this.run = resumed;
|
|
209
267
|
|
|
268
|
+
if (settledGate) {
|
|
269
|
+
this.settleAfterTransition(workflow, {
|
|
270
|
+
...settledGate,
|
|
271
|
+
summary: this.run.lastSummary,
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
210
276
|
if (this.run.status === 'awaiting-gate') {
|
|
211
277
|
this.persist();
|
|
212
278
|
this.restoreBaselineTools();
|
|
@@ -238,10 +304,18 @@ async function resumeNow(
|
|
|
238
304
|
this.dependencies.now(),
|
|
239
305
|
);
|
|
240
306
|
this.persist();
|
|
307
|
+
reportFailedStep(
|
|
308
|
+
this.pi,
|
|
309
|
+
workflow,
|
|
310
|
+
this.run,
|
|
311
|
+
this.run.pauseReason ?? 'Step preflight failed',
|
|
312
|
+
);
|
|
241
313
|
this.restoreBaselineTools();
|
|
242
314
|
this.updateStatus();
|
|
243
315
|
context.ui.notify(
|
|
244
|
-
`Cannot resume workflow
|
|
316
|
+
`Cannot resume workflow: ${conciseStepFailureSummary(
|
|
317
|
+
preflightErrors.join('; '),
|
|
318
|
+
)}`,
|
|
245
319
|
'error',
|
|
246
320
|
);
|
|
247
321
|
return;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { createRun } from '../engine/state.ts';
|
|
3
|
+
import { analyzeWorkflow, formatWorkflowDoctor } from '../workflow-doctor.ts';
|
|
3
4
|
import { formatWorkflowList } from '../workflow-list.ts';
|
|
4
5
|
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
6
|
+
import { formatCatalogDiagnostics } from './catalog.ts';
|
|
5
7
|
import type { WorkflowStartContext } from './types.ts';
|
|
6
8
|
|
|
7
9
|
type HarnessActionContext = Pick<
|
|
@@ -13,7 +15,6 @@ type HarnessActionContext = Pick<
|
|
|
13
15
|
| 'isSessionActive'
|
|
14
16
|
| 'isolateMainSessionTools'
|
|
15
17
|
| 'launchCurrentStep'
|
|
16
|
-
| 'openWorkflowStatus'
|
|
17
18
|
| 'persist'
|
|
18
19
|
| 'pi'
|
|
19
20
|
| 'preflight'
|
|
@@ -40,6 +41,11 @@ export type StartActions = {
|
|
|
40
41
|
this: HarnessActionContext,
|
|
41
42
|
context: ExtensionCommandContext,
|
|
42
43
|
) => Promise<void>;
|
|
44
|
+
doctorWorkflows: (
|
|
45
|
+
this: HarnessActionContext,
|
|
46
|
+
workflowId: string,
|
|
47
|
+
context: ExtensionCommandContext,
|
|
48
|
+
) => Promise<void>;
|
|
43
49
|
startNow: (
|
|
44
50
|
this: HarnessActionContext,
|
|
45
51
|
workflowId: string,
|
|
@@ -76,6 +82,48 @@ async function listWorkflows(
|
|
|
76
82
|
});
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
async function doctorWorkflows(
|
|
86
|
+
this: HarnessActionContext,
|
|
87
|
+
workflowId: string,
|
|
88
|
+
context: ExtensionCommandContext,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
const catalog = await this.dependencies.loadCatalog({
|
|
91
|
+
cwd: context.cwd,
|
|
92
|
+
projectTrusted: context.isProjectTrusted(),
|
|
93
|
+
});
|
|
94
|
+
if (catalog.diagnostics.some((diagnostic) => diagnostic.level === 'error')) {
|
|
95
|
+
context.ui.notify(
|
|
96
|
+
`Workflow configuration errors:\n${formatCatalogDiagnostics(catalog)}`,
|
|
97
|
+
'warning',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const selected = workflowId
|
|
101
|
+
? [catalog.workflows.get(workflowId)].filter(
|
|
102
|
+
(workflow) => workflow !== undefined,
|
|
103
|
+
)
|
|
104
|
+
: [...catalog.workflows.values()].sort((left, right) =>
|
|
105
|
+
left.definition.id.localeCompare(right.definition.id),
|
|
106
|
+
);
|
|
107
|
+
if (workflowId && selected.length === 0) {
|
|
108
|
+
context.ui.notify(`Workflow "${workflowId}" is not loaded`, 'error');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (selected.length === 0) {
|
|
112
|
+
context.ui.notify(
|
|
113
|
+
`No workflows loaded from ${catalog.userDirectory}`,
|
|
114
|
+
catalog.diagnostics.length > 0 ? 'warning' : 'info',
|
|
115
|
+
);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.pi.sendMessage({
|
|
119
|
+
customType: 'workflow-doctor',
|
|
120
|
+
content: formatWorkflowDoctor(
|
|
121
|
+
selected.map((workflow) => analyzeWorkflow(workflow.definition)),
|
|
122
|
+
),
|
|
123
|
+
display: true,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
79
127
|
async function startNow(
|
|
80
128
|
this: HarnessActionContext,
|
|
81
129
|
workflowId: string,
|
|
@@ -134,6 +182,16 @@ async function startNow(
|
|
|
134
182
|
context.ui.notify(`Workflow "${workflowId}" is not loaded`, 'error');
|
|
135
183
|
return;
|
|
136
184
|
}
|
|
185
|
+
const livenessErrors = analyzeWorkflow(workflow.definition).issues.filter(
|
|
186
|
+
(issue) => issue.level === 'error',
|
|
187
|
+
);
|
|
188
|
+
if (livenessErrors.length > 0) {
|
|
189
|
+
context.ui.notify(
|
|
190
|
+
`Cannot start workflow; run /workflow-doctor ${workflowId}:\n${livenessErrors.map((issue) => issue.message).join('\n')}`,
|
|
191
|
+
'error',
|
|
192
|
+
);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
137
195
|
const preflightErrors = this.preflight(workflow, workflow.definition.start);
|
|
138
196
|
if (preflightErrors.length > 0) {
|
|
139
197
|
context.ui.notify(
|
|
@@ -143,17 +201,33 @@ async function startNow(
|
|
|
143
201
|
return;
|
|
144
202
|
}
|
|
145
203
|
|
|
204
|
+
let canonicalStartCwd: string;
|
|
205
|
+
try {
|
|
206
|
+
canonicalStartCwd = this.dependencies.resolveWorkspaceDirectory({
|
|
207
|
+
candidateCwd: context.cwd,
|
|
208
|
+
startCwd: context.cwd,
|
|
209
|
+
allowedRoots: ['.'],
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
context.ui.notify(
|
|
213
|
+
`Cannot capture workflow working directory: ${
|
|
214
|
+
error instanceof Error ? error.message : String(error)
|
|
215
|
+
}`,
|
|
216
|
+
'error',
|
|
217
|
+
);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
146
220
|
this.run = createRun(
|
|
147
221
|
workflow,
|
|
148
222
|
input.trim(),
|
|
149
223
|
this.pi.getActiveTools(),
|
|
150
224
|
this.dependencies.createRequestId(),
|
|
151
225
|
this.dependencies.now(),
|
|
226
|
+
canonicalStartCwd,
|
|
152
227
|
);
|
|
153
228
|
this.persist();
|
|
154
229
|
this.isolateMainSessionTools();
|
|
155
230
|
this.updateStatus();
|
|
156
|
-
this.openWorkflowStatus(context);
|
|
157
231
|
this.launchCurrentStep(workflow);
|
|
158
232
|
}
|
|
159
233
|
|
|
@@ -179,5 +253,5 @@ async function reloadNow(
|
|
|
179
253
|
* Returns workflow listing, start, and reload actions for harness composition.
|
|
180
254
|
*/
|
|
181
255
|
export function createStartActions(): StartActions {
|
|
182
|
-
return { listWorkflows, startNow, reloadNow };
|
|
256
|
+
return { listWorkflows, doctorWorkflows, startNow, reloadNow };
|
|
183
257
|
}
|