@leclabs/agent-flow-navigator-mcp 1.3.0 → 1.4.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.
Files changed (5) hide show
  1. package/engine.js +106 -21
  2. package/index.js +149 -55
  3. package/package.json +1 -1
  4. package/store.js +32 -4
  5. package/types.d.ts +37 -0
package/engine.js CHANGED
@@ -77,7 +77,16 @@ const WORKFLOW_EMOJIS = {
77
77
  /**
78
78
  * Build formatted task subject for write-through
79
79
  */
80
- export function buildTaskSubject(taskId, userDescription, workflowType, stepId, subagent, terminal, maxRetries, retryCount) {
80
+ export function buildTaskSubject(
81
+ taskId,
82
+ userDescription,
83
+ workflowType,
84
+ stepId,
85
+ subagent,
86
+ terminal,
87
+ maxRetries,
88
+ retryCount
89
+ ) {
81
90
  const emoji = WORKFLOW_EMOJIS[workflowType] || "";
82
91
  const line1 = `#${taskId} ${userDescription}${emoji ? ` ${emoji}` : ""}`;
83
92
 
@@ -118,7 +127,13 @@ export function getBaselineInstructions(stepId, stepName) {
118
127
  }
119
128
 
120
129
  // Analysis/Requirements steps
121
- if (id.includes("analyze") || id.includes("analysis") || id.includes("parse") || id.includes("requirements") || name.includes("analyze")) {
130
+ if (
131
+ id.includes("analyze") ||
132
+ id.includes("analysis") ||
133
+ id.includes("parse") ||
134
+ id.includes("requirements") ||
135
+ name.includes("analyze")
136
+ ) {
122
137
  return "Review the task requirements carefully. Identify key constraints, dependencies, and acceptance criteria. Create a clear plan before proceeding.";
123
138
  }
124
139
  if (id.includes("plan") || id.includes("design") || name.includes("plan")) {
@@ -173,13 +188,44 @@ export function getBaselineInstructions(stepId, stepName) {
173
188
  return "Complete this step thoroughly. Document your findings and any decisions made.";
174
189
  }
175
190
 
191
+ /**
192
+ * Resolve a context_files entry to an absolute path.
193
+ *
194
+ * Convention (follows Claude Code plugin path rules):
195
+ * - "./path" → relative to the workflow's source root (plugin root, project root, etc.)
196
+ * - "path" → relative to projectRoot
197
+ *
198
+ * @param {string} file - Context file entry
199
+ * @param {string} projectRoot - Project root directory
200
+ * @param {string|null} sourceRoot - Root directory of the workflow's source
201
+ * @returns {string} Absolute file path
202
+ */
203
+ export function resolveContextFile(file, projectRoot, sourceRoot) {
204
+ if (file.startsWith("./") && sourceRoot) {
205
+ return join(sourceRoot, file);
206
+ }
207
+ return join(projectRoot, file);
208
+ }
209
+
210
+ /**
211
+ * Resolve ./ prefixed paths in prose text against sourceRoot.
212
+ * Leaves text unchanged when sourceRoot is null or text has no ./ references.
213
+ * @param {string|null} text - Prose text that may contain ./ paths
214
+ * @param {string|null} sourceRoot - Root directory for ./ resolution
215
+ * @returns {string|null} Text with ./ paths resolved to absolute paths
216
+ */
217
+ export function resolveProseRefs(text, sourceRoot) {
218
+ if (!text || !sourceRoot) return text;
219
+ return text.replace(/\.\/[\w\-./]+/g, (match) => join(sourceRoot, match));
220
+ }
221
+
176
222
  /**
177
223
  * Build context loading instructions from step-level context_files.
178
224
  * Returns a markdown section or null if no context declared.
179
225
  */
180
- export function buildContextInstructions({ contextFiles, projectRoot }) {
226
+ export function buildContextInstructions({ contextFiles, projectRoot, sourceRoot }) {
181
227
  if (!contextFiles?.length || !projectRoot) return null;
182
- const lines = contextFiles.map((file) => `- Read file: ${join(projectRoot, file)}`);
228
+ const lines = contextFiles.map((file) => `- Read file: ${resolveContextFile(file, projectRoot, sourceRoot)}`);
183
229
  return `## Context\n\nBefore beginning, load the following:\n${lines.join("\n")}`;
184
230
  }
185
231
 
@@ -187,7 +233,15 @@ export function buildContextInstructions({ contextFiles, projectRoot }) {
187
233
  * Build orchestrator instructions for task creation/update
188
234
  * Returns null for terminal nodes (no further work)
189
235
  */
190
- function buildOrchestratorInstructions(workflowType, stepId, stage, subagent, stepInstructions, description, contextBlock) {
236
+ function buildOrchestratorInstructions(
237
+ workflowType,
238
+ stepId,
239
+ stage,
240
+ subagent,
241
+ stepInstructions,
242
+ description,
243
+ contextBlock
244
+ ) {
191
245
  if (!stepInstructions) return null; // Terminal nodes have no instructions
192
246
 
193
247
  const delegationPrefix = subagent ? `Invoke ${subagent} to complete the following task: ` : "";
@@ -212,25 +266,27 @@ function buildNavigateResponse(
212
266
  retryCount = 0,
213
267
  description = null,
214
268
  resetRetryCount = false,
215
- projectRoot = null
269
+ projectRoot = null,
270
+ sourceRoot = null
216
271
  ) {
217
272
  const stage = stepDef.stage || null;
218
273
  const subagent = stepDef.agent ? toSubagentRef(stepDef.agent) : null;
219
274
 
220
275
  // Build step instructions from workflow definition + baseline
276
+ // Resolve ./ paths in prose fields against sourceRoot (same convention as context_files)
221
277
  const isTerminal = isTerminalNode(stepDef);
222
278
  const stepInstructions = isTerminal
223
279
  ? null
224
280
  : {
225
281
  name: stepDef.name || stepId,
226
- description: stepDef.description || null,
227
- guidance: stepDef.instructions || getBaselineInstructions(stepId, stepDef.name),
282
+ description: resolveProseRefs(stepDef.description, sourceRoot) || null,
283
+ guidance: resolveProseRefs(stepDef.instructions, sourceRoot) || getBaselineInstructions(stepId, stepDef.name),
228
284
  };
229
285
 
230
286
  // Build context block from step-level context_files
231
287
  const contextBlock = isTerminal
232
288
  ? null
233
- : buildContextInstructions({ contextFiles: stepDef.context_files, projectRoot });
289
+ : buildContextInstructions({ contextFiles: stepDef.context_files, projectRoot, sourceRoot });
234
290
 
235
291
  // Build orchestrator instructions for all non-terminal actions
236
292
  const orchestratorInstructions = isTerminal
@@ -243,11 +299,7 @@ function buildNavigateResponse(
243
299
  const metadata = {
244
300
  workflowType,
245
301
  currentStep: stepId,
246
- retryCount: retriesIncremented
247
- ? retryCount + 1
248
- : action === "start" || resetRetryCount
249
- ? 0
250
- : retryCount,
302
+ retryCount: retriesIncremented ? retryCount + 1 : action === "start" || resetRetryCount ? 0 : retryCount,
251
303
  };
252
304
 
253
305
  return {
@@ -261,6 +313,7 @@ function buildNavigateResponse(
261
313
  maxRetries: stepDef.maxRetries || 0,
262
314
  orchestratorInstructions,
263
315
  metadata,
316
+ sourceRoot,
264
317
  };
265
318
  }
266
319
 
@@ -469,6 +522,9 @@ export class WorkflowEngine {
469
522
  throw new Error(`Workflow '${workflowType}' must have nodes`);
470
523
  }
471
524
 
525
+ // Resolve source root for context_files with ./ prefix
526
+ const sourceRoot = this.store.getSourceRoot?.(workflowType) || null;
527
+
472
528
  const { nodes } = wfDef;
473
529
 
474
530
  // Case 1: No currentStep - start at first work step
@@ -489,7 +545,18 @@ export class WorkflowEngine {
489
545
  throw new Error(`First step '${firstEdge.to}' not found in workflow`);
490
546
  }
491
547
 
492
- return buildNavigateResponse(workflowType, firstEdge.to, firstStepDef, "start", false, 0, description, false, projectRoot);
548
+ return buildNavigateResponse(
549
+ workflowType,
550
+ firstEdge.to,
551
+ firstStepDef,
552
+ "start",
553
+ false,
554
+ 0,
555
+ description,
556
+ false,
557
+ projectRoot,
558
+ sourceRoot
559
+ );
493
560
  }
494
561
 
495
562
  // Case 2: currentStep but no result - return current state
@@ -499,7 +566,18 @@ export class WorkflowEngine {
499
566
  throw new Error(`Step '${currentStep}' not found in workflow '${workflowType}'`);
500
567
  }
501
568
 
502
- return buildNavigateResponse(workflowType, currentStep, stepDef, "current", false, retryCount, description, false, projectRoot);
569
+ return buildNavigateResponse(
570
+ workflowType,
571
+ currentStep,
572
+ stepDef,
573
+ "current",
574
+ false,
575
+ retryCount,
576
+ description,
577
+ false,
578
+ projectRoot,
579
+ sourceRoot
580
+ );
503
581
  }
504
582
 
505
583
  // Case 3: currentStep and result - advance to next step
@@ -546,7 +624,8 @@ export class WorkflowEngine {
546
624
  retryCount,
547
625
  description,
548
626
  resetRetryCount,
549
- projectRoot
627
+ projectRoot,
628
+ sourceRoot
550
629
  );
551
630
 
552
631
  // Write-through: persist state transition and presentation to task file
@@ -556,13 +635,19 @@ export class WorkflowEngine {
556
635
  const userDesc = task.metadata?.userDescription || "";
557
636
  task.metadata = { ...task.metadata, ...response.metadata };
558
637
  task.subject = buildTaskSubject(
559
- task.id, userDesc, response.metadata.workflowType,
560
- response.currentStep, response.subagent, response.terminal,
561
- response.maxRetries, response.metadata.retryCount
638
+ task.id,
639
+ userDesc,
640
+ response.metadata.workflowType,
641
+ response.currentStep,
642
+ response.subagent,
643
+ response.terminal,
644
+ response.maxRetries,
645
+ response.metadata.retryCount
562
646
  );
563
647
  task.activeForm = buildTaskActiveForm(
564
648
  response.stepInstructions?.name || response.currentStep,
565
- response.subagent, response.terminal
649
+ response.subagent,
650
+ response.terminal
566
651
  );
567
652
  if (response.orchestratorInstructions) {
568
653
  task.description = response.orchestratorInstructions;
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
 
@@ -45,30 +46,25 @@ const store = new WorkflowStore();
45
46
  const engine = new WorkflowEngine(store);
46
47
 
47
48
  /**
48
- * Load workflows from project directory structure: {id}/workflow.json
49
+ * Load workflows from catalog: flat {id}.json files
49
50
  */
50
- function loadProjectWorkflows(dirPath) {
51
+ function loadCatalogWorkflows(dirPath) {
51
52
  if (!existsSync(dirPath)) return [];
52
53
 
53
54
  const loaded = [];
54
- const entries = readdirSync(dirPath, { withFileTypes: true });
55
-
56
- for (const entry of entries) {
57
- if (!entry.isDirectory()) continue;
58
-
59
- const id = entry.name;
60
- const workflowFile = join(dirPath, id, "workflow.json");
55
+ const files = readdirSync(dirPath).filter((f) => f.endsWith(".json"));
61
56
 
62
- if (!existsSync(workflowFile)) continue;
57
+ for (const file of files) {
58
+ const id = file.replace(".json", "");
63
59
 
64
60
  try {
65
- const content = JSON.parse(readFileSync(workflowFile, "utf-8"));
61
+ const content = JSON.parse(readFileSync(join(dirPath, file), "utf-8"));
66
62
  if (validateWorkflow(id, content)) {
67
- store.loadDefinition(id, content, "project");
63
+ store.loadDefinition(id, content, "catalog");
68
64
  loaded.push(id);
69
65
  }
70
66
  } catch (e) {
71
- console.error(`Error loading workflow ${id}: ${e.message}`);
67
+ console.error(`Error loading catalog workflow ${id}: ${e.message}`);
72
68
  }
73
69
  }
74
70
 
@@ -76,25 +72,48 @@ function loadProjectWorkflows(dirPath) {
76
72
  }
77
73
 
78
74
  /**
79
- * Load workflows from catalog: flat {id}.json files
75
+ * Load external workflows from a directory: flat {id}.json files
76
+ * Used by the LoadWorkflows tool at runtime.
77
+ * @param {string} dirPath - Directory containing {id}.json workflow files
78
+ * @param {string} sourceRoot - Root path for resolving ./ context_files
79
+ * @returns {string[]} Array of loaded workflow IDs
80
80
  */
81
- function loadCatalogWorkflows(dirPath) {
81
+ function loadExternalWorkflows(dirPath, sourceRoot) {
82
82
  if (!existsSync(dirPath)) return [];
83
83
 
84
84
  const loaded = [];
85
- const files = readdirSync(dirPath).filter((f) => f.endsWith(".json"));
85
+ const entries = readdirSync(dirPath, { withFileTypes: true });
86
86
 
87
- for (const file of files) {
88
- const id = file.replace(".json", "");
87
+ for (const entry of entries) {
88
+ // Flat format: {id}.json
89
+ if (entry.isFile() && entry.name.endsWith(".json")) {
90
+ const id = entry.name.replace(".json", "");
91
+ try {
92
+ const content = JSON.parse(readFileSync(join(dirPath, entry.name), "utf-8"));
93
+ if (validateWorkflow(id, content)) {
94
+ store.loadDefinition(id, content, "external", sourceRoot);
95
+ loaded.push(id);
96
+ }
97
+ } catch (e) {
98
+ console.error(`Error loading external workflow ${id}: ${e.message}`);
99
+ }
100
+ continue;
101
+ }
89
102
 
90
- try {
91
- const content = JSON.parse(readFileSync(join(dirPath, file), "utf-8"));
92
- if (validateWorkflow(id, content)) {
93
- store.loadDefinition(id, content, "catalog");
94
- loaded.push(id);
103
+ // Directory format: {id}/workflow.json
104
+ if (entry.isDirectory()) {
105
+ const wfFile = join(dirPath, entry.name, "workflow.json");
106
+ if (!existsSync(wfFile)) continue;
107
+ const id = entry.name;
108
+ try {
109
+ const content = JSON.parse(readFileSync(wfFile, "utf-8"));
110
+ if (validateWorkflow(id, content)) {
111
+ store.loadDefinition(id, content, "external", sourceRoot);
112
+ loaded.push(id);
113
+ }
114
+ } catch (e) {
115
+ console.error(`Error loading external workflow ${id}: ${e.message}`);
95
116
  }
96
- } catch (e) {
97
- console.error(`Error loading catalog workflow ${id}: ${e.message}`);
98
117
  }
99
118
  }
100
119
 
@@ -108,16 +127,9 @@ function loadWorkflows() {
108
127
  const catalogPath = join(CATALOG_PATH, "workflows");
109
128
  const catalogLoaded = loadCatalogWorkflows(catalogPath);
110
129
 
111
- const projectLoaded = existsSync(WORKFLOWS_PATH) ? loadProjectWorkflows(WORKFLOWS_PATH) : [];
112
-
113
- // Determine which IDs came from where (project overwrites catalog)
114
- const fromCatalog = catalogLoaded.filter((id) => !projectLoaded.includes(id));
115
- const fromProject = projectLoaded;
116
-
117
130
  return {
118
- catalog: fromCatalog,
119
- project: fromProject,
120
- loaded: [...new Set([...catalogLoaded, ...projectLoaded])],
131
+ catalog: catalogLoaded,
132
+ loaded: catalogLoaded,
121
133
  };
122
134
  }
123
135
 
@@ -209,6 +221,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
209
221
  },
210
222
  },
211
223
  },
224
+ {
225
+ name: "LoadWorkflows",
226
+ description:
227
+ "Load workflows at runtime. External plugins pass path + sourceRoot. For project workflows, pass workflowIds to load specific workflows from .flow/workflows/.",
228
+ inputSchema: {
229
+ type: "object",
230
+ properties: {
231
+ path: {
232
+ type: "string",
233
+ description: "Directory containing {id}.json workflow files. For external plugins only.",
234
+ },
235
+ sourceRoot: {
236
+ type: "string",
237
+ description:
238
+ "Root path for resolving ./ context_files entries. Defaults to path. For external plugins only.",
239
+ },
240
+ workflowIds: {
241
+ type: "array",
242
+ items: { type: "string" },
243
+ description:
244
+ "Specific workflow IDs to load from .flow/workflows/. Required when loading project workflows (no path). Omit to list available workflows without loading.",
245
+ },
246
+ },
247
+ },
248
+ },
212
249
  {
213
250
  name: "ListCatalog",
214
251
  description: "List workflows available in the catalog.",
@@ -337,6 +374,81 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
337
374
  });
338
375
  }
339
376
 
377
+ case "LoadWorkflows": {
378
+ if (args.path) {
379
+ // External plugin: load all from provided path
380
+ const dirPath = resolve(args.path);
381
+ const root = args.sourceRoot ? resolve(args.sourceRoot) : dirPath;
382
+ const loaded = loadExternalWorkflows(dirPath, root);
383
+ return jsonResponse({
384
+ schemaVersion: 2,
385
+ loaded,
386
+ source: "external",
387
+ sourceRoot: root,
388
+ path: dirPath,
389
+ });
390
+ }
391
+
392
+ // Project: require explicit workflowIds or list available
393
+ if (!existsSync(WORKFLOWS_PATH)) {
394
+ return jsonResponse({
395
+ schemaVersion: 2,
396
+ available: [],
397
+ loaded: [],
398
+ source: "project",
399
+ path: WORKFLOWS_PATH,
400
+ hint: "No .flow/workflows/ directory found. Use /flow:init to set up workflows.",
401
+ });
402
+ }
403
+
404
+ const available = readdirSync(WORKFLOWS_PATH, { withFileTypes: true })
405
+ .filter((e) => e.isDirectory() && existsSync(join(WORKFLOWS_PATH, e.name, "workflow.json")))
406
+ .map((e) => e.name);
407
+
408
+ if (!args.workflowIds || args.workflowIds.length === 0) {
409
+ // List only — don't load
410
+ return jsonResponse({
411
+ schemaVersion: 2,
412
+ available,
413
+ loaded: [],
414
+ source: "project",
415
+ path: WORKFLOWS_PATH,
416
+ hint: "Pass workflowIds to load specific workflows.",
417
+ });
418
+ }
419
+
420
+ // Load only the requested workflows
421
+ const loaded = [];
422
+ const errors = [];
423
+ for (const id of args.workflowIds) {
424
+ if (!available.includes(id)) {
425
+ errors.push({ id, error: "not found in .flow/workflows/" });
426
+ continue;
427
+ }
428
+ const wfFile = join(WORKFLOWS_PATH, id, "workflow.json");
429
+ try {
430
+ const content = JSON.parse(readFileSync(wfFile, "utf-8"));
431
+ if (validateWorkflow(id, content)) {
432
+ store.loadDefinition(id, content, "project");
433
+ loaded.push(id);
434
+ } else {
435
+ errors.push({ id, error: "invalid schema" });
436
+ }
437
+ } catch (e) {
438
+ errors.push({ id, error: e.message });
439
+ }
440
+ }
441
+
442
+ return jsonResponse({
443
+ schemaVersion: 2,
444
+ available,
445
+ loaded,
446
+ errors: errors.length > 0 ? errors : undefined,
447
+ source: "project",
448
+ path: WORKFLOWS_PATH,
449
+ });
450
+ }
451
+
340
452
  case "ListCatalog": {
341
453
  const catalogPath = join(CATALOG_PATH, "workflows");
342
454
  if (!existsSync(catalogPath)) {
@@ -371,31 +483,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
371
483
  }
372
484
  });
373
485
 
374
- /**
375
- * Ensure .flow/README.md exists (created on MCP server connect)
376
- */
377
- function ensureFlowReadme() {
378
- const readmePath = join(FLOW_PATH, "README.md");
379
- if (!existsSync(readmePath)) {
380
- mkdirSync(FLOW_PATH, { recursive: true });
381
- writeFileSync(readmePath, generateFlowReadme());
382
- console.error(` Created: ${readmePath}`);
383
- }
384
- }
385
-
386
- // Load workflows and start server
486
+ // Load catalog workflows and start server
387
487
  const workflowInfo = loadWorkflows();
388
- ensureFlowReadme();
389
488
 
390
489
  const transport = new StdioServerTransport();
391
490
  await server.connect(transport);
392
491
 
393
492
  console.error(`Navigator MCP Server v2 running (stateless)`);
394
493
  console.error(` Project: ${PROJECT_ROOT}`);
395
- console.error(` Workflows: ${workflowInfo.loaded.length} total`);
396
- if (workflowInfo.catalog.length > 0) {
397
- console.error(` Catalog: ${workflowInfo.catalog.join(", ")}`);
398
- }
399
- if (workflowInfo.project.length > 0) {
400
- console.error(` Project: ${workflowInfo.project.join(", ")}`);
401
- }
494
+ console.error(` Catalog: ${workflowInfo.catalog.length} workflows`);
495
+ console.error(` Project/external workflows: load via LoadWorkflows tool`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leclabs/agent-flow-navigator-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "MCP server that navigates agents through DAG-based workflows",
5
5
  "license": "MIT",
6
6
  "author": "leclabs",
package/store.js CHANGED
@@ -29,19 +29,26 @@ 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
+ } else {
50
+ this.sourceRoots.delete(id);
51
+ }
45
52
  return id;
46
53
  }
47
54
 
@@ -56,7 +63,7 @@ export class WorkflowStore {
56
63
 
57
64
  /**
58
65
  * List all loaded workflows with metadata
59
- * @param {string} filter - Filter by source: "all" | "project" | "catalog"
66
+ * @param {string} filter - Filter by source: "all" | "project" | "catalog" | "external"
60
67
  * @returns {Array} Array of workflow summaries
61
68
  */
62
69
  listWorkflows(filter = "all") {
@@ -86,6 +93,17 @@ export class WorkflowStore {
86
93
  return false;
87
94
  }
88
95
 
96
+ /**
97
+ * Check if any external workflows exist
98
+ * @returns {boolean}
99
+ */
100
+ hasExternalWorkflows() {
101
+ for (const source of this.sources.values()) {
102
+ if (source === "external") return true;
103
+ }
104
+ return false;
105
+ }
106
+
89
107
  /**
90
108
  * Get the source of a workflow
91
109
  * @param {string} id - Workflow identifier
@@ -95,6 +113,15 @@ export class WorkflowStore {
95
113
  return this.sources.get(id);
96
114
  }
97
115
 
116
+ /**
117
+ * Get the source root path for a workflow (for ./ context_files resolution)
118
+ * @param {string} id - Workflow identifier
119
+ * @returns {string|undefined} Source root path or undefined
120
+ */
121
+ getSourceRoot(id) {
122
+ return this.sourceRoots.get(id);
123
+ }
124
+
98
125
  /**
99
126
  * Check if a workflow exists
100
127
  * @param {string} id - Workflow identifier
@@ -110,6 +137,7 @@ export class WorkflowStore {
110
137
  clear() {
111
138
  this.workflows.clear();
112
139
  this.sources.clear();
140
+ this.sourceRoots.clear();
113
141
  }
114
142
 
115
143
  /**
package/types.d.ts CHANGED
@@ -133,3 +133,40 @@ export type EdgeAction =
133
133
  | "escalate" // Failed and exceeded retry limit
134
134
  | "no_outgoing_edges" // Terminal node (no edges)
135
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(file: string, projectRoot: string, sourceRoot?: string | null): string;
159
+
160
+ export function resolveProseRefs(text: string | null, sourceRoot: string | null): string | null;
161
+
162
+ export function buildContextInstructions(options: {
163
+ contextFiles?: string[];
164
+ projectRoot?: string | null;
165
+ sourceRoot?: string | null;
166
+ }): string | null;
167
+
168
+ // =============================================================================
169
+ // Store Types
170
+ // =============================================================================
171
+
172
+ export type WorkflowSource = "catalog" | "project" | "external";