@leclabs/agent-flow-navigator-mcp 1.2.0 → 1.3.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.
@@ -125,6 +125,12 @@
125
125
  {
126
126
  "from": "commit",
127
127
  "to": "end_success"
128
+ },
129
+ {
130
+ "from": "hitl_failed",
131
+ "to": "implement",
132
+ "on": "passed",
133
+ "label": "Human resolved issue, resume"
128
134
  }
129
135
  ]
130
136
  }
@@ -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
  }
@@ -105,6 +105,12 @@
105
105
  {
106
106
  "from": "commit",
107
107
  "to": "end_success"
108
+ },
109
+ {
110
+ "from": "hitl_blocked",
111
+ "to": "build",
112
+ "on": "passed",
113
+ "label": "Human resolved issue, resume"
108
114
  }
109
115
  ]
110
116
  }
@@ -103,6 +103,12 @@
103
103
  {
104
104
  "from": "commit",
105
105
  "to": "end_success"
106
+ },
107
+ {
108
+ "from": "hitl_blocked",
109
+ "to": "build",
110
+ "on": "passed",
111
+ "label": "Human resolved issue, resume"
106
112
  }
107
113
  ]
108
114
  }
@@ -61,12 +61,19 @@
61
61
  "name": "Complete",
62
62
  "description": "Context optimization complete"
63
63
  },
64
- "hitl_failed": {
64
+ "hitl_design_failed": {
65
65
  "type": "end",
66
66
  "result": "blocked",
67
67
  "escalation": "hitl",
68
- "name": "Needs Help",
69
- "description": "Optimization needs human guidance"
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": "hitl_failed",
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": "hitl_failed",
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
+ }
@@ -110,6 +110,12 @@
110
110
  {
111
111
  "from": "commit",
112
112
  "to": "end_success"
113
+ },
114
+ {
115
+ "from": "hitl_blocked",
116
+ "to": "execute",
117
+ "on": "passed",
118
+ "label": "Human resolved issue, resume"
113
119
  }
114
120
  ]
115
121
  }
@@ -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
  }
@@ -148,6 +148,12 @@
148
148
  {
149
149
  "from": "commit",
150
150
  "to": "end_success"
151
+ },
152
+ {
153
+ "from": "hitl_failed",
154
+ "to": "write_tests",
155
+ "on": "passed",
156
+ "label": "Human resolved issue, resume"
151
157
  }
152
158
  ]
153
159
  }
