@leclabs/agent-flow-navigator-mcp 1.2.0 → 1.4.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/catalog/workflows/agile-task.json +6 -0
- package/catalog/workflows/bug-fix.json +12 -0
- package/catalog/workflows/build-review-murder-board.json +6 -0
- package/catalog/workflows/build-review-quick.json +6 -0
- package/catalog/workflows/context-optimization.json +24 -5
- package/catalog/workflows/feature-development.json +12 -0
- package/catalog/workflows/hitl-test.json +46 -0
- package/catalog/workflows/quick-task.json +6 -0
- package/catalog/workflows/refactor.json +12 -0
- package/catalog/workflows/test-coverage.json +6 -0
- package/catalog/workflows/ui-reconstruction.json +18 -0
- package/engine.js +159 -27
- package/index.js +89 -8
- package/package.json +1 -1
- package/store.js +30 -4
- package/types.d.ts +46 -0
|
@@ -148,6 +148,18 @@
|
|
|
148
148
|
{
|
|
149
149
|
"from": "commit",
|
|
150
150
|
"to": "end_success"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"from": "hitl_cannot_reproduce",
|
|
154
|
+
"to": "reproduce",
|
|
155
|
+
"on": "passed",
|
|
156
|
+
"label": "Human provided reproduction info, resume"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"from": "hitl_fix_failed",
|
|
160
|
+
"to": "write_fix",
|
|
161
|
+
"on": "passed",
|
|
162
|
+
"label": "Human resolved fix issue, resume"
|
|
151
163
|
}
|
|
152
164
|
]
|
|
153
165
|
}
|
|
@@ -61,12 +61,19 @@
|
|
|
61
61
|
"name": "Complete",
|
|
62
62
|
"description": "Context optimization complete"
|
|
63
63
|
},
|
|
64
|
-
"
|
|
64
|
+
"hitl_design_failed": {
|
|
65
65
|
"type": "end",
|
|
66
66
|
"result": "blocked",
|
|
67
67
|
"escalation": "hitl",
|
|
68
|
-
"name": "Needs Help",
|
|
69
|
-
"description": "
|
|
68
|
+
"name": "Design Needs Help",
|
|
69
|
+
"description": "Design optimization needs human guidance"
|
|
70
|
+
},
|
|
71
|
+
"hitl_verify_failed": {
|
|
72
|
+
"type": "end",
|
|
73
|
+
"result": "blocked",
|
|
74
|
+
"escalation": "hitl",
|
|
75
|
+
"name": "Verification Needs Help",
|
|
76
|
+
"description": "Verification needs human intervention"
|
|
70
77
|
}
|
|
71
78
|
},
|
|
72
79
|
"edges": [
|
|
@@ -94,7 +101,7 @@
|
|
|
94
101
|
},
|
|
95
102
|
{
|
|
96
103
|
"from": "review_design",
|
|
97
|
-
"to": "
|
|
104
|
+
"to": "hitl_design_failed",
|
|
98
105
|
"on": "failed",
|
|
99
106
|
"label": "Design needs human input"
|
|
100
107
|
},
|
|
@@ -116,7 +123,7 @@
|
|
|
116
123
|
},
|
|
117
124
|
{
|
|
118
125
|
"from": "verify",
|
|
119
|
-
"to": "
|
|
126
|
+
"to": "hitl_verify_failed",
|
|
120
127
|
"on": "failed",
|
|
121
128
|
"label": "Verification failed"
|
|
122
129
|
},
|
|
@@ -125,6 +132,18 @@
|
|
|
125
132
|
"to": "end_success",
|
|
126
133
|
"on": "passed",
|
|
127
134
|
"label": "Optimization verified"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"from": "hitl_design_failed",
|
|
138
|
+
"to": "design_improvements",
|
|
139
|
+
"on": "passed",
|
|
140
|
+
"label": "Human resolved design issue, resume"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"from": "hitl_verify_failed",
|
|
144
|
+
"to": "implement",
|
|
145
|
+
"on": "passed",
|
|
146
|
+
"label": "Human resolved verification issue, resume"
|
|
128
147
|
}
|
|
129
148
|
]
|
|
130
149
|
}
|
|
@@ -220,6 +220,18 @@
|
|
|
220
220
|
{
|
|
221
221
|
"from": "create_pr",
|
|
222
222
|
"to": "end_success"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"from": "hitl_plan_failed",
|
|
226
|
+
"to": "create_plan",
|
|
227
|
+
"on": "passed",
|
|
228
|
+
"label": "Human resolved planning issue, resume"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"from": "hitl_impl_failed",
|
|
232
|
+
"to": "implement",
|
|
233
|
+
"on": "passed",
|
|
234
|
+
"label": "Human resolved implementation issue, resume"
|
|
223
235
|
}
|
|
224
236
|
]
|
|
225
237
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "hitl-test",
|
|
3
|
+
"name": "HITL Test",
|
|
4
|
+
"description": "Minimal workflow for testing HITL recovery: work, gate, escalate, human resumes.",
|
|
5
|
+
"nodes": {
|
|
6
|
+
"start": {
|
|
7
|
+
"type": "start",
|
|
8
|
+
"name": "Start"
|
|
9
|
+
},
|
|
10
|
+
"work": {
|
|
11
|
+
"type": "task",
|
|
12
|
+
"name": "Do Work",
|
|
13
|
+
"description": "Do the thing",
|
|
14
|
+
"agent": "Developer",
|
|
15
|
+
"stage": "development"
|
|
16
|
+
},
|
|
17
|
+
"check": {
|
|
18
|
+
"type": "gate",
|
|
19
|
+
"name": "Check",
|
|
20
|
+
"description": "Pass or fail",
|
|
21
|
+
"agent": "Reviewer",
|
|
22
|
+
"stage": "verification",
|
|
23
|
+
"maxRetries": 1
|
|
24
|
+
},
|
|
25
|
+
"end_success": {
|
|
26
|
+
"type": "end",
|
|
27
|
+
"result": "success",
|
|
28
|
+
"name": "Done"
|
|
29
|
+
},
|
|
30
|
+
"hitl_blocked": {
|
|
31
|
+
"type": "end",
|
|
32
|
+
"result": "blocked",
|
|
33
|
+
"escalation": "hitl",
|
|
34
|
+
"name": "Blocked",
|
|
35
|
+
"description": "Needs human help"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"edges": [
|
|
39
|
+
{ "from": "start", "to": "work" },
|
|
40
|
+
{ "from": "work", "to": "check" },
|
|
41
|
+
{ "from": "check", "to": "end_success", "on": "passed" },
|
|
42
|
+
{ "from": "check", "to": "work", "on": "failed", "label": "Retry" },
|
|
43
|
+
{ "from": "check", "to": "hitl_blocked", "on": "failed", "label": "Escalate" },
|
|
44
|
+
{ "from": "hitl_blocked", "to": "work", "on": "passed", "label": "Human fixed it, resume" }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -231,6 +231,18 @@
|
|
|
231
231
|
{
|
|
232
232
|
"from": "commit",
|
|
233
233
|
"to": "end_success"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"from": "hitl_analysis_failed",
|
|
237
|
+
"to": "design_refactor",
|
|
238
|
+
"on": "passed",
|
|
239
|
+
"label": "Human resolved analysis issue, resume"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"from": "hitl_dev_failed",
|
|
243
|
+
"to": "extract_core",
|
|
244
|
+
"on": "passed",
|
|
245
|
+
"label": "Human resolved development issue, resume"
|
|
234
246
|
}
|
|
235
247
|
]
|
|
236
248
|
}
|
|
@@ -236,6 +236,24 @@
|
|
|
236
236
|
{
|
|
237
237
|
"from": "commit",
|
|
238
238
|
"to": "end_success"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"from": "hitl_ir_failed",
|
|
242
|
+
"to": "ir_component_tree",
|
|
243
|
+
"on": "passed",
|
|
244
|
+
"label": "Human resolved IR issue, resume"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"from": "hitl_build_failed",
|
|
248
|
+
"to": "uiRebuild_build",
|
|
249
|
+
"on": "passed",
|
|
250
|
+
"label": "Human resolved build issue, resume"
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"from": "hitl_final_failed",
|
|
254
|
+
"to": "uiRebuild_build",
|
|
255
|
+
"on": "passed",
|
|
256
|
+
"label": "Human resolved final review issue, resume"
|
|
239
257
|
}
|
|
240
258
|
]
|
|
241
259
|
}
|
package/engine.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
17
|
+
import { join } from "path";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Read and parse a task file
|
|
@@ -52,16 +53,56 @@ export function getTerminalType(node) {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
|
-
*
|
|
56
|
-
* e.g.,
|
|
56
|
+
* Return agent ID as-is from workflow definition.
|
|
57
|
+
* Prefixing (e.g., @flow:) is the caller's responsibility.
|
|
57
58
|
*/
|
|
58
59
|
export function toSubagentRef(agentId) {
|
|
59
60
|
if (!agentId) return null;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
return agentId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Workflow emoji mapping for task subjects
|
|
66
|
+
*/
|
|
67
|
+
const WORKFLOW_EMOJIS = {
|
|
68
|
+
"feature-development": "✨",
|
|
69
|
+
"bug-fix": "🐛",
|
|
70
|
+
"agile-task": "📋",
|
|
71
|
+
"context-optimization": "🔧",
|
|
72
|
+
"quick-task": "⚡",
|
|
73
|
+
"ui-reconstruction": "🎨",
|
|
74
|
+
"test-coverage": "🧪",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build formatted task subject for write-through
|
|
79
|
+
*/
|
|
80
|
+
export function buildTaskSubject(taskId, userDescription, workflowType, stepId, subagent, terminal, maxRetries, retryCount) {
|
|
81
|
+
const emoji = WORKFLOW_EMOJIS[workflowType] || "";
|
|
82
|
+
const line1 = `#${taskId} ${userDescription}${emoji ? ` ${emoji}` : ""}`;
|
|
83
|
+
|
|
84
|
+
let line2;
|
|
85
|
+
if (terminal === "success") {
|
|
86
|
+
line2 = `→ ${workflowType} · completed ✓`;
|
|
87
|
+
} else if (terminal === "hitl" || terminal === "failure") {
|
|
88
|
+
line2 = `→ ${workflowType} · ${stepId} · HITL`;
|
|
89
|
+
} else {
|
|
90
|
+
const agent = subagent ? `(${subagent})` : "(direct)";
|
|
91
|
+
const retries = maxRetries > 0 ? ` · retries: ${retryCount}/${maxRetries}` : "";
|
|
92
|
+
line2 = `→ ${workflowType} · ${stepId} ${agent}${retries}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return `${line1}\n${line2}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build activeForm for task spinner display
|
|
100
|
+
*/
|
|
101
|
+
export function buildTaskActiveForm(stepName, subagent, terminal) {
|
|
102
|
+
if (terminal === "success") return "Completed";
|
|
103
|
+
if (terminal === "hitl" || terminal === "failure") return "HITL - Needs human help";
|
|
104
|
+
const agent = subagent ? ` (${subagent})` : "";
|
|
105
|
+
return `${stepName}${agent}`;
|
|
65
106
|
}
|
|
66
107
|
|
|
67
108
|
/**
|
|
@@ -71,8 +112,13 @@ export function getBaselineInstructions(stepId, stepName) {
|
|
|
71
112
|
const id = stepId.toLowerCase();
|
|
72
113
|
const name = (stepName || "").toLowerCase();
|
|
73
114
|
|
|
74
|
-
//
|
|
75
|
-
if (id.includes("
|
|
115
|
+
// Review steps (checked early — "plan_review" is a review, not a plan)
|
|
116
|
+
if (id.includes("review")) {
|
|
117
|
+
return "Check for correctness, code quality, and adherence to project standards. Verify the implementation meets requirements.";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Analysis/Requirements steps
|
|
121
|
+
if (id.includes("analyze") || id.includes("analysis") || id.includes("parse") || id.includes("requirements") || name.includes("analyze")) {
|
|
76
122
|
return "Review the task requirements carefully. Identify key constraints, dependencies, and acceptance criteria. Create a clear plan before proceeding.";
|
|
77
123
|
}
|
|
78
124
|
if (id.includes("plan") || id.includes("design") || name.includes("plan")) {
|
|
@@ -90,16 +136,16 @@ export function getBaselineInstructions(stepId, stepName) {
|
|
|
90
136
|
return "Improve code structure without changing behavior. Ensure all tests pass before and after changes.";
|
|
91
137
|
}
|
|
92
138
|
|
|
139
|
+
// Lint/format steps
|
|
140
|
+
if (id.includes("lint") || id.includes("format")) {
|
|
141
|
+
return "Run linting and formatting checks. Auto-fix issues where possible. Flag any issues that require manual attention.";
|
|
142
|
+
}
|
|
143
|
+
|
|
93
144
|
// Testing steps
|
|
94
145
|
if (id.includes("test") || id.includes("verify") || id.includes("validate")) {
|
|
95
146
|
return "Verify the implementation works correctly. Test happy paths, edge cases, and error conditions. Document any issues found.";
|
|
96
147
|
}
|
|
97
148
|
|
|
98
|
-
// Review steps
|
|
99
|
-
if (id.includes("review")) {
|
|
100
|
-
return "Check for correctness, code quality, and adherence to project standards. Verify the implementation meets requirements.";
|
|
101
|
-
}
|
|
102
|
-
|
|
103
149
|
// Documentation steps
|
|
104
150
|
if (id.includes("document") || id.includes("readme")) {
|
|
105
151
|
return "Write clear, concise documentation. Focus on what users need to know, not implementation details.";
|
|
@@ -127,18 +173,61 @@ export function getBaselineInstructions(stepId, stepName) {
|
|
|
127
173
|
return "Complete this step thoroughly. Document your findings and any decisions made.";
|
|
128
174
|
}
|
|
129
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Resolve a context_files entry to an absolute path.
|
|
178
|
+
*
|
|
179
|
+
* Convention (follows Claude Code plugin path rules):
|
|
180
|
+
* - "./path" → relative to the workflow's source root (plugin root, project root, etc.)
|
|
181
|
+
* - "path" → relative to projectRoot
|
|
182
|
+
*
|
|
183
|
+
* @param {string} file - Context file entry
|
|
184
|
+
* @param {string} projectRoot - Project root directory
|
|
185
|
+
* @param {string|null} sourceRoot - Root directory of the workflow's source
|
|
186
|
+
* @returns {string} Absolute file path
|
|
187
|
+
*/
|
|
188
|
+
export function resolveContextFile(file, projectRoot, sourceRoot) {
|
|
189
|
+
if (file.startsWith("./") && sourceRoot) {
|
|
190
|
+
return join(sourceRoot, file);
|
|
191
|
+
}
|
|
192
|
+
return join(projectRoot, file);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Resolve ./ prefixed paths in prose text against sourceRoot.
|
|
197
|
+
* Leaves text unchanged when sourceRoot is null or text has no ./ references.
|
|
198
|
+
* @param {string|null} text - Prose text that may contain ./ paths
|
|
199
|
+
* @param {string|null} sourceRoot - Root directory for ./ resolution
|
|
200
|
+
* @returns {string|null} Text with ./ paths resolved to absolute paths
|
|
201
|
+
*/
|
|
202
|
+
export function resolveProseRefs(text, sourceRoot) {
|
|
203
|
+
if (!text || !sourceRoot) return text;
|
|
204
|
+
return text.replace(/\.\/[\w\-\.\/]+/g, (match) => join(sourceRoot, match));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Build context loading instructions from step-level context_files.
|
|
209
|
+
* Returns a markdown section or null if no context declared.
|
|
210
|
+
*/
|
|
211
|
+
export function buildContextInstructions({ contextFiles, projectRoot, sourceRoot }) {
|
|
212
|
+
if (!contextFiles?.length || !projectRoot) return null;
|
|
213
|
+
const lines = contextFiles.map((file) => `- Read file: ${resolveContextFile(file, projectRoot, sourceRoot)}`);
|
|
214
|
+
return `## Context\n\nBefore beginning, load the following:\n${lines.join("\n")}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
130
217
|
/**
|
|
131
218
|
* Build orchestrator instructions for task creation/update
|
|
132
219
|
* Returns null for terminal nodes (no further work)
|
|
133
220
|
*/
|
|
134
|
-
function buildOrchestratorInstructions(workflowType, stepId, stage, subagent, stepInstructions, description) {
|
|
221
|
+
function buildOrchestratorInstructions(workflowType, stepId, stage, subagent, stepInstructions, description, contextBlock) {
|
|
135
222
|
if (!stepInstructions) return null; // Terminal nodes have no instructions
|
|
136
223
|
|
|
137
224
|
const delegationPrefix = subagent ? `Invoke ${subagent} to complete the following task: ` : "";
|
|
138
225
|
|
|
139
|
-
|
|
226
|
+
let result = `${delegationPrefix}${stepInstructions.guidance}
|
|
140
227
|
|
|
141
228
|
${description || "{task description}"}`;
|
|
229
|
+
if (contextBlock) result += `\n\n${contextBlock}`;
|
|
230
|
+
return result;
|
|
142
231
|
}
|
|
143
232
|
|
|
144
233
|
/**
|
|
@@ -152,31 +241,46 @@ function buildNavigateResponse(
|
|
|
152
241
|
action,
|
|
153
242
|
retriesIncremented = false,
|
|
154
243
|
retryCount = 0,
|
|
155
|
-
description = null
|
|
244
|
+
description = null,
|
|
245
|
+
resetRetryCount = false,
|
|
246
|
+
projectRoot = null,
|
|
247
|
+
sourceRoot = null
|
|
156
248
|
) {
|
|
157
249
|
const stage = stepDef.stage || null;
|
|
158
250
|
const subagent = stepDef.agent ? toSubagentRef(stepDef.agent) : null;
|
|
159
251
|
|
|
160
252
|
// Build step instructions from workflow definition + baseline
|
|
253
|
+
// Resolve ./ paths in prose fields against sourceRoot (same convention as context_files)
|
|
161
254
|
const isTerminal = isTerminalNode(stepDef);
|
|
162
255
|
const stepInstructions = isTerminal
|
|
163
256
|
? null
|
|
164
257
|
: {
|
|
165
258
|
name: stepDef.name || stepId,
|
|
166
|
-
description: stepDef.description || null,
|
|
167
|
-
guidance: stepDef.instructions || getBaselineInstructions(stepId, stepDef.name),
|
|
259
|
+
description: resolveProseRefs(stepDef.description, sourceRoot) || null,
|
|
260
|
+
guidance: resolveProseRefs(stepDef.instructions, sourceRoot) || getBaselineInstructions(stepId, stepDef.name),
|
|
168
261
|
};
|
|
169
262
|
|
|
263
|
+
// Build context block from step-level context_files
|
|
264
|
+
const contextBlock = isTerminal
|
|
265
|
+
? null
|
|
266
|
+
: buildContextInstructions({ contextFiles: stepDef.context_files, projectRoot, sourceRoot });
|
|
267
|
+
|
|
170
268
|
// Build orchestrator instructions for all non-terminal actions
|
|
171
269
|
const orchestratorInstructions = isTerminal
|
|
172
270
|
? null
|
|
173
|
-
: buildOrchestratorInstructions(workflowType, stepId, stage, subagent, stepInstructions, description);
|
|
271
|
+
: buildOrchestratorInstructions(workflowType, stepId, stage, subagent, stepInstructions, description, contextBlock);
|
|
174
272
|
|
|
175
273
|
// Build metadata for task storage
|
|
274
|
+
// Increment on retry, reset on start or explicit forward progress (conditional advance),
|
|
275
|
+
// preserve on unconditional advances within retry loops and escalations
|
|
176
276
|
const metadata = {
|
|
177
277
|
workflowType,
|
|
178
278
|
currentStep: stepId,
|
|
179
|
-
retryCount: retriesIncremented
|
|
279
|
+
retryCount: retriesIncremented
|
|
280
|
+
? retryCount + 1
|
|
281
|
+
: action === "start" || resetRetryCount
|
|
282
|
+
? 0
|
|
283
|
+
: retryCount,
|
|
180
284
|
};
|
|
181
285
|
|
|
182
286
|
return {
|
|
@@ -187,6 +291,7 @@ function buildNavigateResponse(
|
|
|
187
291
|
terminal: getTerminalType(stepDef),
|
|
188
292
|
action,
|
|
189
293
|
retriesIncremented,
|
|
294
|
+
maxRetries: stepDef.maxRetries || 0,
|
|
190
295
|
orchestratorInstructions,
|
|
191
296
|
metadata,
|
|
192
297
|
};
|
|
@@ -357,7 +462,7 @@ export class WorkflowEngine {
|
|
|
357
462
|
* @param {string} [options.description] - User's task description
|
|
358
463
|
* @returns {Object} Navigation response with currentStep, stepInstructions, terminal, action, metadata, etc.
|
|
359
464
|
*/
|
|
360
|
-
navigate({ taskFilePath, workflowType, result, description } = {}) {
|
|
465
|
+
navigate({ taskFilePath, workflowType, result, description, projectRoot } = {}) {
|
|
361
466
|
let currentStep = null;
|
|
362
467
|
let retryCount = 0;
|
|
363
468
|
|
|
@@ -397,6 +502,9 @@ export class WorkflowEngine {
|
|
|
397
502
|
throw new Error(`Workflow '${workflowType}' must have nodes`);
|
|
398
503
|
}
|
|
399
504
|
|
|
505
|
+
// Resolve source root for context_files with ./ prefix
|
|
506
|
+
const sourceRoot = this.store.getSourceRoot?.(workflowType) || null;
|
|
507
|
+
|
|
400
508
|
const { nodes } = wfDef;
|
|
401
509
|
|
|
402
510
|
// Case 1: No currentStep - start at first work step
|
|
@@ -417,7 +525,7 @@ export class WorkflowEngine {
|
|
|
417
525
|
throw new Error(`First step '${firstEdge.to}' not found in workflow`);
|
|
418
526
|
}
|
|
419
527
|
|
|
420
|
-
return buildNavigateResponse(workflowType, firstEdge.to, firstStepDef, "start", false, 0, description);
|
|
528
|
+
return buildNavigateResponse(workflowType, firstEdge.to, firstStepDef, "start", false, 0, description, false, projectRoot, sourceRoot);
|
|
421
529
|
}
|
|
422
530
|
|
|
423
531
|
// Case 2: currentStep but no result - return current state
|
|
@@ -427,7 +535,7 @@ export class WorkflowEngine {
|
|
|
427
535
|
throw new Error(`Step '${currentStep}' not found in workflow '${workflowType}'`);
|
|
428
536
|
}
|
|
429
537
|
|
|
430
|
-
return buildNavigateResponse(workflowType, currentStep, stepDef, "current", false, retryCount, description);
|
|
538
|
+
return buildNavigateResponse(workflowType, currentStep, stepDef, "current", false, retryCount, description, false, projectRoot, sourceRoot);
|
|
431
539
|
}
|
|
432
540
|
|
|
433
541
|
// Case 3: currentStep and result - advance to next step
|
|
@@ -447,9 +555,13 @@ export class WorkflowEngine {
|
|
|
447
555
|
}
|
|
448
556
|
|
|
449
557
|
// Determine action and whether retries incremented
|
|
558
|
+
const currentStepDef = nodes[currentStep];
|
|
559
|
+
const isHitlResume = getTerminalType(currentStepDef) === "hitl";
|
|
450
560
|
const isRetry = evaluation.action === "retry";
|
|
451
561
|
let action;
|
|
452
|
-
if (
|
|
562
|
+
if (isHitlResume) {
|
|
563
|
+
action = "advance"; // Human fixed it → fresh advance, retryCount resets
|
|
564
|
+
} else if (isRetry) {
|
|
453
565
|
action = "retry";
|
|
454
566
|
} else if (getTerminalType(nextStepDef) === "hitl") {
|
|
455
567
|
action = "escalate";
|
|
@@ -457,6 +569,10 @@ export class WorkflowEngine {
|
|
|
457
569
|
action = "advance";
|
|
458
570
|
}
|
|
459
571
|
|
|
572
|
+
// Only reset retryCount on genuine forward progress (conditional edge like on:"passed")
|
|
573
|
+
// Unconditional advances within retry loops (e.g., work → gate) preserve the count
|
|
574
|
+
const resetRetryCount = action === "advance" && evaluation.action === "conditional";
|
|
575
|
+
|
|
460
576
|
const response = buildNavigateResponse(
|
|
461
577
|
workflowType,
|
|
462
578
|
evaluation.nextStep,
|
|
@@ -464,14 +580,30 @@ export class WorkflowEngine {
|
|
|
464
580
|
action,
|
|
465
581
|
isRetry,
|
|
466
582
|
retryCount,
|
|
467
|
-
description
|
|
583
|
+
description,
|
|
584
|
+
resetRetryCount,
|
|
585
|
+
projectRoot,
|
|
586
|
+
sourceRoot
|
|
468
587
|
);
|
|
469
588
|
|
|
470
|
-
// Write-through: persist state transition to task file
|
|
589
|
+
// Write-through: persist state transition and presentation to task file
|
|
471
590
|
if (taskFilePath) {
|
|
472
591
|
const task = readTaskFile(taskFilePath);
|
|
473
592
|
if (task) {
|
|
593
|
+
const userDesc = task.metadata?.userDescription || "";
|
|
474
594
|
task.metadata = { ...task.metadata, ...response.metadata };
|
|
595
|
+
task.subject = buildTaskSubject(
|
|
596
|
+
task.id, userDesc, response.metadata.workflowType,
|
|
597
|
+
response.currentStep, response.subagent, response.terminal,
|
|
598
|
+
response.maxRetries, response.metadata.retryCount
|
|
599
|
+
);
|
|
600
|
+
task.activeForm = buildTaskActiveForm(
|
|
601
|
+
response.stepInstructions?.name || response.currentStep,
|
|
602
|
+
response.subagent, response.terminal
|
|
603
|
+
);
|
|
604
|
+
if (response.orchestratorInstructions) {
|
|
605
|
+
task.description = response.orchestratorInstructions;
|
|
606
|
+
}
|
|
475
607
|
writeFileSync(taskFilePath, JSON.stringify(task, null, 2));
|
|
476
608
|
}
|
|
477
609
|
}
|
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - ListWorkflows: List available workflows
|
|
11
11
|
* - Diagram: Generate mermaid diagram for workflow
|
|
12
12
|
* - CopyWorkflows: Copy workflows from catalog to project
|
|
13
|
+
* - LoadWorkflows: Load workflows from a directory at runtime (external plugins or project reload)
|
|
13
14
|
* - ListCatalog: List workflows available in catalog
|
|
14
15
|
*/
|
|
15
16
|
|
|
@@ -101,23 +102,51 @@ function loadCatalogWorkflows(dirPath) {
|
|
|
101
102
|
return loaded;
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Load external workflows from a directory: flat {id}.json files
|
|
107
|
+
* Used by the LoadWorkflows tool at runtime.
|
|
108
|
+
* @param {string} dirPath - Directory containing {id}.json workflow files
|
|
109
|
+
* @param {string} sourceRoot - Root path for resolving ./ context_files
|
|
110
|
+
* @returns {string[]} Array of loaded workflow IDs
|
|
111
|
+
*/
|
|
112
|
+
function loadExternalWorkflows(dirPath, sourceRoot) {
|
|
113
|
+
if (!existsSync(dirPath)) return [];
|
|
114
|
+
|
|
115
|
+
const loaded = [];
|
|
116
|
+
const files = readdirSync(dirPath).filter((f) => f.endsWith(".json"));
|
|
117
|
+
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
const id = file.replace(".json", "");
|
|
120
|
+
try {
|
|
121
|
+
const content = JSON.parse(readFileSync(join(dirPath, file), "utf-8"));
|
|
122
|
+
if (validateWorkflow(id, content)) {
|
|
123
|
+
store.loadDefinition(id, content, "external", sourceRoot);
|
|
124
|
+
loaded.push(id);
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.error(`Error loading external workflow ${id}: ${e.message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return loaded;
|
|
132
|
+
}
|
|
133
|
+
|
|
104
134
|
/**
|
|
105
135
|
* Load workflows: catalog first, then project overwrites (project takes precedence)
|
|
106
136
|
*/
|
|
107
137
|
function loadWorkflows() {
|
|
108
138
|
const catalogPath = join(CATALOG_PATH, "workflows");
|
|
109
139
|
const catalogLoaded = loadCatalogWorkflows(catalogPath);
|
|
110
|
-
|
|
111
140
|
const projectLoaded = existsSync(WORKFLOWS_PATH) ? loadProjectWorkflows(WORKFLOWS_PATH) : [];
|
|
112
141
|
|
|
113
|
-
|
|
114
|
-
const fromCatalog = catalogLoaded.filter((id) => !projectLoaded.includes(id));
|
|
142
|
+
const allLoaded = [...new Set([...catalogLoaded, ...projectLoaded])];
|
|
115
143
|
const fromProject = projectLoaded;
|
|
144
|
+
const fromCatalog = catalogLoaded.filter((id) => !projectLoaded.includes(id));
|
|
116
145
|
|
|
117
146
|
return {
|
|
118
147
|
catalog: fromCatalog,
|
|
119
148
|
project: fromProject,
|
|
120
|
-
loaded:
|
|
149
|
+
loaded: allLoaded,
|
|
121
150
|
};
|
|
122
151
|
}
|
|
123
152
|
|
|
@@ -186,6 +215,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
186
215
|
properties: {
|
|
187
216
|
workflowType: { type: "string", description: "Workflow ID to visualize" },
|
|
188
217
|
currentStep: { type: "string", description: "Optional: highlight this step" },
|
|
218
|
+
filePath: {
|
|
219
|
+
type: "string",
|
|
220
|
+
description: "Optional: absolute path to save the diagram. Defaults to .flow/diagrams/{workflowType}.md",
|
|
221
|
+
},
|
|
189
222
|
},
|
|
190
223
|
required: ["workflowType"],
|
|
191
224
|
},
|
|
@@ -205,6 +238,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
205
238
|
},
|
|
206
239
|
},
|
|
207
240
|
},
|
|
241
|
+
{
|
|
242
|
+
name: "LoadWorkflows",
|
|
243
|
+
description:
|
|
244
|
+
"Load workflows from a directory at runtime. External plugins pass their root path; omit path to reload project workflows.",
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: "object",
|
|
247
|
+
properties: {
|
|
248
|
+
path: {
|
|
249
|
+
type: "string",
|
|
250
|
+
description:
|
|
251
|
+
"Directory containing {id}.json workflow files. Omit to reload project workflows from .flow/workflows/.",
|
|
252
|
+
},
|
|
253
|
+
sourceRoot: {
|
|
254
|
+
type: "string",
|
|
255
|
+
description:
|
|
256
|
+
"Root path for resolving ./ context_files entries. Defaults to path (or project root when path omitted).",
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
208
261
|
{
|
|
209
262
|
name: "ListCatalog",
|
|
210
263
|
description: "List workflows available in the catalog.",
|
|
@@ -229,6 +282,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
229
282
|
workflowType: args.workflowType,
|
|
230
283
|
result: args.result,
|
|
231
284
|
description: args.description,
|
|
285
|
+
projectRoot: PROJECT_ROOT,
|
|
232
286
|
});
|
|
233
287
|
return jsonResponse(result);
|
|
234
288
|
}
|
|
@@ -264,11 +318,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
264
318
|
const source = store.getSource(args.workflowType);
|
|
265
319
|
const markdown = generateDiagram(wfDef, args.currentStep);
|
|
266
320
|
|
|
267
|
-
// Save diagram to file
|
|
268
|
-
|
|
269
|
-
|
|
321
|
+
// Save diagram to file (use provided path or default)
|
|
322
|
+
const filePath = args.filePath || join(DIAGRAMS_PATH, `${args.workflowType}.md`);
|
|
323
|
+
const fileDir = dirname(filePath);
|
|
324
|
+
if (!existsSync(fileDir)) {
|
|
325
|
+
mkdirSync(fileDir, { recursive: true });
|
|
270
326
|
}
|
|
271
|
-
const filePath = join(DIAGRAMS_PATH, `${args.workflowType}.md`);
|
|
272
327
|
writeFileSync(filePath, markdown);
|
|
273
328
|
|
|
274
329
|
return jsonResponse({ savedTo: filePath, source });
|
|
@@ -331,6 +386,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
331
386
|
});
|
|
332
387
|
}
|
|
333
388
|
|
|
389
|
+
case "LoadWorkflows": {
|
|
390
|
+
if (args.path) {
|
|
391
|
+
// External plugin: load from provided path
|
|
392
|
+
const dirPath = resolve(args.path);
|
|
393
|
+
const root = args.sourceRoot ? resolve(args.sourceRoot) : dirPath;
|
|
394
|
+
const loaded = loadExternalWorkflows(dirPath, root);
|
|
395
|
+
return jsonResponse({
|
|
396
|
+
schemaVersion: 2,
|
|
397
|
+
loaded,
|
|
398
|
+
source: "external",
|
|
399
|
+
sourceRoot: root,
|
|
400
|
+
path: dirPath,
|
|
401
|
+
});
|
|
402
|
+
} else {
|
|
403
|
+
// Project: reload from .flow/workflows/
|
|
404
|
+
const loaded = existsSync(WORKFLOWS_PATH) ? loadProjectWorkflows(WORKFLOWS_PATH) : [];
|
|
405
|
+
return jsonResponse({
|
|
406
|
+
schemaVersion: 2,
|
|
407
|
+
loaded,
|
|
408
|
+
source: "project",
|
|
409
|
+
sourceRoot: PROJECT_ROOT,
|
|
410
|
+
path: WORKFLOWS_PATH,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
334
415
|
case "ListCatalog": {
|
|
335
416
|
const catalogPath = join(CATALOG_PATH, "workflows");
|
|
336
417
|
if (!existsSync(catalogPath)) {
|
package/package.json
CHANGED
package/store.js
CHANGED
|
@@ -29,19 +29,24 @@ export function validateWorkflow(id, content) {
|
|
|
29
29
|
export class WorkflowStore {
|
|
30
30
|
constructor() {
|
|
31
31
|
this.workflows = new Map();
|
|
32
|
-
this.sources = new Map(); // Track source: "catalog" | "project"
|
|
32
|
+
this.sources = new Map(); // Track source: "catalog" | "project" | "external"
|
|
33
|
+
this.sourceRoots = new Map(); // Track source root path per workflow (for ./ resolution)
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Load a workflow definition into the store
|
|
37
38
|
* @param {string} id - Workflow identifier
|
|
38
39
|
* @param {Object} workflow - Workflow definition
|
|
39
|
-
* @param {string} source - Source: "catalog" | "project"
|
|
40
|
+
* @param {string} source - Source: "catalog" | "project" | "external"
|
|
41
|
+
* @param {string|null} sourceRoot - Root path for resolving ./ context_files
|
|
40
42
|
* @returns {string} The workflow id
|
|
41
43
|
*/
|
|
42
|
-
loadDefinition(id, workflow, source = "catalog") {
|
|
44
|
+
loadDefinition(id, workflow, source = "catalog", sourceRoot = null) {
|
|
43
45
|
this.workflows.set(id, workflow);
|
|
44
46
|
this.sources.set(id, source);
|
|
47
|
+
if (sourceRoot) {
|
|
48
|
+
this.sourceRoots.set(id, sourceRoot);
|
|
49
|
+
}
|
|
45
50
|
return id;
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -56,7 +61,7 @@ export class WorkflowStore {
|
|
|
56
61
|
|
|
57
62
|
/**
|
|
58
63
|
* List all loaded workflows with metadata
|
|
59
|
-
* @param {string} filter - Filter by source: "all" | "project" | "catalog"
|
|
64
|
+
* @param {string} filter - Filter by source: "all" | "project" | "catalog" | "external"
|
|
60
65
|
* @returns {Array} Array of workflow summaries
|
|
61
66
|
*/
|
|
62
67
|
listWorkflows(filter = "all") {
|
|
@@ -86,6 +91,17 @@ export class WorkflowStore {
|
|
|
86
91
|
return false;
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Check if any external workflows exist
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
hasExternalWorkflows() {
|
|
99
|
+
for (const source of this.sources.values()) {
|
|
100
|
+
if (source === "external") return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
/**
|
|
90
106
|
* Get the source of a workflow
|
|
91
107
|
* @param {string} id - Workflow identifier
|
|
@@ -95,6 +111,15 @@ export class WorkflowStore {
|
|
|
95
111
|
return this.sources.get(id);
|
|
96
112
|
}
|
|
97
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Get the source root path for a workflow (for ./ context_files resolution)
|
|
116
|
+
* @param {string} id - Workflow identifier
|
|
117
|
+
* @returns {string|undefined} Source root path or undefined
|
|
118
|
+
*/
|
|
119
|
+
getSourceRoot(id) {
|
|
120
|
+
return this.sourceRoots.get(id);
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
/**
|
|
99
124
|
* Check if a workflow exists
|
|
100
125
|
* @param {string} id - Workflow identifier
|
|
@@ -110,6 +135,7 @@ export class WorkflowStore {
|
|
|
110
135
|
clear() {
|
|
111
136
|
this.workflows.clear();
|
|
112
137
|
this.sources.clear();
|
|
138
|
+
this.sourceRoots.clear();
|
|
113
139
|
}
|
|
114
140
|
|
|
115
141
|
/**
|
package/types.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ export interface TaskNode {
|
|
|
51
51
|
outputs?: string[];
|
|
52
52
|
maxRetries?: number;
|
|
53
53
|
config?: Record<string, unknown>;
|
|
54
|
+
context_files?: string[];
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/**
|
|
@@ -66,6 +67,7 @@ export interface GateNode {
|
|
|
66
67
|
outputs?: string[];
|
|
67
68
|
maxRetries?: number;
|
|
68
69
|
config?: Record<string, unknown>;
|
|
70
|
+
context_files?: string[];
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
/**
|
|
@@ -131,3 +133,47 @@ export type EdgeAction =
|
|
|
131
133
|
| "escalate" // Failed and exceeded retry limit
|
|
132
134
|
| "no_outgoing_edges" // Terminal node (no edges)
|
|
133
135
|
| "no_matching_edge"; // No edge matched the output
|
|
136
|
+
|
|
137
|
+
// =============================================================================
|
|
138
|
+
// Navigate Options
|
|
139
|
+
// =============================================================================
|
|
140
|
+
|
|
141
|
+
export interface NavigateOptions {
|
|
142
|
+
taskFilePath?: string;
|
|
143
|
+
workflowType?: string;
|
|
144
|
+
result?: "passed" | "failed";
|
|
145
|
+
description?: string;
|
|
146
|
+
projectRoot?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// =============================================================================
|
|
150
|
+
// Context Resolution
|
|
151
|
+
// =============================================================================
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolve a context_files entry to an absolute path.
|
|
155
|
+
* - "./path" → relative to sourceRoot (the workflow's source directory)
|
|
156
|
+
* - "path" → relative to projectRoot
|
|
157
|
+
*/
|
|
158
|
+
export function resolveContextFile(
|
|
159
|
+
file: string,
|
|
160
|
+
projectRoot: string,
|
|
161
|
+
sourceRoot?: string | null
|
|
162
|
+
): string;
|
|
163
|
+
|
|
164
|
+
export function resolveProseRefs(
|
|
165
|
+
text: string | null,
|
|
166
|
+
sourceRoot: string | null
|
|
167
|
+
): string | null;
|
|
168
|
+
|
|
169
|
+
export function buildContextInstructions(options: {
|
|
170
|
+
contextFiles?: string[];
|
|
171
|
+
projectRoot?: string | null;
|
|
172
|
+
sourceRoot?: string | null;
|
|
173
|
+
}): string | null;
|
|
174
|
+
|
|
175
|
+
// =============================================================================
|
|
176
|
+
// Store Types
|
|
177
|
+
// =============================================================================
|
|
178
|
+
|
|
179
|
+
export type WorkflowSource = "catalog" | "project" | "external";
|