Phase 3: Dynamic Workflow Engine - VM sandbox (node:vm) with agent/parallel/pipeline API, Claude Code compatible - Workflow file discovery (.boocode/workflows/*.js + ~/.boocode/workflows/*.js) - Workflow manager with session/chat creation and inference dispatch - Built-in catalog: deep-research, review-code, find-issues - Resumability cache: SHA-256 hash of agent spec, in-memory Map Phase 4: Background Subagents - background-task.ts service: spawn/poll/cancel lifecycle - spawn_subagent, subagent_status, subagent_result tools in ALL_TOOLS Phase 5: Multi-modal + Cache Shape - Multi-modal stub with type defs and hook point in payload.ts - CacheShapeBadge component in trace viewer (colored bar + %)
135 lines
4.1 KiB
TypeScript
135 lines
4.1 KiB
TypeScript
// v2.8.0: Workflow file discovery — walks project-local and global workflow
|
|
// directories to find runnable scripts. Built-in workflows from the catalog
|
|
// are merged into the results (they take precedence over user-defined files).
|
|
// All functions exported for testing.
|
|
|
|
import { readdirSync, existsSync } from 'node:fs';
|
|
import { join, basename, extname } from 'node:path';
|
|
import { homedir } from 'node:os';
|
|
import { getBuiltinWorkflows, getBuiltinWorkflow } from './catalog.js';
|
|
|
|
/**
|
|
* Sentinel prefix used in `sourceFile` for built-in workflows from the
|
|
* catalog so callers (e.g. WorkflowManager) can detect and handle them
|
|
* by calling `generateScript()` instead of reading a file from disk.
|
|
*/
|
|
const BUILTIN_PREFIX = 'builtin:';
|
|
|
|
/**
|
|
* Metadata about a discovered workflow file (or built-in workflow).
|
|
*/
|
|
export interface WorkflowMeta {
|
|
/** Workflow name (file stem without .js extension). */
|
|
name: string;
|
|
/** Description loaded from the workflow module's `meta.description`.
|
|
* Empty string until loadWorkflowMeta() resolves it. */
|
|
description: string;
|
|
/** Absolute path to the .js file.
|
|
* For built-in workflows this is `'builtin:<name>'` — the caller
|
|
* should use `getBuiltinWorkflow(name)` and `generateScript()`
|
|
* instead of reading this path from disk. */
|
|
sourceFile: string;
|
|
}
|
|
|
|
/**
|
|
* Test whether a `WorkflowMeta.sourceFile` points to a built-in workflow
|
|
* (rather than a file on disk).
|
|
*
|
|
* @param meta - The workflow metadata to check.
|
|
*/
|
|
export function isBuiltinWorkflow(meta: WorkflowMeta): boolean {
|
|
return meta.sourceFile.startsWith(BUILTIN_PREFIX);
|
|
}
|
|
|
|
/**
|
|
* Find all workflow .js files in the standard search paths, merged with
|
|
* built-in workflows from the catalog.
|
|
*
|
|
* Priority order (first match wins for same-named workflows):
|
|
* 1. Built-in catalog (always takes precedence)
|
|
* 2. <projectRoot>/.boocode/workflows/ (project-local)
|
|
* 3. ~/.boocode/workflows/ (global, per-user)
|
|
*
|
|
* @param projectRoot - Absolute path to the current project root.
|
|
*/
|
|
export function discoverWorkflows(projectRoot: string): WorkflowMeta[] {
|
|
const seen = new Set<string>();
|
|
const results: WorkflowMeta[] = [];
|
|
|
|
// 1. Built-in workflows (highest priority)
|
|
for (const builtin of getBuiltinWorkflows()) {
|
|
seen.add(builtin.name);
|
|
results.push({
|
|
name: builtin.name,
|
|
description: builtin.description,
|
|
sourceFile: `${BUILTIN_PREFIX}${builtin.name}`,
|
|
});
|
|
}
|
|
|
|
// 2. Project-local + global file-based workflows
|
|
const dirs = [
|
|
join(projectRoot, '.boocode', 'workflows'),
|
|
join(homedir(), '.boocode', 'workflows'),
|
|
];
|
|
|
|
for (const dir of dirs) {
|
|
if (!existsSync(dir)) continue;
|
|
try {
|
|
const entries = readdirSync(dir);
|
|
for (const f of entries) {
|
|
if (!f.endsWith('.js')) continue;
|
|
const name = basename(f, '.js');
|
|
if (seen.has(name)) continue; // built-in shadows project-local,
|
|
// project-local shadows global
|
|
seen.add(name);
|
|
results.push({
|
|
name,
|
|
description: '',
|
|
sourceFile: join(dir, f),
|
|
});
|
|
}
|
|
} catch {
|
|
// Permission error on directory — skip silently
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Find a single workflow by name across built-in catalog and search paths.
|
|
*
|
|
* Priority: built-in > project-local > global.
|
|
*
|
|
* @param name - Workflow name (without .js extension).
|
|
* @param projectRoot - Absolute path to the current project root.
|
|
*/
|
|
export function findWorkflow(
|
|
name: string,
|
|
projectRoot: string,
|
|
): WorkflowMeta | undefined {
|
|
// Check built-in catalog first
|
|
const builtin = getBuiltinWorkflow(name);
|
|
if (builtin) {
|
|
return {
|
|
name: builtin.name,
|
|
description: builtin.description,
|
|
sourceFile: `${BUILTIN_PREFIX}${builtin.name}`,
|
|
};
|
|
}
|
|
|
|
// Fall back to file-based discovery
|
|
return discoverWorkflows(projectRoot).find((w) => w.name === name);
|
|
}
|
|
|
|
/**
|
|
* Validate a candidate workflow file path.
|
|
* Checks that the file exists and has a .js extension.
|
|
*
|
|
* @param filePath - Absolute path to check.
|
|
*/
|
|
export function isValidWorkflowPath(filePath: string): boolean {
|
|
return extname(filePath) === '.js' && existsSync(filePath);
|
|
}
|