mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: add templates for architecture documents, epics, product briefs, and requirements PRD
- Introduced architecture document template for Phase 4, including structure and individual ADR records. - Added epics & stories template for Phase 5, detailing epic breakdown and dependencies. - Created product brief template for Phase 2, summarizing product vision, problem statement, and target users. - Developed requirements PRD template for Phase 3, outlining functional and non-functional requirements with traceability. - Implemented spec command for project spec management with subcommands for loading, listing, rebuilding, and initializing specs.
This commit is contained in:
@@ -12,6 +12,7 @@ import { cliCommand } from './commands/cli.js';
|
||||
import { memoryCommand } from './commands/memory.js';
|
||||
import { coreMemoryCommand } from './commands/core-memory.js';
|
||||
import { hookCommand } from './commands/hook.js';
|
||||
import { specCommand } from './commands/spec.js';
|
||||
import { issueCommand } from './commands/issue.js';
|
||||
import { workflowCommand } from './commands/workflow.js';
|
||||
import { loopCommand } from './commands/loop.js';
|
||||
@@ -297,6 +298,16 @@ export function run(argv: string[]): void {
|
||||
.option('--limit <n>', 'Max entries to return (for project-state)')
|
||||
.action((subcommand, args, options) => hookCommand(subcommand, args, options));
|
||||
|
||||
// Spec command - Project spec management (load/list/rebuild/status/init)
|
||||
program
|
||||
.command('spec [subcommand] [args...]')
|
||||
.description('Project spec management for conventions and guidelines')
|
||||
.option('--dimension <dim>', 'Target dimension: specs, roadmap, changelog, personal')
|
||||
.option('--context <text>', 'Context text for keyword extraction (CLI mode)')
|
||||
.option('--stdin', 'Read input from stdin (Hook mode)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action((subcommand, args, options) => specCommand(subcommand, args, options));
|
||||
|
||||
// Issue command - Issue lifecycle management with JSONL task tracking
|
||||
program
|
||||
.command('issue [subcommand] [args...]')
|
||||
|
||||
439
ccw/src/commands/spec.ts
Normal file
439
ccw/src/commands/spec.ts
Normal file
@@ -0,0 +1,439 @@
|
||||
/**
|
||||
* Spec Command - CLI endpoint for project spec management
|
||||
*
|
||||
* Provides 6 subcommands: load, list, rebuild, status, init, help.
|
||||
* The load subcommand supports dual-mode: CLI direct and Hook stdin.
|
||||
*
|
||||
* Pattern: cli.ts register -> commands/spec.ts dispatch -> tools/spec-*.ts execute
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
interface SpecOptions {
|
||||
dimension?: string;
|
||||
context?: string;
|
||||
stdin?: boolean;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
interface StdinData {
|
||||
session_id?: string;
|
||||
cwd?: string;
|
||||
user_prompt?: string;
|
||||
prompt?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read JSON data from stdin (for Claude Code hooks).
|
||||
*/
|
||||
async function readStdin(): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
let data = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = process.stdin.read()) !== null) {
|
||||
data += chunk;
|
||||
}
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
resolve(data);
|
||||
});
|
||||
if (process.stdin.isTTY) {
|
||||
resolve('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project path from hook data or current working directory.
|
||||
*/
|
||||
function getProjectPath(hookCwd?: string): string {
|
||||
return hookCwd || process.cwd();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Subcommand Actions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Load action - load specs matching dimension/keywords.
|
||||
*
|
||||
* CLI mode: --dimension and --context options, outputs formatted markdown.
|
||||
* Hook mode: --stdin reads JSON {session_id, cwd, user_prompt}, outputs JSON {continue, systemMessage}.
|
||||
*/
|
||||
async function loadAction(options: SpecOptions): Promise<void> {
|
||||
const { stdin, dimension, context } = options;
|
||||
let projectPath: string;
|
||||
let stdinData: StdinData | undefined;
|
||||
|
||||
if (stdin) {
|
||||
try {
|
||||
const raw = await readStdin();
|
||||
if (raw) {
|
||||
stdinData = JSON.parse(raw) as StdinData;
|
||||
projectPath = getProjectPath(stdinData.cwd);
|
||||
} else {
|
||||
projectPath = getProjectPath();
|
||||
}
|
||||
} catch {
|
||||
// Malformed stdin - output continue and exit
|
||||
process.stdout.write(JSON.stringify({ continue: true }));
|
||||
process.exit(0);
|
||||
}
|
||||
} else {
|
||||
projectPath = getProjectPath();
|
||||
}
|
||||
|
||||
try {
|
||||
const { loadSpecs } = await import('../tools/spec-loader.js');
|
||||
|
||||
const keywords = context
|
||||
? context.split(/[\s,]+/).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
const result = await loadSpecs({
|
||||
projectPath,
|
||||
dimension: dimension as 'specs' | 'roadmap' | 'changelog' | 'personal' | undefined,
|
||||
keywords,
|
||||
outputFormat: stdin ? 'hook' : 'cli',
|
||||
stdinData,
|
||||
});
|
||||
|
||||
if (stdin) {
|
||||
process.stdout.write(result.content);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(result.content);
|
||||
} catch (error) {
|
||||
if (stdin) {
|
||||
process.stdout.write(JSON.stringify({ continue: true }));
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List action - show all indexed specs with readMode and keyword info.
|
||||
*/
|
||||
async function listAction(options: SpecOptions): Promise<void> {
|
||||
const { dimension, json } = options;
|
||||
const projectPath = getProjectPath();
|
||||
|
||||
try {
|
||||
const { getDimensionIndex, SPEC_DIMENSIONS } = await import(
|
||||
'../tools/spec-index-builder.js'
|
||||
);
|
||||
|
||||
const dimensions = dimension ? [dimension] : [...SPEC_DIMENSIONS];
|
||||
const allEntries: Array<{
|
||||
dimension: string;
|
||||
title: string;
|
||||
readMode: string;
|
||||
priority: string;
|
||||
keywords: string[];
|
||||
file: string;
|
||||
}> = [];
|
||||
|
||||
for (const dim of dimensions) {
|
||||
const index = await getDimensionIndex(projectPath, dim);
|
||||
for (const entry of index.entries) {
|
||||
allEntries.push({
|
||||
dimension: entry.dimension,
|
||||
title: entry.title,
|
||||
readMode: entry.readMode,
|
||||
priority: entry.priority,
|
||||
keywords: entry.keywords,
|
||||
file: entry.file,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (json) {
|
||||
console.log(JSON.stringify(allEntries, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (allEntries.length === 0) {
|
||||
console.log(chalk.gray('No specs found. Run "ccw spec init" to create seed documents.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold(`Specs (${allEntries.length} total)\n`));
|
||||
|
||||
let currentDim = '';
|
||||
for (const entry of allEntries) {
|
||||
if (entry.dimension !== currentDim) {
|
||||
currentDim = entry.dimension;
|
||||
console.log(chalk.yellow(` [${currentDim}]`));
|
||||
}
|
||||
|
||||
const modeTag =
|
||||
entry.readMode === 'required'
|
||||
? chalk.red('required')
|
||||
: chalk.gray('optional');
|
||||
const priTag = chalk.cyan(entry.priority);
|
||||
const kw = entry.keywords.length > 0
|
||||
? chalk.gray(` (${entry.keywords.join(', ')})`)
|
||||
: '';
|
||||
|
||||
console.log(` ${entry.title} ${modeTag} ${priTag}${kw}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild action - force re-scan of MD files and rebuild .spec-index cache.
|
||||
*/
|
||||
async function rebuildAction(options: SpecOptions): Promise<void> {
|
||||
const { dimension } = options;
|
||||
const projectPath = getProjectPath();
|
||||
|
||||
try {
|
||||
const { buildAllIndices, buildDimensionIndex, getIndexPath, SPEC_DIMENSIONS } =
|
||||
await import('../tools/spec-index-builder.js');
|
||||
const { writeFileSync } = await import('fs');
|
||||
|
||||
if (dimension) {
|
||||
console.log(chalk.cyan(`Rebuilding index for: ${dimension}`));
|
||||
const index = await buildDimensionIndex(projectPath, dimension);
|
||||
const indexPath = getIndexPath(projectPath, dimension);
|
||||
writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
|
||||
console.log(
|
||||
chalk.green(` ${dimension}: ${index.entries.length} entries indexed`)
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.cyan('Rebuilding all spec indices...'));
|
||||
await buildAllIndices(projectPath);
|
||||
// Show stats
|
||||
const { readCachedIndex } = await import('../tools/spec-index-builder.js');
|
||||
for (const dim of SPEC_DIMENSIONS) {
|
||||
const cached = readCachedIndex(projectPath, dim);
|
||||
const count = cached?.entries.length ?? 0;
|
||||
console.log(chalk.green(` ${dim}: ${count} entries indexed`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.green('\nIndex rebuild complete.'));
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Status action - show per-dimension stats.
|
||||
*/
|
||||
async function statusAction(options: SpecOptions): Promise<void> {
|
||||
const { json } = options;
|
||||
const projectPath = getProjectPath();
|
||||
|
||||
try {
|
||||
const { readCachedIndex, SPEC_DIMENSIONS, getDimensionDir } = await import(
|
||||
'../tools/spec-index-builder.js'
|
||||
);
|
||||
const { existsSync } = await import('fs');
|
||||
|
||||
const stats: Array<{
|
||||
dimension: string;
|
||||
total: number;
|
||||
required: number;
|
||||
optional: number;
|
||||
indexed: boolean;
|
||||
built_at: string | null;
|
||||
dirExists: boolean;
|
||||
}> = [];
|
||||
|
||||
for (const dim of SPEC_DIMENSIONS) {
|
||||
const cached = readCachedIndex(projectPath, dim);
|
||||
const dimDir = getDimensionDir(projectPath, dim);
|
||||
const dirExists = existsSync(dimDir);
|
||||
|
||||
const entries = cached?.entries ?? [];
|
||||
const required = entries.filter(e => e.readMode === 'required').length;
|
||||
const optional = entries.filter(e => e.readMode === 'optional').length;
|
||||
|
||||
stats.push({
|
||||
dimension: dim,
|
||||
total: entries.length,
|
||||
required,
|
||||
optional,
|
||||
indexed: cached !== null,
|
||||
built_at: cached?.built_at ?? null,
|
||||
dirExists,
|
||||
});
|
||||
}
|
||||
|
||||
if (json) {
|
||||
console.log(JSON.stringify(stats, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold('Spec System Status\n'));
|
||||
|
||||
for (const s of stats) {
|
||||
const dirStatus = s.dirExists ? chalk.green('OK') : chalk.red('missing');
|
||||
const indexStatus = s.indexed ? chalk.green('cached') : chalk.yellow('not built');
|
||||
const builtAt = s.built_at
|
||||
? chalk.gray(` (${new Date(s.built_at).toLocaleString()})`)
|
||||
: '';
|
||||
|
||||
console.log(chalk.yellow(` [${s.dimension}]`));
|
||||
console.log(` Directory: ${dirStatus}`);
|
||||
console.log(` Index: ${indexStatus}${builtAt}`);
|
||||
console.log(
|
||||
` Specs: ${s.total} total (${chalk.red(String(s.required))} required, ${chalk.gray(String(s.optional))} optional)`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init action - create directory structure and seed documents.
|
||||
*/
|
||||
async function initAction(): Promise<void> {
|
||||
const projectPath = getProjectPath();
|
||||
|
||||
try {
|
||||
const { initSpecSystem } = await import('../tools/spec-init.js');
|
||||
|
||||
console.log(chalk.cyan('Initializing spec system...'));
|
||||
const result = initSpecSystem(projectPath);
|
||||
|
||||
if (result.directories.length > 0) {
|
||||
console.log(chalk.green('\nDirectories created:'));
|
||||
for (const dir of result.directories) {
|
||||
console.log(chalk.gray(` + ${dir}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (result.created.length > 0) {
|
||||
console.log(chalk.green('\nSeed files created:'));
|
||||
for (const file of result.created) {
|
||||
console.log(chalk.gray(` + ${file}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (result.skipped.length > 0) {
|
||||
console.log(chalk.gray('\nSkipped (already exist):'));
|
||||
for (const file of result.skipped) {
|
||||
console.log(chalk.gray(` - ${file}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (result.directories.length === 0 && result.created.length === 0) {
|
||||
console.log(chalk.gray('\nSpec system already initialized. No changes made.'));
|
||||
} else {
|
||||
console.log(chalk.green('\nSpec system initialized. Run "ccw spec rebuild" to build index.'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show help for spec command.
|
||||
*/
|
||||
function showHelp(): void {
|
||||
console.log(`
|
||||
${chalk.bold('ccw spec')} - Project spec management
|
||||
|
||||
${chalk.bold('USAGE')}
|
||||
ccw spec <subcommand> [options]
|
||||
|
||||
${chalk.bold('SUBCOMMANDS')}
|
||||
load Load specs matching dimension/keywords (CLI or Hook mode)
|
||||
list List all indexed specs with readMode and keyword info
|
||||
rebuild Force re-scan of MD files and rebuild .spec-index cache
|
||||
status Show per-dimension stats (total, required, optional, freshness)
|
||||
init Create 4-dimension directory structure with seed MD documents
|
||||
|
||||
${chalk.bold('OPTIONS')}
|
||||
--dimension <dim> Target dimension: specs, roadmap, changelog, personal
|
||||
--context <text> Context text for keyword extraction (CLI mode)
|
||||
--stdin Read input from stdin (Hook mode)
|
||||
--json Output as JSON
|
||||
|
||||
${chalk.bold('EXAMPLES')}
|
||||
${chalk.gray('# Initialize spec system:')}
|
||||
ccw spec init
|
||||
|
||||
${chalk.gray('# Load specs for a topic (CLI mode):')}
|
||||
ccw spec load --dimension specs --context "auth jwt security"
|
||||
|
||||
${chalk.gray('# Load all matching specs:')}
|
||||
ccw spec load --context "implement user authentication"
|
||||
|
||||
${chalk.gray('# Use as Claude Code hook (settings.json):')}
|
||||
ccw spec load --stdin
|
||||
|
||||
${chalk.gray('# List all specs:')}
|
||||
ccw spec list
|
||||
|
||||
${chalk.gray('# List specs for a specific dimension:')}
|
||||
ccw spec list --dimension specs
|
||||
|
||||
${chalk.gray('# Rebuild index cache:')}
|
||||
ccw spec rebuild
|
||||
|
||||
${chalk.gray('# Rebuild single dimension:')}
|
||||
ccw spec rebuild --dimension roadmap
|
||||
|
||||
${chalk.gray('# Check system status:')}
|
||||
ccw spec status
|
||||
`);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Command Dispatcher
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Main spec command handler.
|
||||
*
|
||||
* Dispatches to subcommand action functions following the same switch
|
||||
* pattern used by hookCommand in commands/hook.ts.
|
||||
*/
|
||||
export async function specCommand(
|
||||
subcommand: string,
|
||||
args: string | string[],
|
||||
options: SpecOptions
|
||||
): Promise<void> {
|
||||
switch (subcommand) {
|
||||
case 'load':
|
||||
await loadAction(options);
|
||||
break;
|
||||
case 'list':
|
||||
case 'ls':
|
||||
await listAction(options);
|
||||
break;
|
||||
case 'rebuild':
|
||||
await rebuildAction(options);
|
||||
break;
|
||||
case 'status':
|
||||
await statusAction(options);
|
||||
break;
|
||||
case 'init':
|
||||
await initAction();
|
||||
break;
|
||||
case 'help':
|
||||
case undefined:
|
||||
showHelp();
|
||||
break;
|
||||
default:
|
||||
console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
|
||||
console.error(chalk.gray('Run "ccw spec help" for usage information'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -165,6 +165,10 @@ export async function buildDimensionIndex(
|
||||
const entry = parseSpecFile(filePath, dimension, projectPath);
|
||||
if (entry) {
|
||||
entries.push(entry);
|
||||
} else {
|
||||
process.stderr.write(
|
||||
`[spec-index-builder] Skipping malformed spec file: ${file}\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user