#!/usr/bin/env bun import { Command } from 'commander' import { loadConfig, ensurePlansDir, resolveAgent, resolveAgentWithoutConfigCreation, initProject, } from './config' import type { AgentType } from './config' import { listPrds } from './prds' import { listIncompleteTaskFiles } from './status' import { generatePRD } from './prd-generator' import { generateTasksFromPRD } from './task-generator' import { setVerbose } from './logger' import packageJson from '../package.json' const program = new Command() // No automatic config creation - only create config during init command program .name('hone') .description( 'AI Coding Agent Orchestrator - Orchestrate AI agents to implement features based on PRDs' ) .version(packageJson.version, '-v, --version', 'output the current version') .addHelpText( 'after', ` Common Workflow: hone agents-md # Generate AGENTS.md (if none exists) hone prd "your feature description" # Create a PRD # Manually review .plans/prd-.md hone prd-to-tasks .plans/prd-.md # Generate tasks from PRD hone extend-prd .plans/prd-.md "new requirement" # Add requirements to existing PRD hone run .plans/tasks-.yml -i 10 # Implement the feature hone prune # Archive completed PRDs to .plans/archive/ Extended PRD Workflow: # Add new requirements with interactive refinement hone extend-prd .plans/prd-user-auth.md "Add OAuth integration with Google and GitHub" # Reference files in requirement description hone extend-prd .plans/prd-api.md "Add rate limiting based on ./docs/rate-limits.md" # Reference URLs for external specifications hone extend-prd .plans/prd-payment.md "Integrate Stripe API from https://docs.stripe.com/api" Model Configuration (v2): Configure models in .plans/hone.config.yml: version: 2 agent: claude # Default agent (claude or opencode) claude: model: claude-sonnet-4-6 # Default model for claude agent (optional) models: prd: claude-opus-4-6 # Override for PRD generation (optional) prdToTasks: claude-opus-4-6 # Override for task generation (optional) implement: claude-opus-4-6 # Override for implementation (optional) review: claude-sonnet-4-6 # Override for review (optional) finalize: claude-sonnet-4-6 # Override for finalization (optional) agentsMd: claude-sonnet-4-6 # Override for AGENTS.md generation (optional) extendPrd: claude-sonnet-4-6 # Override for PRD extension (optional) opencode: model: anthropic/claude-sonnet-4-6 # Default model for opencode agent (optional) models: prd: anthropic/claude-opus-4-6 # Phase overrides follow same pattern agentsDocsDir: '.agents/' # Directory for AGENTS.md detail files (default: '.agents/') Model resolution: phase model > agent model > hardcoded default. Phase keys: prd, prdToTasks, implement, review, finalize, agentsMd, extendPrd. Use agentsDocsDir: '.agents-docs' to preserve old directory name. Check available models: opencode --help or claude --help Upgrading from v1: hone auto-migrates v1 configs to v2 on first load. ` ) // Global flags program.option('--agent ', 'Override default agent (opencode or claude)') program.option('--verbose', 'Show detailed agent interaction logs') // Commands program .command('init') .description('Initialize hone in current directory') .action(async () => { try { const result = await initProject() if (!result.plansCreated && !result.configCreated) { console.log('hone is already initialized in this directory.') console.log('') console.log(' .plans/ directory: exists') console.log(' config file: exists') return } console.log('Initialized hone successfully!') console.log('') if (result.plansCreated) { console.log(' ✓ Created .plans/ directory') } else { console.log(' • .plans/ directory already exists') } if (result.configCreated) { console.log(' ✓ Created .plans/hone.config.yml') } else { console.log(' • .plans/hone.config.yml already exists') } console.log('') console.log('Next steps:') console.log(' 1. Install opencode or claude CLI (hone uses agent subprocesses)') console.log(' 2. Generate a PRD: hone prd "your feature description"') console.log(' 3. Generate tasks: hone prd-to-tasks .plans/prd-.md') console.log(' 4. Execute tasks: hone run .plans/tasks-.yml -i 5') } catch (error) { console.error('\n✗ Error initializing hone:', error instanceof Error ? error.message : error) process.exit(1) } }) program .command('prds') .description('List all PRDs in .plans/ directory') .action(async () => { const prds = await listPrds() if (prds.length === 0) { console.log('No PRDs found in .plans/') console.log('') console.log('Create a PRD with: hone prd "your feature description"') return } console.log('PRDs in .plans/') console.log('') for (const prd of prds) { console.log(` .plans/${prd.filename}`) console.log(` Tasks: ${prd.taskFile ? `.plans/${prd.taskFile}` : 'none'}`) if ( prd.status === 'in progress' && prd.completedCount !== undefined && prd.totalCount !== undefined ) { console.log(` Status: ${prd.status} (${prd.completedCount}/${prd.totalCount} completed)`) } else { console.log(` Status: ${prd.status}`) } console.log('') } }) program .command('status') .description('Show task status for incomplete task lists') .action(async () => { const taskFiles = await listIncompleteTaskFiles() if (taskFiles.length === 0) { console.log('No incomplete task lists found.') console.log('') console.log('All tasks completed! 🎉') return } console.log('Incomplete task lists:') console.log('') for (const taskFile of taskFiles) { console.log(` .plans/${taskFile.filename}`) console.log(` Feature: ${taskFile.feature}`) console.log(` Progress: ${taskFile.completedCount}/${taskFile.totalCount} tasks completed`) if (taskFile.nextTask) { console.log(` Next: ${taskFile.nextTask.id} - ${taskFile.nextTask.title}`) } else { console.log(` Next: (waiting for dependencies)`) } console.log('') } }) program .command('prd ') .description('Generate PRD interactively from feature description (supports file paths and URLs)') .action(async (description: string) => { try { setVerbose(program.opts().verbose || false) await generatePRD(description) } catch (error) { console.error('\n✗ Error generating PRD:', error instanceof Error ? error.message : error) process.exit(1) } }) program .command('prd-to-tasks ') .description('Generate task list from PRD file') .action(async (prdFile: string) => { try { setVerbose(program.opts().verbose || false) await generateTasksFromPRD(prdFile) } catch (error) { console.error('\n✗ Error generating tasks:', error instanceof Error ? error.message : error) process.exit(1) } }) program .command('extend-prd ') .description( 'Add new requirements to existing PRD file with AI-generated questions and task generation (supports file paths and URLs)' ) .action(async (prdFile: string, requirementDescription: string) => { try { setVerbose(program.opts().verbose || false) const { extendPRD } = await import('./extend-prd') await extendPRD(prdFile, requirementDescription) } catch (error) { console.error('\n✗ Error extending PRD:', error instanceof Error ? error.message : error) process.exit(1) } }) program .command('run ') .description('Execute tasks iteratively') .requiredOption('-i, --iterations ', 'Number of iterations to run') .option('--skip ', 'Skip a phase (e.g., review)') .action(async (tasksFile: string, options: { iterations: string; skip?: string }) => { try { setVerbose(program.opts().verbose || false) const agent = await resolveAgent(program.opts().agent) const { executeTasks } = await import('./run') await executeTasks({ tasksFile, iterations: parseInt(options.iterations, 10), agent, skipPhase: options.skip as 'review' | undefined, }) } catch (error) { console.error('\n✗ Error executing tasks:', error instanceof Error ? error.message : error) process.exit(1) } }) program .command('agents-md') .description('Generate AGENTS.md documentation (files in agentsDocsDir, default: .agents/)') .option('--overwrite', 'Overwrite existing AGENTS.md file if it exists') .action(async (options: { overwrite?: boolean }) => { try { setVerbose(program.opts().verbose || false) const agent = await resolveAgentWithoutConfigCreation(program.opts().agent) const { generateAgentsMd } = await import('./agents-md-generator') const result = await generateAgentsMd({ overwrite: options.overwrite, agent }) if (!result.success) { if (result.error?.message.includes('already exists')) { console.error('\n✗ AGENTS.md already exists') console.error('\nUse --overwrite to replace the existing file.') console.error('Or review the current AGENTS.md before regenerating.') } else { console.error('\n✗ Failed to generate AGENTS.md') console.error(`\nError: ${result.error?.message || 'Unknown error'}`) } process.exit(1) } } catch (error) { console.error('\n✗ Failed to generate AGENTS.md') console.error(`\nError: ${error instanceof Error ? error.message : error}`) process.exit(1) } }) program .command('skill') .description('Print hone skill installation instructions and skill file contents') .action(async () => { try { const { printSkill } = await import('./skill') await printSkill() } catch (error) { console.error('\n✗ Error printing skill:', error instanceof Error ? error.message : error) process.exit(1) } }) program .command('prune') .description('Move completed PRDs and their associated files to .plans/archive/') .option('--dry-run', 'Preview operations without executing moves') .action(async (options: { dryRun?: boolean }) => { try { setVerbose(program.opts().verbose || false) const { pruneCompletedPrds } = await import('./prune') await pruneCompletedPrds(options.dryRun || false) } catch (error) { // If it's a HoneError, it's already formatted nicely if (error instanceof Error && error.name === 'HoneError') { console.error(`\n✗ ${error.message}`) } else { console.error('\n✗ Error pruning PRDs:', error instanceof Error ? error.message : error) } process.exit(1) } }) // Handle unknown commands and options by showing help program.configureOutput({ outputError: (str, write) => { // Suppress error messages for unknown commands/options since we show help instead if (!str.includes('unknown option') && !str.includes('unknown command')) { write(str) } }, }) program.exitOverride(err => { if (err.code === 'commander.unknownOption' || err.code === 'commander.unknownCommand') { program.outputHelp() process.exit(0) } // Re-throw all other errors to maintain normal behavior if (err.exitCode === 0) { // For normal exits (like --version, --help), just exit normally process.exit(0) } throw err }) program.parse()