@davidorex/pi-workflows 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -4
- package/README.md +12 -2
- package/agents/architecture-designer.agent.yaml +58 -0
- package/agents/architecture-inferrer.agent.yaml +60 -0
- package/agents/gap-identifier.agent.yaml +66 -0
- package/agents/handoff-writer.agent.yaml +64 -0
- package/agents/plan-creator.agent.yaml +63 -0
- package/agents/project-definer.agent.yaml +53 -0
- package/agents/project-inferrer.agent.yaml +63 -0
- package/agents/requirements-gatherer.agent.yaml +58 -0
- package/dist/agent-spec.d.ts +27 -0
- package/dist/agent-spec.d.ts.map +1 -0
- package/dist/agent-spec.js +112 -0
- package/dist/agent-spec.js.map +1 -0
- package/dist/checkpoint.d.ts +50 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +109 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/completion.d.ts +18 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +58 -0
- package/dist/completion.js.map +1 -0
- package/dist/dag.d.ts +56 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +178 -0
- package/dist/dag.js.map +1 -0
- package/dist/dispatch.d.ts +35 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/expression.d.ts +64 -0
- package/dist/expression.d.ts.map +1 -0
- package/dist/expression.js +294 -0
- package/dist/expression.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +42 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +724 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +59 -0
- package/dist/output.js.map +1 -0
- package/dist/state.d.ts +66 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +227 -0
- package/dist/state.js.map +1 -0
- package/dist/step-agent.d.ts +35 -0
- package/dist/step-agent.d.ts.map +1 -0
- package/dist/step-agent.js +171 -0
- package/dist/step-agent.js.map +1 -0
- package/dist/step-command.d.ts +21 -0
- package/dist/step-command.d.ts.map +1 -0
- package/dist/step-command.js +150 -0
- package/dist/step-command.js.map +1 -0
- package/dist/step-foreach.d.ts +33 -0
- package/dist/step-foreach.d.ts.map +1 -0
- package/dist/step-foreach.js +112 -0
- package/dist/step-foreach.js.map +1 -0
- package/dist/step-gate.d.ts +18 -0
- package/dist/step-gate.d.ts.map +1 -0
- package/dist/step-gate.js +137 -0
- package/dist/step-gate.js.map +1 -0
- package/dist/step-loop.d.ts +32 -0
- package/dist/step-loop.d.ts.map +1 -0
- package/dist/step-loop.js +278 -0
- package/dist/step-loop.js.map +1 -0
- package/dist/step-monitor.d.ts +56 -0
- package/dist/step-monitor.d.ts.map +1 -0
- package/dist/step-monitor.js +317 -0
- package/dist/step-monitor.js.map +1 -0
- package/dist/step-parallel.d.ts +43 -0
- package/dist/step-parallel.d.ts.map +1 -0
- package/dist/step-parallel.js +122 -0
- package/dist/step-parallel.js.map +1 -0
- package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
- package/dist/step-pause.d.ts.map +1 -0
- package/dist/step-pause.js +22 -0
- package/dist/step-pause.js.map +1 -0
- package/dist/step-shared.d.ts +63 -0
- package/dist/step-shared.d.ts.map +1 -0
- package/dist/step-shared.js +136 -0
- package/dist/step-shared.js.map +1 -0
- package/dist/step-transform.d.ts +11 -0
- package/dist/step-transform.d.ts.map +1 -0
- package/dist/step-transform.js +41 -0
- package/dist/step-transform.js.map +1 -0
- package/dist/template.d.ts +51 -0
- package/dist/template.d.ts.map +1 -0
- package/{src/template.ts → dist/template.js} +32 -38
- package/dist/template.js.map +1 -0
- package/dist/test-helpers.d.ts +17 -0
- package/dist/test-helpers.d.ts.map +1 -0
- package/dist/test-helpers.js +46 -0
- package/dist/test-helpers.js.map +1 -0
- package/dist/tui.d.ts +47 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +143 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +165 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-discovery.d.ts +23 -0
- package/dist/workflow-discovery.d.ts.map +1 -0
- package/dist/workflow-discovery.js +103 -0
- package/dist/workflow-discovery.js.map +1 -0
- package/dist/workflow-executor.d.ts +50 -0
- package/dist/workflow-executor.d.ts.map +1 -0
- package/dist/workflow-executor.js +738 -0
- package/dist/workflow-executor.js.map +1 -0
- package/dist/workflow-sdk.d.ts +44 -0
- package/dist/workflow-sdk.d.ts.map +1 -0
- package/dist/workflow-sdk.js +457 -0
- package/dist/workflow-sdk.js.map +1 -0
- package/dist/workflow-spec.d.ts +33 -0
- package/dist/workflow-spec.d.ts.map +1 -0
- package/dist/workflow-spec.js +476 -0
- package/dist/workflow-spec.js.map +1 -0
- package/dist/workflows-dir.d.ts +3 -0
- package/dist/workflows-dir.d.ts.map +1 -0
- package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
- package/dist/workflows-dir.js.map +1 -0
- package/package.json +53 -38
- package/schemas/decomposition-specs.schema.json +48 -48
- package/schemas/execution-results.schema.json +48 -48
- package/schemas/investigation-findings.schema.json +43 -43
- package/schemas/pattern-analysis.schema.json +41 -41
- package/schemas/phase.schema.json +48 -0
- package/schemas/plan-breakdown.schema.json +20 -20
- package/schemas/quality-analysis.schema.json +33 -33
- package/schemas/research-findings.schema.json +42 -42
- package/schemas/structure-analysis.schema.json +40 -40
- package/schemas/synthesis.schema.json +31 -31
- package/schemas/verifier-output.schema.json +94 -94
- package/skill-narrative.md +107 -0
- package/skills/pi-workflows/SKILL.md +185 -0
- package/skills/pi-workflows/references/bundled-resources.md +87 -0
- package/templates/architecture-designer/task.md +117 -0
- package/templates/architecture-inferrer/task.md +61 -0
- package/templates/gap-identifier/task.md +103 -0
- package/templates/handoff-writer/task.md +91 -0
- package/templates/plan-creator/task.md +143 -0
- package/templates/project-definer/task.md +67 -0
- package/templates/project-inferrer/task.md +56 -0
- package/templates/requirements-gatherer/task.md +90 -0
- package/workflows/analyze-existing-project.workflow.yaml +94 -0
- package/workflows/create-handoff.workflow.yaml +66 -0
- package/workflows/create-phase.workflow.yaml +1 -1
- package/workflows/do-gap.workflow.yaml +4 -4
- package/workflows/fix-audit.workflow.yaml +1 -1
- package/workflows/gap-to-phase.workflow.yaml +1 -1
- package/workflows/init-new-project.workflow.yaml +81 -0
- package/workflows/plan-from-requirements.workflow.yaml +59 -0
- package/workflows/self-implement.workflow.yaml +3 -3
- package/workflows/typed-analysis.workflow.yaml +4 -4
- package/src/agent-spec.test.ts +0 -289
- package/src/agent-spec.ts +0 -122
- package/src/block-validation.test.ts +0 -350
- package/src/checkpoint.test.ts +0 -237
- package/src/checkpoint.ts +0 -140
- package/src/completion.test.ts +0 -143
- package/src/completion.ts +0 -68
- package/src/dag.test.ts +0 -350
- package/src/dag.ts +0 -219
- package/src/dispatch.test.ts +0 -475
- package/src/dispatch.ts +0 -353
- package/src/expression.test.ts +0 -353
- package/src/expression.ts +0 -332
- package/src/format.test.ts +0 -80
- package/src/format.ts +0 -41
- package/src/graduated-failure.test.ts +0 -556
- package/src/index.test.ts +0 -114
- package/src/index.ts +0 -516
- package/src/loop.test.ts +0 -551
- package/src/output.test.ts +0 -213
- package/src/output.ts +0 -70
- package/src/parallel-integration.test.ts +0 -175
- package/src/resume.test.ts +0 -192
- package/src/state.test.ts +0 -192
- package/src/state.ts +0 -254
- package/src/step-agent.test.ts +0 -327
- package/src/step-agent.ts +0 -178
- package/src/step-command.test.ts +0 -330
- package/src/step-command.ts +0 -132
- package/src/step-foreach.test.ts +0 -647
- package/src/step-foreach.ts +0 -148
- package/src/step-gate.test.ts +0 -185
- package/src/step-gate.ts +0 -116
- package/src/step-loop.test.ts +0 -626
- package/src/step-loop.ts +0 -323
- package/src/step-parallel.test.ts +0 -475
- package/src/step-parallel.ts +0 -168
- package/src/step-pause.test.ts +0 -30
- package/src/step-shared.test.ts +0 -355
- package/src/step-shared.ts +0 -157
- package/src/step-transform.test.ts +0 -155
- package/src/step-transform.ts +0 -47
- package/src/template-integration.test.ts +0 -72
- package/src/template.test.ts +0 -162
- package/src/test-helpers.ts +0 -51
- package/src/tui.test.ts +0 -355
- package/src/tui.ts +0 -182
- package/src/types.ts +0 -195
- package/src/verifier-schema.test.ts +0 -226
- package/src/workflow-discovery.test.ts +0 -105
- package/src/workflow-discovery.ts +0 -114
- package/src/workflow-executor.test.ts +0 -1532
- package/src/workflow-executor.ts +0 -810
- package/src/workflow-sdk.test.ts +0 -238
- package/src/workflow-sdk.ts +0 -262
- package/src/workflow-spec.test.ts +0 -576
- package/src/workflow-spec.ts +0 -471
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow executor — orchestrates step execution with DAG-based layering,
|
|
3
|
+
* parallel dispatch, timeout enforcement, state persistence, and TUI updates.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { readBlock, writeBlock } from "@davidorex/pi-project/src/block-api.js";
|
|
8
|
+
import { rollbackBlockFiles, snapshotBlockFiles, validateChangedBlocks, } from "@davidorex/pi-project/src/block-validation.js";
|
|
9
|
+
import { PROJECT_DIR } from "@davidorex/pi-project/src/project-dir.js";
|
|
10
|
+
import { validate, validateFromFile } from "@davidorex/pi-project/src/schema-validator.js";
|
|
11
|
+
import { truncateTail } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { resolveCompletion } from "./completion.js";
|
|
13
|
+
import { buildPlanFromDeps, extractDependencies } from "./dag.js";
|
|
14
|
+
import { dispatch } from "./dispatch.js";
|
|
15
|
+
import { evaluateCondition, resolveExpressions } from "./expression.js";
|
|
16
|
+
import { buildResult, formatResult, generateRunId, getWorkflowDir, initRunDir, writeMetrics, writeState, } from "./state.js";
|
|
17
|
+
import { executeAgentStep } from "./step-agent.js";
|
|
18
|
+
import { executeCommand } from "./step-command.js";
|
|
19
|
+
import { executeForEach } from "./step-foreach.js";
|
|
20
|
+
import { executeGate } from "./step-gate.js";
|
|
21
|
+
import { executeLoop } from "./step-loop.js";
|
|
22
|
+
import { executeMonitor } from "./step-monitor.js";
|
|
23
|
+
import { executeParallelLayer, executeParallelStep } from "./step-parallel.js";
|
|
24
|
+
import { executePause } from "./step-pause.js";
|
|
25
|
+
import { persistStep, resolveSchemaPath, SIGKILL_GRACE_MS, WIDGET_ID, zeroUsage } from "./step-shared.js";
|
|
26
|
+
import { executeTransform } from "./step-transform.js";
|
|
27
|
+
import { createTemplateEnv } from "./template.js";
|
|
28
|
+
import { createProgressWidget } from "./tui.js";
|
|
29
|
+
// Re-export SIGKILL_GRACE_MS so tests that grep this file still find it
|
|
30
|
+
export { SIGKILL_GRACE_MS };
|
|
31
|
+
/** Module-level flag set by the Ctrl+H keybinding handler. */
|
|
32
|
+
let pauseRequested = false;
|
|
33
|
+
/** Set by the extension keybinding to request a pause after the current step. */
|
|
34
|
+
export function requestPause() {
|
|
35
|
+
pauseRequested = true;
|
|
36
|
+
}
|
|
37
|
+
/** Clear the pause flag (called at the start of each workflow execution). */
|
|
38
|
+
function clearPauseRequest() {
|
|
39
|
+
pauseRequested = false;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build a conservative execution plan that preserves declaration-order
|
|
43
|
+
* sequencing for steps without explicit `${{ steps.X }}` dependencies.
|
|
44
|
+
*
|
|
45
|
+
* Steps with no explicit dependencies implicitly depend on the previous
|
|
46
|
+
* step in declaration order. This ensures backward compatibility with
|
|
47
|
+
* workflows written for sequential execution while still allowing
|
|
48
|
+
* DAG-inferred parallelism for steps that DO have explicit dependencies
|
|
49
|
+
* (e.g., diamond patterns where two steps both depend on an earlier step).
|
|
50
|
+
*/
|
|
51
|
+
function buildConservativePlan(spec) {
|
|
52
|
+
const deps = extractDependencies(spec);
|
|
53
|
+
const allSteps = Object.keys(spec.steps);
|
|
54
|
+
// Add implicit declaration-order dependency for steps with no explicit deps.
|
|
55
|
+
// If a step has no ${{ steps.X }} references at all, it depends on the
|
|
56
|
+
// immediately preceding step in YAML order.
|
|
57
|
+
for (let i = 1; i < allSteps.length; i++) {
|
|
58
|
+
const stepDeps = deps.get(allSteps[i]);
|
|
59
|
+
if (stepDeps.size === 0) {
|
|
60
|
+
stepDeps.add(allSteps[i - 1]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return buildPlanFromDeps(allSteps, deps);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Determine if a step type supports retry.
|
|
67
|
+
* Agent, forEach, and loop steps are retryable.
|
|
68
|
+
* Command, gate, and transform are deterministic and not retryable.
|
|
69
|
+
*/
|
|
70
|
+
function isRetryableStepType(stepSpec) {
|
|
71
|
+
if (stepSpec.command || stepSpec.gate || stepSpec.transform || stepSpec.monitor)
|
|
72
|
+
return false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Execute a single step (agent, gate, transform, loop, or parallel).
|
|
77
|
+
*
|
|
78
|
+
* This is the central step type dispatcher. It delegates to the appropriate
|
|
79
|
+
* step executor module based on the step spec type.
|
|
80
|
+
* Returns true if the workflow should continue, false if it should stop
|
|
81
|
+
* (due to failure, break, or cancellation).
|
|
82
|
+
*
|
|
83
|
+
* Supports per-step retry: if step.retry is configured and the step type
|
|
84
|
+
* is retryable, failures (including block validation) trigger rollback
|
|
85
|
+
* and re-execution with error context injected into the prompt.
|
|
86
|
+
*/
|
|
87
|
+
async function executeSingleStep(stepName, stepSpec, state, options) {
|
|
88
|
+
const { ctx, signal, loadAgent, runDir, spec, widgetState } = options;
|
|
89
|
+
// Check cancellation
|
|
90
|
+
if (signal?.aborted) {
|
|
91
|
+
state.steps[stepName] = {
|
|
92
|
+
step: stepName,
|
|
93
|
+
agent: stepSpec.agent ?? "",
|
|
94
|
+
status: "failed",
|
|
95
|
+
usage: zeroUsage(),
|
|
96
|
+
durationMs: 0,
|
|
97
|
+
error: "Workflow cancelled",
|
|
98
|
+
};
|
|
99
|
+
state.status = "failed";
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
// Build expression scope
|
|
103
|
+
const scope = { input: state.input, steps: state.steps };
|
|
104
|
+
// Expose forEach bindings (as name + forEach metadata) if present on the state
|
|
105
|
+
const stateAny = state;
|
|
106
|
+
if (stateAny.forEach !== undefined) {
|
|
107
|
+
scope.forEach = stateAny.forEach;
|
|
108
|
+
}
|
|
109
|
+
// Expose any custom bindings (e.g. from forEach as)
|
|
110
|
+
for (const key of Object.keys(stateAny)) {
|
|
111
|
+
if (key !== "input" &&
|
|
112
|
+
key !== "steps" &&
|
|
113
|
+
key !== "status" &&
|
|
114
|
+
key !== "loop" &&
|
|
115
|
+
key !== "workflowName" &&
|
|
116
|
+
key !== "specVersion" &&
|
|
117
|
+
key !== "startedAt" &&
|
|
118
|
+
key !== "updatedAt" &&
|
|
119
|
+
key !== "forEach") {
|
|
120
|
+
scope[key] = stateAny[key];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Evaluate `when` conditional
|
|
124
|
+
if (stepSpec.when) {
|
|
125
|
+
const conditionExpr = stepSpec.when.replace(/^\$\{\{\s*/, "").replace(/\s*\}\}$/, "");
|
|
126
|
+
const shouldRun = evaluateCondition(conditionExpr, scope);
|
|
127
|
+
if (!shouldRun) {
|
|
128
|
+
persistStep(state, stepName, {
|
|
129
|
+
step: stepName,
|
|
130
|
+
agent: stepSpec.agent ?? "skipped",
|
|
131
|
+
status: "skipped",
|
|
132
|
+
usage: zeroUsage(),
|
|
133
|
+
durationMs: 0,
|
|
134
|
+
}, runDir, widgetState, ctx);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Determine retry config
|
|
139
|
+
const retryConfig = stepSpec.retry;
|
|
140
|
+
const maxAttempts = retryConfig?.maxAttempts ?? 1;
|
|
141
|
+
const canRetry = isRetryableStepType(stepSpec) && maxAttempts > 1;
|
|
142
|
+
const totalAttempts = canRetry ? maxAttempts : 1;
|
|
143
|
+
const priorErrors = [];
|
|
144
|
+
for (let attempt = 1; attempt <= totalAttempts; attempt++) {
|
|
145
|
+
// Check cancellation between retry attempts
|
|
146
|
+
if (attempt > 1 && signal?.aborted) {
|
|
147
|
+
state.steps[stepName] = {
|
|
148
|
+
step: stepName,
|
|
149
|
+
agent: stepSpec.agent ?? "",
|
|
150
|
+
status: "failed",
|
|
151
|
+
usage: zeroUsage(),
|
|
152
|
+
durationMs: 0,
|
|
153
|
+
error: "Workflow cancelled",
|
|
154
|
+
};
|
|
155
|
+
state.status = "failed";
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
// Snapshot project block files before step execution for post-step validation
|
|
159
|
+
const blockSnapshot = snapshotBlockFiles(ctx.cwd);
|
|
160
|
+
// Update widget: mark this step as current
|
|
161
|
+
widgetState.currentStep = stepName;
|
|
162
|
+
if (ctx.hasUI) {
|
|
163
|
+
ctx.ui.setWidget(WIDGET_ID, createProgressWidget(widgetState));
|
|
164
|
+
}
|
|
165
|
+
// Build retry context for this attempt (if retrying)
|
|
166
|
+
const retryContext = attempt > 1
|
|
167
|
+
? {
|
|
168
|
+
attempt,
|
|
169
|
+
priorErrors: [...priorErrors],
|
|
170
|
+
steeringMessage: retryConfig?.steeringMessage,
|
|
171
|
+
}
|
|
172
|
+
: undefined;
|
|
173
|
+
// Execute the step, then validate any changed block files
|
|
174
|
+
// Reset failed status before re-attempt
|
|
175
|
+
if (attempt > 1) {
|
|
176
|
+
delete state.steps[stepName];
|
|
177
|
+
state.status = "running";
|
|
178
|
+
}
|
|
179
|
+
const continueWorkflow = await executeStepByType(stepName, stepSpec, state, scope, options, retryContext);
|
|
180
|
+
// Post-step block validation: if the step succeeded, validate changed project block files
|
|
181
|
+
let blockValidationFailed = false;
|
|
182
|
+
if (continueWorkflow && state.steps[stepName]?.status === "completed") {
|
|
183
|
+
try {
|
|
184
|
+
validateChangedBlocks(ctx.cwd, blockSnapshot);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
188
|
+
blockValidationFailed = true;
|
|
189
|
+
priorErrors.push(msg);
|
|
190
|
+
// Always rollback — invalid data must not persist
|
|
191
|
+
rollbackBlockFiles(ctx.cwd, blockSnapshot);
|
|
192
|
+
if (attempt < totalAttempts) {
|
|
193
|
+
// Retry
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// Last attempt — mark failed
|
|
197
|
+
state.steps[stepName].status = "failed";
|
|
198
|
+
state.steps[stepName].error = msg;
|
|
199
|
+
state.steps[stepName].attempt = attempt;
|
|
200
|
+
state.steps[stepName].totalAttempts = attempt;
|
|
201
|
+
state.steps[stepName].priorErrors = [...priorErrors];
|
|
202
|
+
state.status = "failed";
|
|
203
|
+
persistStep(state, stepName, state.steps[stepName], runDir, widgetState, ctx);
|
|
204
|
+
// Check onExhausted
|
|
205
|
+
if (retryConfig?.onExhausted === "skip") {
|
|
206
|
+
state.steps[stepName].status = "skipped";
|
|
207
|
+
state.steps[stepName].warnings = [
|
|
208
|
+
...(state.steps[stepName].warnings ?? []),
|
|
209
|
+
`Step skipped after ${attempt} failed attempts (onExhausted: skip)`,
|
|
210
|
+
];
|
|
211
|
+
state.status = "running";
|
|
212
|
+
persistStep(state, stepName, state.steps[stepName], runDir, widgetState, ctx);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!continueWorkflow || state.steps[stepName]?.status === "failed") {
|
|
219
|
+
const errorMsg = state.steps[stepName]?.error ?? "Step failed";
|
|
220
|
+
priorErrors.push(errorMsg);
|
|
221
|
+
if (attempt < totalAttempts && canRetry) {
|
|
222
|
+
// Rollback block files and retry
|
|
223
|
+
rollbackBlockFiles(ctx.cwd, blockSnapshot);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
// Last attempt or not retryable — annotate and return
|
|
227
|
+
if (state.steps[stepName]) {
|
|
228
|
+
state.steps[stepName].attempt = attempt;
|
|
229
|
+
state.steps[stepName].totalAttempts = attempt;
|
|
230
|
+
state.steps[stepName].priorErrors = attempt > 1 ? [...priorErrors] : undefined;
|
|
231
|
+
persistStep(state, stepName, state.steps[stepName], runDir, widgetState, ctx);
|
|
232
|
+
}
|
|
233
|
+
// Check onExhausted
|
|
234
|
+
if (canRetry && retryConfig?.onExhausted === "skip") {
|
|
235
|
+
if (state.steps[stepName]) {
|
|
236
|
+
state.steps[stepName].status = "skipped";
|
|
237
|
+
state.steps[stepName].warnings = [
|
|
238
|
+
...(state.steps[stepName].warnings ?? []),
|
|
239
|
+
`Step skipped after ${attempt} failed attempts (onExhausted: skip)`,
|
|
240
|
+
];
|
|
241
|
+
state.status = "running";
|
|
242
|
+
persistStep(state, stepName, state.steps[stepName], runDir, widgetState, ctx);
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
// Success — annotate with attempt tracking
|
|
249
|
+
if (state.steps[stepName]) {
|
|
250
|
+
state.steps[stepName].attempt = attempt;
|
|
251
|
+
state.steps[stepName].totalAttempts = attempt;
|
|
252
|
+
state.steps[stepName].priorErrors = attempt > 1 ? [...priorErrors] : undefined;
|
|
253
|
+
}
|
|
254
|
+
// Parse step output into a summary for TUI display
|
|
255
|
+
const stepOutput = state.steps[stepName]?.output;
|
|
256
|
+
if (stepOutput && typeof stepOutput === "object") {
|
|
257
|
+
const out = stepOutput;
|
|
258
|
+
const summary = {};
|
|
259
|
+
if (Array.isArray(out.tasks)) {
|
|
260
|
+
summary.tasks = out.tasks
|
|
261
|
+
.filter((t) => t && typeof t === "object")
|
|
262
|
+
.map((t) => ({
|
|
263
|
+
name: String(t.name ?? "?"),
|
|
264
|
+
status: String(t.status ?? "?"),
|
|
265
|
+
files: Array.isArray(t.files_modified) ? t.files_modified.map(String) : undefined,
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
if (typeof out.test_count === "number") {
|
|
269
|
+
summary.testCount = out.test_count;
|
|
270
|
+
}
|
|
271
|
+
if (typeof out.notes === "string") {
|
|
272
|
+
summary.note = out.notes;
|
|
273
|
+
}
|
|
274
|
+
if (summary.tasks || summary.testCount || summary.note) {
|
|
275
|
+
widgetState.outputSummaries.set(stepName, summary);
|
|
276
|
+
if (ctx.hasUI) {
|
|
277
|
+
ctx.ui.setWidget(WIDGET_ID, createProgressWidget(widgetState));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return continueWorkflow;
|
|
282
|
+
}
|
|
283
|
+
// Should not reach here, but safety net
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Execute a step based on its type (agent, gate, transform, etc.).
|
|
288
|
+
* Factored out of executeSingleStep to allow post-step validation.
|
|
289
|
+
*/
|
|
290
|
+
async function executeStepByType(stepName, stepSpec, state, scope, options, retryContext) {
|
|
291
|
+
const { ctx, signal, loadAgent, runDir, spec, widgetState } = options;
|
|
292
|
+
// ── ForEach step (wraps any step type) ──
|
|
293
|
+
if (stepSpec.forEach) {
|
|
294
|
+
const asName = stepSpec.as ?? "item";
|
|
295
|
+
const forEachResult = await executeForEach(stepName, stepSpec, state, stepSpec.forEach, asName, executeSingleStep, options);
|
|
296
|
+
persistStep(state, stepName, forEachResult, runDir, widgetState, ctx);
|
|
297
|
+
if (forEachResult.status === "failed") {
|
|
298
|
+
state.status = "failed";
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
// ── Gate step ──
|
|
304
|
+
if (stepSpec.gate) {
|
|
305
|
+
const resolvedCheck = String(resolveExpressions(stepSpec.gate.check, scope));
|
|
306
|
+
const resolvedGate = { ...stepSpec.gate, check: resolvedCheck };
|
|
307
|
+
const resolvedGateOutputPath = stepSpec.output?.path
|
|
308
|
+
? String(resolveExpressions(stepSpec.output.path, scope))
|
|
309
|
+
: undefined;
|
|
310
|
+
const gateResult = await executeGate(resolvedGate, stepName, {
|
|
311
|
+
cwd: ctx.cwd,
|
|
312
|
+
signal,
|
|
313
|
+
timeoutMs: stepSpec.timeout ? stepSpec.timeout.seconds * 1000 : undefined,
|
|
314
|
+
runDir,
|
|
315
|
+
outputPath: resolvedGateOutputPath,
|
|
316
|
+
});
|
|
317
|
+
const gateOutput = gateResult.output;
|
|
318
|
+
if (gateOutput.passed) {
|
|
319
|
+
const onPass = stepSpec.gate.onPass ?? "continue";
|
|
320
|
+
persistStep(state, stepName, gateResult, runDir, widgetState, ctx);
|
|
321
|
+
return onPass !== "break";
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const onFail = stepSpec.gate.onFail ?? "fail";
|
|
325
|
+
if (onFail === "fail") {
|
|
326
|
+
gateResult.status = "failed";
|
|
327
|
+
gateResult.error = `Gate check failed (exit ${gateOutput.exitCode}): ${gateOutput.output}`;
|
|
328
|
+
persistStep(state, stepName, gateResult, runDir, widgetState, ctx);
|
|
329
|
+
state.status = "failed";
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
else if (onFail === "continue") {
|
|
333
|
+
persistStep(state, stepName, gateResult, runDir, widgetState, ctx);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
else if (onFail === "break") {
|
|
337
|
+
persistStep(state, stepName, gateResult, runDir, widgetState, ctx);
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// ── Command step ──
|
|
343
|
+
if (stepSpec.command) {
|
|
344
|
+
const resolvedCommand = String(resolveExpressions(stepSpec.command, scope));
|
|
345
|
+
const resolvedCommandOutputPath = stepSpec.output?.path
|
|
346
|
+
? String(resolveExpressions(stepSpec.output.path, scope))
|
|
347
|
+
: undefined;
|
|
348
|
+
const commandResult = await executeCommand(resolvedCommand, stepName, {
|
|
349
|
+
cwd: ctx.cwd,
|
|
350
|
+
signal,
|
|
351
|
+
timeoutMs: stepSpec.timeout ? stepSpec.timeout.seconds * 1000 : undefined,
|
|
352
|
+
runDir,
|
|
353
|
+
outputPath: resolvedCommandOutputPath,
|
|
354
|
+
}, stepSpec.output?.format);
|
|
355
|
+
persistStep(state, stepName, commandResult, runDir, widgetState, ctx);
|
|
356
|
+
if (commandResult.status === "failed") {
|
|
357
|
+
state.status = "failed";
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
// ── Monitor step ──
|
|
363
|
+
if (stepSpec.monitor) {
|
|
364
|
+
const resolvedInput = stepSpec.input ? resolveExpressions(stepSpec.input, scope) : {};
|
|
365
|
+
const resolvedMonitorOutputPath = stepSpec.output?.path
|
|
366
|
+
? String(resolveExpressions(stepSpec.output.path, scope))
|
|
367
|
+
: undefined;
|
|
368
|
+
const monitorResult = await executeMonitor(stepSpec.monitor, stepName, resolvedInput, {
|
|
369
|
+
cwd: ctx.cwd,
|
|
370
|
+
ctx,
|
|
371
|
+
signal,
|
|
372
|
+
runDir,
|
|
373
|
+
outputPath: resolvedMonitorOutputPath,
|
|
374
|
+
});
|
|
375
|
+
persistStep(state, stepName, monitorResult, runDir, widgetState, ctx);
|
|
376
|
+
if (monitorResult.status === "failed") {
|
|
377
|
+
state.status = "failed";
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
// ── Transform step ──
|
|
383
|
+
if (stepSpec.transform) {
|
|
384
|
+
const resolvedTransformOutputPath = stepSpec.output?.path
|
|
385
|
+
? String(resolveExpressions(stepSpec.output.path, scope))
|
|
386
|
+
: undefined;
|
|
387
|
+
const transformResult = executeTransform(stepSpec.transform, stepName, scope, runDir, resolvedTransformOutputPath);
|
|
388
|
+
persistStep(state, stepName, transformResult, runDir, widgetState, ctx);
|
|
389
|
+
if (transformResult.status === "failed") {
|
|
390
|
+
state.status = "failed";
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
// ── Loop step ──
|
|
396
|
+
if (stepSpec.loop) {
|
|
397
|
+
const resolvedLoopOutputPath = stepSpec.output?.path
|
|
398
|
+
? String(resolveExpressions(stepSpec.output.path, scope))
|
|
399
|
+
: undefined;
|
|
400
|
+
const loopResult = await executeLoop(stepSpec.loop, stepName, state, {
|
|
401
|
+
ctx,
|
|
402
|
+
pi: options.pi,
|
|
403
|
+
signal,
|
|
404
|
+
loadAgent,
|
|
405
|
+
runDir,
|
|
406
|
+
spec,
|
|
407
|
+
dispatchAgent: (s, a, p, o) => (options.dispatchFn ?? dispatch)(s, a, p, { ...o, modelConfig: options.modelConfig }),
|
|
408
|
+
templateEnv: options.templateEnv,
|
|
409
|
+
outputPath: resolvedLoopOutputPath,
|
|
410
|
+
});
|
|
411
|
+
persistStep(state, stepName, loopResult, runDir, widgetState, ctx);
|
|
412
|
+
if (loopResult.status === "failed") {
|
|
413
|
+
state.status = "failed";
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
// ── Parallel step ──
|
|
419
|
+
if (stepSpec.parallel) {
|
|
420
|
+
const parallelResult = await executeParallelStep(stepSpec.parallel, stepName, state, executeSingleStep, options);
|
|
421
|
+
persistStep(state, stepName, parallelResult, runDir, widgetState, ctx);
|
|
422
|
+
if (parallelResult.status === "failed") {
|
|
423
|
+
state.status = "failed";
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
// ── Pause step ──
|
|
429
|
+
if (stepSpec.pause !== undefined) {
|
|
430
|
+
const message = typeof stepSpec.pause === "string" ? stepSpec.pause : undefined;
|
|
431
|
+
const pauseResult = executePause(stepName, message);
|
|
432
|
+
persistStep(state, stepName, pauseResult, runDir, widgetState, ctx);
|
|
433
|
+
state.status = "paused";
|
|
434
|
+
if (ctx.hasUI) {
|
|
435
|
+
ctx.ui.notify(message || "Workflow paused. Use /workflow resume or Ctrl+J to continue.", "info");
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
// ── Agent step (default) ──
|
|
440
|
+
const agentResult = await executeAgentStep(stepName, stepSpec, state, {
|
|
441
|
+
ctx,
|
|
442
|
+
signal,
|
|
443
|
+
loadAgent,
|
|
444
|
+
runDir,
|
|
445
|
+
specFilePath: spec.filePath,
|
|
446
|
+
widgetState,
|
|
447
|
+
templateEnv: options.templateEnv,
|
|
448
|
+
dispatchFn: options.dispatchFn,
|
|
449
|
+
modelConfig: options.modelConfig,
|
|
450
|
+
retryContext,
|
|
451
|
+
onStepActivity: (activity) => {
|
|
452
|
+
if (!widgetState.activities)
|
|
453
|
+
widgetState.activities = new Map();
|
|
454
|
+
const existing = widgetState.activities.get(stepName) || [];
|
|
455
|
+
if (existing.length >= 5)
|
|
456
|
+
existing.shift();
|
|
457
|
+
existing.push(activity);
|
|
458
|
+
widgetState.activities.set(stepName, existing);
|
|
459
|
+
if (ctx.hasUI) {
|
|
460
|
+
ctx.ui.setWidget(WIDGET_ID, createProgressWidget(widgetState));
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
persistStep(state, stepName, agentResult, runDir, widgetState, ctx);
|
|
465
|
+
if (agentResult.status === "failed") {
|
|
466
|
+
state.status = "failed";
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Execute a workflow from a parsed spec and validated input.
|
|
473
|
+
*
|
|
474
|
+
* Runs steps sequentially (phase 1), resolving ${{ }} expressions,
|
|
475
|
+
* dispatching subprocesses, validating outputs, persisting state,
|
|
476
|
+
* updating TUI, and injecting the result into the conversation.
|
|
477
|
+
*
|
|
478
|
+
* Supports step types: agent (default), gate, transform, loop.
|
|
479
|
+
* Supports `when` conditionals for skipping steps.
|
|
480
|
+
*
|
|
481
|
+
* Returns the WorkflowResult (also injected into conversation via sendMessage).
|
|
482
|
+
*/
|
|
483
|
+
export async function executeWorkflow(spec, input, options) {
|
|
484
|
+
const { ctx, pi, signal, loadAgent } = options;
|
|
485
|
+
clearPauseRequest();
|
|
486
|
+
// 1. Validate input against workflow input schema (if defined)
|
|
487
|
+
// Skip on resume — input was validated on first run.
|
|
488
|
+
if (!options.resume && spec.input) {
|
|
489
|
+
validate(spec.input, input, `workflow input for '${spec.name}'`);
|
|
490
|
+
}
|
|
491
|
+
// 2. Initialize or resume run
|
|
492
|
+
let runId;
|
|
493
|
+
let runDir;
|
|
494
|
+
let state;
|
|
495
|
+
if (options.resume) {
|
|
496
|
+
runId = options.resume.runId;
|
|
497
|
+
runDir = options.resume.runDir;
|
|
498
|
+
state = options.resume.state;
|
|
499
|
+
// Reset failed steps so they re-run
|
|
500
|
+
for (const [name, result] of Object.entries(state.steps)) {
|
|
501
|
+
if (result.status === "failed") {
|
|
502
|
+
delete state.steps[name];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
state.status = "running";
|
|
506
|
+
input = state.input; // use original input
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
runId = generateRunId(spec.name);
|
|
510
|
+
runDir = initRunDir(ctx.cwd, spec.name, runId);
|
|
511
|
+
state = {
|
|
512
|
+
input,
|
|
513
|
+
steps: {},
|
|
514
|
+
status: "running",
|
|
515
|
+
workflowName: spec.name,
|
|
516
|
+
specVersion: spec.version,
|
|
517
|
+
startedAt: new Date().toISOString(),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
// 3. Show TUI progress widget
|
|
521
|
+
const widgetState = {
|
|
522
|
+
spec,
|
|
523
|
+
state,
|
|
524
|
+
startTime: Date.now(),
|
|
525
|
+
activities: new Map(),
|
|
526
|
+
outputSummaries: new Map(),
|
|
527
|
+
};
|
|
528
|
+
if (options.resume) {
|
|
529
|
+
widgetState.resumedSteps = Object.keys(state.steps).filter((s) => state.steps[s].status === "completed" || state.steps[s].status === "skipped").length;
|
|
530
|
+
}
|
|
531
|
+
if (ctx.hasUI) {
|
|
532
|
+
ctx.ui.setWidget(WIDGET_ID, createProgressWidget(widgetState));
|
|
533
|
+
}
|
|
534
|
+
// 4. Set working message
|
|
535
|
+
if (ctx.hasUI) {
|
|
536
|
+
ctx.ui.setWorkingMessage(`Running ${spec.name} workflow...`);
|
|
537
|
+
}
|
|
538
|
+
// 5. Load model config (project-level model assignments by role)
|
|
539
|
+
let modelConfig = options.modelConfig;
|
|
540
|
+
if (!modelConfig) {
|
|
541
|
+
try {
|
|
542
|
+
modelConfig = readBlock(ctx.cwd, "model-config");
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
/* no config file — models come from agent specs or step specs */
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// 6. Build execution plan and execute layers
|
|
549
|
+
const plan = buildConservativePlan(spec);
|
|
550
|
+
const templateEnv = createTemplateEnv(ctx.cwd);
|
|
551
|
+
const stepOpts = {
|
|
552
|
+
ctx,
|
|
553
|
+
pi,
|
|
554
|
+
signal,
|
|
555
|
+
loadAgent,
|
|
556
|
+
runDir,
|
|
557
|
+
spec,
|
|
558
|
+
widgetState,
|
|
559
|
+
templateEnv,
|
|
560
|
+
dispatchFn: options.dispatchFn,
|
|
561
|
+
modelConfig,
|
|
562
|
+
};
|
|
563
|
+
for (const layer of plan) {
|
|
564
|
+
if (signal?.aborted) {
|
|
565
|
+
// Mark first unprocessed step as cancelled
|
|
566
|
+
for (const sn of layer.steps) {
|
|
567
|
+
if (!state.steps[sn]) {
|
|
568
|
+
state.steps[sn] = {
|
|
569
|
+
step: sn,
|
|
570
|
+
agent: spec.steps[sn].agent ?? "",
|
|
571
|
+
status: "failed",
|
|
572
|
+
usage: zeroUsage(),
|
|
573
|
+
durationMs: 0,
|
|
574
|
+
error: "Workflow cancelled",
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
state.status = "failed";
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
// Filter to only pending steps in this layer (resume support)
|
|
582
|
+
const pendingSteps = layer.steps.filter((s) => !state.steps[s] || state.steps[s].status === "failed");
|
|
583
|
+
if (pendingSteps.length === 0)
|
|
584
|
+
continue; // entire layer already done
|
|
585
|
+
if (pendingSteps.length === 1) {
|
|
586
|
+
const stepName = pendingSteps[0];
|
|
587
|
+
const stepSpec = spec.steps[stepName];
|
|
588
|
+
const cont = await executeSingleStep(stepName, stepSpec, state, stepOpts);
|
|
589
|
+
if (!cont)
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
// Run only the pending steps concurrently
|
|
594
|
+
const pendingLayer = { steps: pendingSteps };
|
|
595
|
+
await executeParallelLayer(pendingLayer, spec, state, executeSingleStep, stepOpts);
|
|
596
|
+
if (state.status === "failed" || state.status === "paused")
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
// Check keybinding-initiated pause (between layers/steps)
|
|
600
|
+
if (pauseRequested) {
|
|
601
|
+
pauseRequested = false;
|
|
602
|
+
state.status = "paused";
|
|
603
|
+
try {
|
|
604
|
+
writeState(runDir, state);
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
608
|
+
if (ctx.hasUI) {
|
|
609
|
+
ctx.ui.notify(`Warning: state write failed after pause — resume may not work: ${msg}`, "error");
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (ctx.hasUI) {
|
|
613
|
+
ctx.ui.notify("Workflow paused. Use /workflow resume or Ctrl+J to continue.", "info");
|
|
614
|
+
}
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// 5. Process artifacts (before finalization — block write failures are fatal)
|
|
619
|
+
const writtenArtifacts = {};
|
|
620
|
+
if (spec.artifacts && state.status === "running") {
|
|
621
|
+
const workflowDir = getWorkflowDir(ctx.cwd, spec.name);
|
|
622
|
+
const artifactScope = {
|
|
623
|
+
input: state.input,
|
|
624
|
+
steps: state.steps,
|
|
625
|
+
runId,
|
|
626
|
+
runDir,
|
|
627
|
+
};
|
|
628
|
+
for (const [name, artifactSpec] of Object.entries(spec.artifacts)) {
|
|
629
|
+
try {
|
|
630
|
+
// Resolve the output path (may contain expressions)
|
|
631
|
+
// Relative paths resolve against the workflow's output directory
|
|
632
|
+
const resolvedPath = String(resolveExpressions(artifactSpec.path, artifactScope));
|
|
633
|
+
const absolutePath = path.isAbsolute(resolvedPath) ? resolvedPath : path.resolve(workflowDir, resolvedPath);
|
|
634
|
+
// Resolve the data source — wrap `from` as ${{ from }} for expression resolution
|
|
635
|
+
const fromExpr = artifactSpec.from.startsWith("${{") ? artifactSpec.from : `\${{ ${artifactSpec.from} }}`;
|
|
636
|
+
const data = resolveExpressions(fromExpr, artifactScope);
|
|
637
|
+
// Validate against schema if specified
|
|
638
|
+
if (artifactSpec.schema) {
|
|
639
|
+
const schemaPath = resolveSchemaPath(artifactSpec.schema, spec.filePath, ctx.cwd);
|
|
640
|
+
validateFromFile(schemaPath, data, `artifact '${name}'`);
|
|
641
|
+
}
|
|
642
|
+
// Route project block JSON targets through block-api for block-schema validation
|
|
643
|
+
const workflowPrefix = path.join(ctx.cwd, PROJECT_DIR) + path.sep;
|
|
644
|
+
if (absolutePath.startsWith(workflowPrefix) && absolutePath.endsWith(".json")) {
|
|
645
|
+
const blockName = path.basename(absolutePath, ".json");
|
|
646
|
+
writeBlock(ctx.cwd, blockName, data);
|
|
647
|
+
writtenArtifacts[name] = absolutePath;
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
// Write non-block artifacts directly
|
|
651
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
652
|
+
if (typeof data === "string") {
|
|
653
|
+
fs.writeFileSync(absolutePath, data);
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
fs.writeFileSync(absolutePath, JSON.stringify(data, null, 2));
|
|
657
|
+
}
|
|
658
|
+
writtenArtifacts[name] = absolutePath;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
663
|
+
// Block artifact failures are fatal — the typed-state contract requires it.
|
|
664
|
+
// Non-block artifact failures remain non-fatal.
|
|
665
|
+
const resolvedPath = String(resolveExpressions(artifactSpec.path, artifactScope));
|
|
666
|
+
const absolutePath = path.isAbsolute(resolvedPath) ? resolvedPath : path.resolve(workflowDir, resolvedPath);
|
|
667
|
+
const isBlockTarget = absolutePath.startsWith(path.join(ctx.cwd, PROJECT_DIR) + path.sep) && absolutePath.endsWith(".json");
|
|
668
|
+
if (isBlockTarget) {
|
|
669
|
+
state.status = "failed";
|
|
670
|
+
if (ctx.hasUI) {
|
|
671
|
+
ctx.ui.notify(`Block artifact '${name}' failed: ${msg}`, "error");
|
|
672
|
+
}
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
// Non-block: warn and continue
|
|
676
|
+
if (ctx.hasUI) {
|
|
677
|
+
ctx.ui.notify(`Artifact '${name}' failed: ${msg}`, "warning");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
// 6. Finalize
|
|
683
|
+
if (state.status === "running") {
|
|
684
|
+
state.status = "completed";
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
writeState(runDir, state);
|
|
688
|
+
}
|
|
689
|
+
catch (err) {
|
|
690
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
691
|
+
if (ctx.hasUI) {
|
|
692
|
+
ctx.ui.notify(`Warning: final state write failed — run history may be incomplete: ${msg}`, "error");
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
writeMetrics(runDir, state.steps);
|
|
696
|
+
// 7. Clean up TUI
|
|
697
|
+
if (ctx.hasUI) {
|
|
698
|
+
ctx.ui.setWidget(WIDGET_ID, undefined);
|
|
699
|
+
ctx.ui.setWorkingMessage(undefined);
|
|
700
|
+
}
|
|
701
|
+
// 8. Build and inject result
|
|
702
|
+
const result = buildResult(spec, runId, runDir, state, state.status);
|
|
703
|
+
// Attach written artifact paths to the result
|
|
704
|
+
if (Object.keys(writtenArtifacts).length > 0) {
|
|
705
|
+
result.artifacts = writtenArtifacts;
|
|
706
|
+
}
|
|
707
|
+
const triggerTurn = spec.triggerTurn !== false;
|
|
708
|
+
if (state.status === "paused") {
|
|
709
|
+
const completedCount = Object.values(state.steps).filter((s) => s.status === "completed").length;
|
|
710
|
+
const totalCount = Object.keys(spec.steps).length;
|
|
711
|
+
const pauseContent = `Workflow '${spec.name}' paused (${completedCount}/${totalCount} steps completed). Use /workflow resume ${spec.name} or Ctrl+J to continue.`;
|
|
712
|
+
pi.sendMessage({ customType: "workflow-result", content: pauseContent, display: "verbose" }, { triggerTurn: false });
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
let content;
|
|
716
|
+
if (spec.completion) {
|
|
717
|
+
try {
|
|
718
|
+
content = resolveCompletion(spec.completion, result, input);
|
|
719
|
+
}
|
|
720
|
+
catch (err) {
|
|
721
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
722
|
+
content = formatResult(result) + `\n\nCompletion template error: ${msg}`;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
content = formatResult(result);
|
|
727
|
+
}
|
|
728
|
+
// P7: Truncate completion message to avoid context overflow (50KB / 2000 lines)
|
|
729
|
+
const truncated = truncateTail(content, { maxLines: 2000, maxBytes: 50 * 1024 });
|
|
730
|
+
content = truncated.content;
|
|
731
|
+
if (truncated.truncated) {
|
|
732
|
+
content += `\n\n[Truncated: output exceeded ${truncated.truncatedBy === "bytes" ? "50KB" : "2000 lines"}. Full output in run dir: ${runDir}]`;
|
|
733
|
+
}
|
|
734
|
+
pi.sendMessage({ customType: "workflow-result", content, display: "verbose" }, { triggerTurn });
|
|
735
|
+
}
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
//# sourceMappingURL=workflow-executor.js.map
|