@@ -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
- * Convert agent ID to subagent reference
56
- * e.g., "developer" -> "@flow:developer"
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
- if (agentId.startsWith("@")) return agentId;
61
- // Namespaced: "org:developer" -> "@org:developer"
62
- if (agentId.includes(":")) return `@${agentId}`;
63
- // Simple: "developer" -> "@flow:developer"
64
- return `@flow:${agentId}`;
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
- // Analysis/Planning steps
75
- if (id.includes("analyze") || id.includes("analysis") || name.includes("analyze")) {
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,30 @@ 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
+ * Build context loading instructions from step-level context_files.
178
+ * Returns a markdown section or null if no context declared.
179
+ */
180
+ export function buildContextInstructions({ contextFiles, projectRoot }) {
181
+ if (!contextFiles?.length || !projectRoot) return null;
182
+ const lines = contextFiles.map((file) => `- Read file: ${join(projectRoot, file)}`);
183
+ return `## Context\n\nBefore beginning, load the following:\n${lines.join("\n")}`;
184
+ }
185
+
130
186
  /**
131
187
  * Build orchestrator instructions for task creation/update
132
188
  * Returns null for terminal nodes (no further work)
133
189
  */
134
- function buildOrchestratorInstructions(workflowType, stepId, stage, subagent, stepInstructions, description) {
190
+ function buildOrchestratorInstructions(workflowType, stepId, stage, subagent, stepInstructions, description, contextBlock) {
135
191
  if (!stepInstructions) return null; // Terminal nodes have no instructions
136
192
 
137
193
  const delegationPrefix = subagent ? `Invoke ${subagent} to complete the following task: ` : "";
138
194
 
139
- return `${delegationPrefix}${stepInstructions.guidance}
195
+ let result = `${delegationPrefix}${stepInstructions.guidance}
140
196
 
141
197
  ${description || "{task description}"}`;
198
+ if (contextBlock) result += `\n\n${contextBlock}`;
199
+ return result;
142
200
  }
143
201
 
144
202
  /**
@@ -152,7 +210,9 @@ function buildNavigateResponse(
152
210
  action,
153
211
  retriesIncremented = false,
154
212
  retryCount = 0,
155
- description = null
213
+ description = null,
214
+ resetRetryCount = false,
215
+ projectRoot = null
156
216
  ) {
157
217
  const stage = stepDef.stage || null;
158
218
  const subagent = stepDef.agent ? toSubagentRef(stepDef.agent) : null;
@@ -167,16 +227,27 @@ function buildNavigateResponse(
167
227
  guidance: stepDef.instructions || getBaselineInstructions(stepId, stepDef.name),
168
228
  };
169
229
 
230
+ // Build context block from step-level context_files
231
+ const contextBlock = isTerminal
232
+ ? null
233
+ : buildContextInstructions({ contextFiles: stepDef.context_files, projectRoot });
234
+
170
235
  // Build orchestrator instructions for all non-terminal actions
171
236
  const orchestratorInstructions = isTerminal
172
237
  ? null
173
- : buildOrchestratorInstructions(workflowType, stepId, stage, subagent, stepInstructions, description);
238
+ : buildOrchestratorInstructions(workflowType, stepId, stage, subagent, stepInstructions, description, contextBlock);
174
239
 
175
240
  // Build metadata for task storage
241
+ // Increment on retry, reset on start or explicit forward progress (conditional advance),
242
+ // preserve on unconditional advances within retry loops and escalations
176
243
  const metadata = {
177
244
  workflowType,
178
245
  currentStep: stepId,
179
- retryCount: retriesIncremented ? retryCount + 1 : retryCount,
246
+ retryCount: retriesIncremented
247
+ ? retryCount + 1
248
+ : action === "start" || resetRetryCount
249
+ ? 0
250
+ : retryCount,
180
251
  };
181
252
 
182
253
  return {
@@ -187,6 +258,7 @@ function buildNavigateResponse(
187
258
  terminal: getTerminalType(stepDef),
188
259
  action,
189
260
  retriesIncremented,
261
+ maxRetries: stepDef.maxRetries || 0,
190
262
  orchestratorInstructions,
191
263
  metadata,
192
264
  };
@@ -357,7 +429,7 @@ export class WorkflowEngine {
357
429
  * @param {string} [options.description] - User's task description
358
430
  * @returns {Object} Navigation response with currentStep, stepInstructions, terminal, action, metadata, etc.
359
431
  */
360
- navigate({ taskFilePath, workflowType, result, description } = {}) {
432
+ navigate({ taskFilePath, workflowType, result, description, projectRoot } = {}) {
361
433
  let currentStep = null;
362
434
  let retryCount = 0;
363
435
 
@@ -417,7 +489,7 @@ export class WorkflowEngine {
417
489
  throw new Error(`First step '${firstEdge.to}' not found in workflow`);
418
490
  }
419
491
 
420
- return buildNavigateResponse(workflowType, firstEdge.to, firstStepDef, "start", false, 0, description);
492
+ return buildNavigateResponse(workflowType, firstEdge.to, firstStepDef, "start", false, 0, description, false, projectRoot);
421
493
  }
422
494
 
423
495
  // Case 2: currentStep but no result - return current state
@@ -427,7 +499,7 @@ export class WorkflowEngine {
427
499
  throw new Error(`Step '${currentStep}' not found in workflow '${workflowType}'`);
428
500
  }
429
501
 
430
- return buildNavigateResponse(workflowType, currentStep, stepDef, "current", false, retryCount, description);
502
+ return buildNavigateResponse(workflowType, currentStep, stepDef, "current", false, retryCount, description, false, projectRoot);
431
503
  }
432
504
 
433
505
  // Case 3: currentStep and result - advance to next step
@@ -447,9 +519,13 @@ export class WorkflowEngine {
447
519
  }
448
520
 
449
521
  // Determine action and whether retries incremented
522
+ const currentStepDef = nodes[currentStep];
523
+ const isHitlResume = getTerminalType(currentStepDef) === "hitl";
450
524
  const isRetry = evaluation.action === "retry";
451
525
  let action;
452
- if (isRetry) {
526
+ if (isHitlResume) {
527
+ action = "advance"; // Human fixed it → fresh advance, retryCount resets
528
+ } else if (isRetry) {
453
529
  action = "retry";
454
530
  } else if (getTerminalType(nextStepDef) === "hitl") {
455
531
  action = "escalate";
@@ -457,6 +533,10 @@ export class WorkflowEngine {
457
533
  action = "advance";
458
534
  }
459
535
 
536
+ // Only reset retryCount on genuine forward progress (conditional edge like on:"passed")
537
+ // Unconditional advances within retry loops (e.g., work → gate) preserve the count
538
+ const resetRetryCount = action === "advance" && evaluation.action === "conditional";
539
+
460
540
  const response = buildNavigateResponse(
461
541
  workflowType,
462
542
  evaluation.nextStep,
@@ -464,14 +544,29 @@ export class WorkflowEngine {
464
544
  action,
465
545
  isRetry,
466
546
  retryCount,
467
- description
547
+ description,
548
+ resetRetryCount,
549
+ projectRoot
468
550
  );
469
551
 
470
- // Write-through: persist state transition to task file
552
+ // Write-through: persist state transition and presentation to task file
471
553
  if (taskFilePath) {
472
554
  const task = readTaskFile(taskFilePath);
473
555
  if (task) {
556
+ const userDesc = task.metadata?.userDescription || "";
474
557
  task.metadata = { ...task.metadata, ...response.metadata };
558
+ task.subject = buildTaskSubject(
559
+ task.id, userDesc, response.metadata.workflowType,
560
+ response.currentStep, response.subagent, response.terminal,
561
+ response.maxRetries, response.metadata.retryCount
562
+ );
563
+ task.activeForm = buildTaskActiveForm(
564
+ response.stepInstructions?.name || response.currentStep,
565
+ response.subagent, response.terminal
566
+ );
567
+ if (response.orchestratorInstructions) {
568
+ task.description = response.orchestratorInstructions;
569
+ }
475
570
  writeFileSync(taskFilePath, JSON.stringify(task, null, 2));
476
571
  }
477
572
  }
package/index.js CHANGED
@@ -186,6 +186,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
186
186
  properties: {
187
187
  workflowType: { type: "string", description: "Workflow ID to visualize" },
188
188
  currentStep: { type: "string", description: "Optional: highlight this step" },
189
+ filePath: {
190
+ type: "string",
191
+ description: "Optional: absolute path to save the diagram. Defaults to .flow/diagrams/{workflowType}.md",
192
+ },
189
193
  },
190
194
  required: ["workflowType"],
191
195
  },
@@ -229,6 +233,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
229
233
  workflowType: args.workflowType,
230
234
  result: args.result,
231
235
  description: args.description,
236
+ projectRoot: PROJECT_ROOT,
232
237
  });
233
238
  return jsonResponse(result);
234
239
  }
@@ -264,11 +269,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
264
269
  const source = store.getSource(args.workflowType);
265
270
  const markdown = generateDiagram(wfDef, args.currentStep);
266
271
 
267
- // Save diagram to file
268
- if (!existsSync(DIAGRAMS_PATH)) {
269
- mkdirSync(DIAGRAMS_PATH, { recursive: true });
272
+ // Save diagram to file (use provided path or default)
273
+ const filePath = args.filePath || join(DIAGRAMS_PATH, `${args.workflowType}.md`);
274
+ const fileDir = dirname(filePath);
275
+ if (!existsSync(fileDir)) {
276
+ mkdirSync(fileDir, { recursive: true });
270
277
  }
271
- const filePath = join(DIAGRAMS_PATH, `${args.workflowType}.md`);
272
278
  writeFileSync(filePath, markdown);
273
279
 
274
280
  return jsonResponse({ savedTo: filePath, source });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leclabs/agent-flow-navigator-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server that navigates agents through DAG-based workflows",
5
5
  "license": "MIT",
6
6
  "author": "leclabs",
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
  /**