Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
498 changes: 498 additions & 0 deletions .vibe/development-plan-refactor-configurable-default-domain.md

Large diffs are not rendered by default.

197 changes: 168 additions & 29 deletions packages/core/src/workflow-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,52 @@ import { ConfigManager } from './config-manager.js';

const logger = createLogger('WorkflowManager');

/**
* Domain descriptions for tool parameter metadata.
* These are exposed to the LLM via the load_workflows tool to help it
* discover and choose domains intelligently.
*
* Each description summarizes what the domain is suitable for,
* based on the actual workflow YAML descriptions.
*/
export const DOMAIN_DESCRIPTIONS: Record<string, string> = {
code: 'Day-to-day software engineering: features (epcc), test-driven development (tdd), bug fixes (bugfix, minor), greenfield projects (greenfield), large structured development (waterfall), and code reviews (pr-review)',
architecture:
'System understanding and planning: architectural decisions (adr), legacy system modernization (big-bang-conversion), API and boundary analysis (boundary-testing), business capability modeling (business-analysis), and progressive architecture discovery (c4-analysis)',
sdd: 'Specification-driven development: write detailed specs before coding — structured requirements, user stories, testability focus, and constitutional compliance gates for bugfixes, features, and greenfield projects',
'sdd-crowd':
'Multi-agent collaborative specification-driven development: role-based handoffs between business analysts (specify), architects (plan), and developers (implement) for coordinated distributed teams',
skilled:
'Skill-augmented development: explicit prompts to apply specialized expertise (architecture, coding, testing, application design) at each phase — for scenarios where best practices and domain expertise should be leveraged',
office:
'Content creation and communication: structured workflows for writing blog posts (discovery through distribution) and creating slide presentations (ideate through deliver)',
children:
'Educational game development for children ages 8-12: simplified, age-appropriate programming concepts with frequent positive reinforcement and incremental achievement',
};

/**
* Known domain names — use this constant to avoid duplicating the domain list.
* Import from '@codemcp/workflows-core' in plugin/tool handler code.
*/
export const KNOWN_DOMAIN_NAMES = Object.keys(DOMAIN_DESCRIPTIONS) as [
'code',
'architecture',
'sdd',
'sdd-crowd',
'skilled',
'office',
'children',
];

export interface WorkflowManagerOptions {
/**
* Default domains to use for workflow filtering.
* Takes precedence over all environment variables.
* Can be a comma-separated string or an array of domain names.
*/
defaultDomains?: string | string[];
}

