Agent skill
maestro-dev
Development workflow for maestroCLI itself. Encodes the hexagonal architecture pattern (port -> adapter -> use-case -> command -> MCP tool -> test) and project-specific conventions. Use when implementing new maestro features, adding CLI commands, extending the MCP server, creating new adapters, modifying ports, writing use-cases, or debugging maestro's own code. Also use when you need to understand how maestro's layers connect or where to put new code.
Install this agent skill to your Project
npx add-skill https://github.com/ReinaMacCredy/maestro/tree/main/.claude/skills/maestro-dev
SKILL.md
maestroCLI Development Workflow
Architecture
maestroCLI follows hexagonal architecture. Every feature touches the same layers in the same order:
commands/ --> usecases/ --> ports/ <-- adapters/
(CLI I/O) (rules) (interfaces) (implementations)
server/ --> usecases/ --> ports/ <-- adapters/
(MCP I/O) (rules) (interfaces) (implementations)
Commands and MCP server tools are thin I/O shells. Business logic lives in use-cases. Ports define what the system needs. Adapters provide it.
Adding a New Feature: Step by Step
1. Define or Extend the Port Interface (src/ports/)
Ports are TypeScript interfaces that describe what the system needs without saying how to provide it. If your feature needs new persistence or external interaction, define it here.
// src/ports/memory.ts
export interface MemoryPort {
write(feature: string, name: string, content: string): Promise<void>;
read(feature: string, name: string): Promise<string | undefined>;
list(feature: string): Promise<string[]>;
delete(feature: string, name: string): Promise<void>;
compile(feature: string): Promise<string>;
}
Rules:
- Ports are pure interfaces -- no implementation, no imports from adapters
- Method signatures use domain types, not framework types
- Every method returns a Promise (even if the current adapter is sync)
- One port per domain concern (tasks, features, plans, memory)
2. Implement the Adapter (src/adapters/)
Adapters implement port interfaces against concrete backends (filesystem, beads_rust, etc.).
// src/adapters/fs/memory.ts
export class FsMemoryAdapter implements MemoryPort {
constructor(private directory: string) {}
async write(feature: string, name: string, content: string): Promise<void> {
const dir = join(this.directory, '.maestro', 'features', feature, 'memory');
await ensureDir(dir);
const filePath = join(dir, `${name}.md`);
await writeFileAtomic(filePath, content);
}
// ... other methods
}
Rules:
- Adapter classes are named
Fs{Domain}Adapterfor filesystem,Br{Domain}Adapterfor beads_rust - Constructor takes
directory: string(project root) - Use
ensureDir()before writes,writeFileAtomic()for atomic I/O (temp + rename) - Path length: respect
MAX_PATH_LENGTH = 240 - Adapters live in
src/adapters/(flat files) orsrc/adapters/fs/(filesystem-specific)
3. Wire the Use-Case (src/usecases/)
Use-cases contain business logic. They receive ports via the services singleton and orchestrate operations.
// src/usecases/check-status.ts
export async function checkStatus(
featureAdapter: FeaturePort,
taskPort: TaskPort,
planAdapter: PlanPort,
memoryAdapter: MemoryPort,
directory: string,
featureName?: string,
): Promise<FeatureStatus> {
const feature = featureName
? await featureAdapter.get(featureName)
: await featureAdapter.getActive();
if (!feature) throw new MaestroError('No active feature', ['Run: maestro feature-active <name>']);
// ... orchestrate across ports
}
Rules:
- Use-cases are pure functions (no classes), exported from their own file
- Parameters are port interfaces, not adapter instances (testable)
- Throw
MaestroErrorwith actionable.hints[]array - One use-case per business operation
- Use-cases never import from
src/commands/orsrc/server/
4. Add the CLI Command (src/commands/<noun>/<verb>.ts)
Commands are organized as noun/verb directories using citty's defineCommand.
// src/commands/memory/write.ts
import { defineCommand } from 'citty';
import { getServices } from '../../services.ts';
import { output } from '../../lib/output.ts';
export default defineCommand({
meta: { name: 'memory-write', description: 'Write a memory file for a feature' },
args: {
feature: { type: 'string', required: true, description: 'Feature name' },
name: { type: 'string', required: true, description: 'Memory file name' },
content: { type: 'string', required: true, description: 'Content to write' },
json: { type: 'boolean', default: false, description: 'Output as JSON' },
},
async run({ args }) {
const { memoryAdapter } = getServices();
await memoryAdapter.write(args.feature, args.name, args.content);
output(args.json, { feature: args.feature, name: args.name }, (r) => [
`[ok] Wrote memory: ${r.name} for feature: ${r.feature}`,
]);
},
});
Rules:
- File path =
src/commands/{noun}/{verb}.ts(e.g.,memory/write.ts) - CLI name =
{noun}-{verb}(e.g.,memory-write) - Always include
jsonboolean arg for dual-mode output - Use
getServices()to access ports -- never instantiate adapters directly - Use
output(isJson, data, textFormatter)for all output - Error handling: let
MaestroErrorpropagate -- the root command catches it
5. Register the MCP Server Tool (src/server/)
CLI commands are the primary surface. Each command calls a shared use-case function.
// In src/surfaces/cli/commands/memory-write.ts
export const memoryWrite = defineCommand({
name: 'memory-write',
description: 'Write a memory file for a feature',
args: {
feature: { type: 'string', describe: 'Feature name' },
name: { type: 'string', describe: 'Memory file name', required: true },
file: { type: 'string', describe: 'Path to content file', required: true },
},
}, async (args, services) => {
const content = readFileSync(args.file, 'utf-8');
await services.memoryAdapter.write(args.feature, args.name, content);
return { path: `Written: ${args.name}` };
});
Rules:
- Command name:
kebab-case(hyphens, not underscores) - Args use yargs-style type declarations
- All commands accept
--jsonfor structured output - Share the same use-case logic as the CLI command
6. Add Tests (src/__tests__/)
// src/__tests__/unit/memory.test.ts
import { describe, it, expect } from 'bun:test';
describe('FsMemoryAdapter', () => {
it('writes and reads a memory file', async () => {
const adapter = new FsMemoryAdapter(tmpDir);
await adapter.write('my-feature', 'notes', 'hello world');
const content = await adapter.read('my-feature', 'notes');
expect(content).toBe('hello world');
});
});
Rules:
- Unit tests in
src/__tests__/unit/-- test adapters and use-cases - Integration tests in
src/__tests__/integration/-- test CLI commands - Use
bun:test(describe, it, expect) - Create temp directories for filesystem tests
- Mock external dependencies, not internal modules
7. Build and Verify
bun run build # Runs generators (skills registry, command registry) + bundles
bun test src/ # Runs all tests
Build must pass before proceeding. Tests must pass before committing.
Service Wiring
All ports are wired in src/services.ts via a module-level singleton:
// Root command calls this once
initServices(directory);
// All commands and MCP tools call this
const { taskPort, memoryAdapter, featureAdapter, planAdapter } = getServices();
The task backend is selected by config: configAdapter.get().taskBackend chooses between FsTaskAdapter (default) and BrTaskAdapter.
Error Handling Pattern
throw new MaestroError(
'Feature not found', // Title
['Run: maestro feature-list to see available features'] // Actionable hints
);
Hints are printed line-by-line after the error title. Every error should tell the user what to do next.
State Machine Pattern
Task and feature states use explicit transition maps:
const VALID_TRANSITIONS: Record<TaskStatus, TaskStatus[]> = {
open: ['claimed'],
claimed: ['done', 'blocked'],
blocked: ['open'],
done: [],
};
Always validate transitions before applying them. Invalid transitions throw MaestroError.
Anti-Patterns
- Do not import adapters in commands -- use
getServices() - Do not put business logic in commands -- extract to use-cases
- Do not use
console.log-- useoutput()for dual-mode JSON/text - Do not write files without
ensureDir()first - Do not create new ports when an existing one can be extended
- Do not skip the build step -- generators produce required files
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
maestro-skill-author
Create, update, or debug maestro built-in skills. Covers SKILL.md frontmatter, reference directory structure, step-file architecture, build-time embedding, naming conventions, alias management, and registry validation. Use when creating a new maestro built-in skill, modifying an existing SKILL.md, adding reference files, debugging skill loading failures, updating the skills registry, or working on the skills full port. Also use when frontmatter validation fails, skills don't appear in skill-list, or reference files fail to load.
maestro:brainstorming
Use before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.
mcp-builder
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
maestro:plan-review-loop
Deep-review any plan (maestro, Codex, Claude Code plan mode, or plain markdown) using iterative subagent review loops with BMAD-inspired adversarial edge-case discovery. Spawns reviewer subagents that find issues using pre-mortem, inversion, and red-team techniques, auto-fixes them with structured fix strategies, and re-reviews until the plan passes with zero actionable issues. Use when the user says 'review the plan', 'deep review', 'check the plan thoroughly', 'review loop', 'validate before approving', or wants rigorous plan validation before execution. Also use proactively before plan-approve when the plan is complex or high-risk.
maestro:research
Structured research workflow for maestro features. Guides tool selection across three tiers (codebase exploration, Context7 for library docs, NotebookLM for deep analysis), defines research patterns, finding organization via memory_write, and completion criteria. Use during the research pipeline stage after feature_create and before plan_write. Also use when investigating a problem space, comparing technical approaches, gathering context on unfamiliar code, or needing to understand external library APIs before making architectural decisions.
cli-for-agents
Designs or reviews CLIs so coding agents can run them reliably: non-interactive flags, layered --help with examples, stdin/pipelines, fast actionable errors, idempotency, dry-run, and predictable structure. Use when building a CLI, adding commands, writing --help, or when the user mentions agents, terminals, or automation-friendly CLIs.
Didn't find tool you were looking for?