export interface WorkflowInfo {
name: string;
displayName: string;
Expand All @@ -41,43 +87,88 @@ export class WorkflowManager {
private stateMachineLoader: StateMachineLoader;
private lastProjectPath: string | null = null; // Track last loaded project path
private enabledDomains: Set<string>;
private _defaultDomains: string | string[] | null = null; // Constructor override

constructor() {
constructor(options?: WorkflowManagerOptions) {
this.stateMachineLoader = new StateMachineLoader();
if (options?.defaultDomains !== undefined) {
this._defaultDomains = options.defaultDomains;
}
this.enabledDomains = this.parseEnabledDomains();
this.loadPredefinedWorkflows();
}

/**
* Parse enabled domains from environment variable.
* WORKFLOW_DOMAINS is the canonical name.
* VIBE_WORKFLOW_DOMAINS is supported as a legacy alias for backward compatibility.
* WORKFLOW_DOMAINS takes precedence when both are set.
* Parse enabled domains from environment variable with four-level precedence chain:
* 1. Constructor parameter `defaultDomains` (highest priority)
* 2. `WORKFLOW_DOMAINS` env var (canonical runtime configuration)
* 3. `DEFAULT_DOMAINS` env var (new: runtime default when canonical is unset)
* 4. `VIBE_WORKFLOW_DOMAINS` env var (legacy alias for backward compatibility)
* 5. Empty Set — final fallback: no filtering, all workflows load
*/
private parseEnabledDomains(): Set<string> {
// WORKFLOW_DOMAINS (canonical) takes precedence over VIBE_WORKFLOW_DOMAINS (legacy alias)
const domainsEnv =
process.env['WORKFLOW_DOMAINS'] || process.env['VIBE_WORKFLOW_DOMAINS'];
// 1. Constructor parameter (highest priority)
if (this._defaultDomains !== null) {
const domains = new Set(
Array.isArray(this._defaultDomains)
? this._defaultDomains
: this._defaultDomains
.split(',')
.map(d => d.trim())
.filter(d => d)
);
logger.debug('Using constructor default domains', {
domains: Array.from(domains),
});
return domains;
}

// 2. WORKFLOW_DOMAINS (canonical)
if (process.env['WORKFLOW_DOMAINS']) {
return this._parseDomainString(
process.env['WORKFLOW_DOMAINS'],
'WORKFLOW_DOMAINS'
);
}

if (!domainsEnv) {
logger.debug('No domain configuration found, using default: code');
return new Set(['code']);
// 3. DEFAULT_DOMAINS (new: runtime default)
if (process.env['DEFAULT_DOMAINS']) {
return this._parseDomainString(
process.env['DEFAULT_DOMAINS'],
'DEFAULT_DOMAINS'
);
}

// 4. VIBE_WORKFLOW_DOMAINS (legacy alias)
if (process.env['VIBE_WORKFLOW_DOMAINS']) {
return this._parseDomainString(
process.env['VIBE_WORKFLOW_DOMAINS'],
'VIBE_WORKFLOW_DOMAINS (legacy)'
);
}

// 5. Default — 'code' (maintains backward-compatible out-of-the-box behavior)
logger.debug('No domain configuration found, using default: code');
return new Set(['code']);
}

/**
* Parse a comma-separated domain string into a Set.
*/
private _parseDomainString(
domainString: string,
source: string
): Set<string> {
const domains = new Set(
domainsEnv
domainString
.split(',')
.map(d => d.trim())
.filter(d => d)
);

logger.debug('Parsed enabled domains', {
source: process.env['WORKFLOW_DOMAINS']
? 'WORKFLOW_DOMAINS'
: 'VIBE_WORKFLOW_DOMAINS (legacy)',
source,
domains: Array.from(domains),
});

return domains;
}

Expand Down Expand Up @@ -183,23 +274,67 @@ export class WorkflowManager {
}
}
/**
* Get all available workflows regardless of domain filtering
* Get all available workflows regardless of domain filtering.
* Uses DEFAULT_ALL_DOMAINS env var if set, otherwise falls back to all known domains.
*/
public getAllAvailableWorkflows(): WorkflowInfo[] {
// Create a temporary manager with all domains enabled
const originalEnv = process.env['WORKFLOW_DOMAINS'];
process.env['WORKFLOW_DOMAINS'] = 'code,architecture,office,sdd';
// Use constructor param instead of env var mutation for thread safety
const allDomains =
process.env['DEFAULT_ALL_DOMAINS'] ||
'code,architecture,office,sdd,sdd-crowd,skilled,children';
const tempManager = new WorkflowManager({ defaultDomains: allDomains });
return tempManager.getAvailableWorkflows();
}

try {
const tempManager = new WorkflowManager();
return tempManager.getAvailableWorkflows();
} finally {
if (originalEnv !== undefined) {
process.env['WORKFLOW_DOMAINS'] = originalEnv;
} else {
delete process.env['WORKFLOW_DOMAINS'];
/**
* Replace the current domain set and reload workflows.
*
* This allows runtime switching of domains without recreating the WorkflowManager.
* Validates domains against known set.
*
* NOTE: Active workflow conflict detection is not yet implemented (see getActiveWorkflow).
* Callers can switch domains freely regardless of active workflow state.
*
* @param domains - Comma-separated string or array of domain names.
* Pass an empty string or empty array to load ALL workflows (no domain filtering).
* To reset to the default domain, pass 'code' explicitly.
* @throws Error if an unknown domain is provided
*/
public setDomains(domains: string | string[]): void {
const newSet = new Set(
Array.isArray(domains)
? domains
: domains
.split(',')
.map(d => d.trim())
.filter(d => d)
);

// Validate domains against known set
const knownDomains = new Set(Object.keys(DOMAIN_DESCRIPTIONS));
for (const domain of newSet) {
if (!knownDomains.has(domain)) {
throw new Error(
`Unknown domain: '${domain}'. Known domains: ${Array.from(knownDomains).join(', ')}`
);
}
}

// NOTE: Active workflow conflict detection is not yet implemented.
// getActiveWorkflow() always returns null until ConversationManager integration is complete.
// Callers can switch domains freely regardless of active workflow state.

// Update and reload
this.enabledDomains = newSet;
this.loadPredefinedWorkflows();
if (this.lastProjectPath) {
this.loadProjectWorkflows(this.lastProjectPath);
}

logger.info('Domains updated', {
domains: Array.from(newSet),
totalWorkflows: this.predefinedWorkflows.size,
});
}

public getAvailableWorkflows(): WorkflowInfo[] {
Expand Down Expand Up @@ -525,6 +660,10 @@ export class WorkflowManager {
*/
private loadPredefinedWorkflows(): void {
try {
// Clear existing workflows before reloading (important for setDomains)
this.predefinedWorkflows.clear();
this.workflowInfos.clear();

const workflowsDir = this.findWorkflowsDirectory();

if (!workflowsDir || !fs.existsSync(workflowsDir)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/test/unit/workflow-domain-filtering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ describe('Workflow Domain Filtering', () => {
}
});

it('should load only code workflows when no domain filter is set', () => {
it('should load only code workflows when no domain filter is set (default: code)', () => {
delete process.env.WORKFLOW_DOMAINS;

const manager = new WorkflowManager();
const workflows = manager.getAvailableWorkflows();

// Should only include code domain workflows and workflows without domain
// Default is 'code' — should only include code domain workflows
const codeWorkflows = workflows.filter(
w => !w.metadata?.domain || w.metadata.domain === 'code'
);
Expand Down
Loading
Loading