@hivehub/rulebook 5.3.2 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +393 -354
- package/dist/cli/commands/compress.d.ts +18 -0
- package/dist/cli/commands/compress.d.ts.map +1 -0
- package/dist/cli/commands/compress.js +100 -0
- package/dist/cli/commands/compress.js.map +1 -0
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +2 -0
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/core/claude-settings-manager.d.ts +7 -0
- package/dist/core/claude-settings-manager.d.ts.map +1 -1
- package/dist/core/claude-settings-manager.js +31 -14
- package/dist/core/claude-settings-manager.js.map +1 -1
- package/dist/core/compress/compressor.d.ts +60 -0
- package/dist/core/compress/compressor.d.ts.map +1 -0
- package/dist/core/compress/compressor.js +232 -0
- package/dist/core/compress/compressor.js.map +1 -0
- package/dist/core/compress/discover.d.ts +19 -0
- package/dist/core/compress/discover.d.ts.map +1 -0
- package/dist/core/compress/discover.js +100 -0
- package/dist/core/compress/discover.js.map +1 -0
- package/dist/core/compress/validator.d.ts +47 -0
- package/dist/core/compress/validator.d.ts.map +1 -0
- package/dist/core/compress/validator.js +131 -0
- package/dist/core/compress/validator.js.map +1 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +66 -0
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/generator.d.ts +16 -0
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +38 -69
- package/dist/core/generator.js.map +1 -1
- package/dist/core/merger.js +2 -2
- package/dist/core/merger.js.map +1 -1
- package/dist/hooks/safe-flag-io.d.ts +77 -0
- package/dist/hooks/safe-flag-io.d.ts.map +1 -0
- package/dist/hooks/safe-flag-io.js +169 -0
- package/dist/hooks/safe-flag-io.js.map +1 -0
- package/dist/hooks/terse-activate.d.ts +59 -0
- package/dist/hooks/terse-activate.d.ts.map +1 -0
- package/dist/hooks/terse-activate.js +149 -0
- package/dist/hooks/terse-activate.js.map +1 -0
- package/dist/hooks/terse-config.d.ts +51 -0
- package/dist/hooks/terse-config.d.ts.map +1 -0
- package/dist/hooks/terse-config.js +130 -0
- package/dist/hooks/terse-config.js.map +1 -0
- package/dist/hooks/terse-mode-tracker.d.ts +78 -0
- package/dist/hooks/terse-mode-tracker.d.ts.map +1 -0
- package/dist/hooks/terse-mode-tracker.js +213 -0
- package/dist/hooks/terse-mode-tracker.js.map +1 -0
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/rulebook-server.d.ts.map +1 -1
- package/dist/mcp/rulebook-server.js +236 -0
- package/dist/mcp/rulebook-server.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/templates/hooks/terse-activate.ps1 +143 -0
- package/templates/hooks/terse-activate.sh +197 -0
- package/templates/hooks/terse-mode-tracker.ps1 +153 -0
- package/templates/hooks/terse-mode-tracker.sh +187 -0
- package/templates/modules/RULEBOOK_MCP.md +52 -0
- package/templates/skills/core/rulebook-terse/SKILL.md +116 -0
- package/templates/skills/core/rulebook-terse-commit/SKILL.md +96 -0
- package/templates/skills/core/rulebook-terse-review/SKILL.md +112 -0
- package/dist/cli/commands.d.ts +0 -225
- package/dist/cli/commands.d.ts.map +0 -1
- package/dist/cli/commands.js +0 -3984
- package/dist/cli/commands.js.map +0 -1
package/dist/cli/commands.js
DELETED
|
@@ -1,3984 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import ora from 'ora';
|
|
3
|
-
import { detectProject } from '../core/detector.js';
|
|
4
|
-
import { generateFullAgents } from '../core/generator.js';
|
|
5
|
-
import { mergeFullAgents } from '../core/merger.js';
|
|
6
|
-
import { generateWorkflows, generateIDEFiles, generateAICLIFiles, } from '../core/workflow-generator.js';
|
|
7
|
-
import { writeFile, createBackup, ensureDir } from '../utils/file-system.js';
|
|
8
|
-
import { existsSync } from 'fs';
|
|
9
|
-
import { parseRulesIgnore } from '../utils/rulesignore.js';
|
|
10
|
-
import { installGitHooks } from '../utils/git-hooks.js';
|
|
11
|
-
import { scaffoldMinimalProject } from '../core/minimal-scaffolder.js';
|
|
12
|
-
import path from 'path';
|
|
13
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
14
|
-
import { fileURLToPath } from 'url';
|
|
15
|
-
import { SkillsManager, getDefaultTemplatesPath } from '../core/skills-manager.js';
|
|
16
|
-
import { WorkspaceManager } from '../core/workspace/workspace-manager.js';
|
|
17
|
-
import { migrateLegacyMcpConfigs } from '../core/workspace/legacy-migrator.js';
|
|
18
|
-
const FRAMEWORK_LABELS = {
|
|
19
|
-
nestjs: 'NestJS',
|
|
20
|
-
spring: 'Spring Boot',
|
|
21
|
-
laravel: 'Laravel',
|
|
22
|
-
angular: 'Angular',
|
|
23
|
-
react: 'React',
|
|
24
|
-
vue: 'Vue.js',
|
|
25
|
-
nuxt: 'Nuxt',
|
|
26
|
-
nextjs: 'Next.js',
|
|
27
|
-
django: 'Django',
|
|
28
|
-
rails: 'Ruby on Rails',
|
|
29
|
-
flask: 'Flask',
|
|
30
|
-
symfony: 'Symfony',
|
|
31
|
-
zend: 'Zend Framework',
|
|
32
|
-
jquery: 'jQuery',
|
|
33
|
-
reactnative: 'React Native',
|
|
34
|
-
flutter: 'Flutter',
|
|
35
|
-
electron: 'Electron',
|
|
36
|
-
};
|
|
37
|
-
// Get version from package.json
|
|
38
|
-
function getRulebookVersion() {
|
|
39
|
-
try {
|
|
40
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
41
|
-
const packagePath = path.join(__dirname, '..', '..', 'package.json');
|
|
42
|
-
const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
43
|
-
return packageJson.version;
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return '0.12.1'; // Fallback version
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
export async function initCommand(options) {
|
|
50
|
-
try {
|
|
51
|
-
const cwd = process.cwd();
|
|
52
|
-
console.log(chalk.bold.blue('\n🔍 Rulebook Project Initializer\n'));
|
|
53
|
-
// Detect project
|
|
54
|
-
const spinner = ora('Detecting project structure...').start();
|
|
55
|
-
const detection = await detectProject(cwd);
|
|
56
|
-
spinner.succeed('Project detection complete');
|
|
57
|
-
// Assess project complexity for calibrated generation (v5)
|
|
58
|
-
const { assessComplexity } = await import('../core/complexity-detector.js');
|
|
59
|
-
const complexity = assessComplexity(cwd);
|
|
60
|
-
console.log(chalk.gray(` Complexity: ${complexity.tier.toUpperCase()} (${complexity.metrics.estimatedLoc.toLocaleString()} LOC, ${complexity.metrics.languageCount} languages)`));
|
|
61
|
-
// Show detection results
|
|
62
|
-
if (detection.languages.length > 0) {
|
|
63
|
-
console.log(chalk.green('\n✓ Detected languages:'));
|
|
64
|
-
for (const lang of detection.languages) {
|
|
65
|
-
console.log(` - ${lang.language} (${(lang.confidence * 100).toFixed(0)}% confidence)`);
|
|
66
|
-
console.log(` Indicators: ${lang.indicators.join(', ')}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (detection.modules.filter((m) => m.detected).length > 0) {
|
|
70
|
-
console.log(chalk.green('\n✓ Detected modules:'));
|
|
71
|
-
for (const module of detection.modules.filter((m) => m.detected)) {
|
|
72
|
-
console.log(` - ${module.module} (${module.source})`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// Show monorepo detection
|
|
76
|
-
if (detection.monorepo?.detected) {
|
|
77
|
-
const mono = detection.monorepo;
|
|
78
|
-
console.log(chalk.green(`\n✓ Monorepo detected: ${chalk.bold(mono.tool ?? 'manual')}`));
|
|
79
|
-
if (mono.packages.length > 0) {
|
|
80
|
-
console.log(` Packages (${mono.packages.length}): ${mono.packages.slice(0, 5).join(', ')}${mono.packages.length > 5 ? ` +${mono.packages.length - 5} more` : ''}`);
|
|
81
|
-
}
|
|
82
|
-
if (options.package) {
|
|
83
|
-
console.log(chalk.cyan(` → Initializing package: ${options.package}`));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// Recommend sequential-thinking MCP if not detected
|
|
87
|
-
const seqThinking = detection.modules.find((m) => m.module === 'sequential_thinking');
|
|
88
|
-
if (seqThinking && !seqThinking.detected) {
|
|
89
|
-
console.log(chalk.yellow('\n💡 Tip: Install sequential-thinking MCP for structured problem solving:\n' +
|
|
90
|
-
' npx @modelcontextprotocol/create-server sequential-thinking\n' +
|
|
91
|
-
' or add to .mcp.json: { "mcpServers": { "sequential-thinking": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-sequential-thinking"] } } }'));
|
|
92
|
-
}
|
|
93
|
-
const detectedFrameworks = detection.frameworks.filter((f) => f.detected);
|
|
94
|
-
if (detectedFrameworks.length > 0) {
|
|
95
|
-
console.log(chalk.green('\n✓ Detected frameworks:'));
|
|
96
|
-
for (const framework of detectedFrameworks) {
|
|
97
|
-
const languagesLabel = framework.languages.map((lang) => lang.toUpperCase()).join(', ');
|
|
98
|
-
const indicators = framework.indicators.join(', ');
|
|
99
|
-
const label = FRAMEWORK_LABELS[framework.framework] || framework.framework;
|
|
100
|
-
console.log(` - ${label} (${languagesLabel})${indicators ? ` [${indicators}]` : ''}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (detection.existingAgents) {
|
|
104
|
-
console.log(chalk.yellow(`\n⚠ Found existing AGENTS.md with ${detection.existingAgents.blocks.length} blocks`));
|
|
105
|
-
for (const block of detection.existingAgents.blocks) {
|
|
106
|
-
console.log(` - ${block.name}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// Get project configuration — auto-setup from detection, no prompts
|
|
110
|
-
const cliMinimal = Boolean(options.minimal);
|
|
111
|
-
const cliLight = Boolean(options.light);
|
|
112
|
-
const cliLean = Boolean(options.lean);
|
|
113
|
-
const config = {
|
|
114
|
-
languages: detection.languages.map((l) => l.language),
|
|
115
|
-
modules: cliMinimal ? [] : detection.modules.filter((m) => m.detected).map((m) => m.module),
|
|
116
|
-
frameworks: detection.frameworks.filter((f) => f.detected).map((f) => f.framework),
|
|
117
|
-
ides: cliMinimal ? [] : ['cursor'],
|
|
118
|
-
projectType: 'application',
|
|
119
|
-
coverageThreshold: 95,
|
|
120
|
-
strictDocs: true,
|
|
121
|
-
generateWorkflows: true,
|
|
122
|
-
includeGitWorkflow: true,
|
|
123
|
-
gitPushMode: 'manual',
|
|
124
|
-
installGitHooks: false,
|
|
125
|
-
minimal: cliMinimal,
|
|
126
|
-
lightMode: cliLight,
|
|
127
|
-
modular: true,
|
|
128
|
-
};
|
|
129
|
-
console.log(chalk.blue('\nAuto-configuring from detection results...'));
|
|
130
|
-
const minimalMode = config.minimal ?? cliMinimal;
|
|
131
|
-
config.minimal = minimalMode;
|
|
132
|
-
config.modules = minimalMode ? [] : config.modules || [];
|
|
133
|
-
config.frameworks = config.frameworks || [];
|
|
134
|
-
config.ides = minimalMode ? [] : config.ides || ['cursor'];
|
|
135
|
-
config.includeGitWorkflow = config.includeGitWorkflow ?? true;
|
|
136
|
-
config.generateWorkflows = config.generateWorkflows ?? true;
|
|
137
|
-
config.modular = config.modular ?? true; // Enable modular by default
|
|
138
|
-
if (cliLean) {
|
|
139
|
-
config.agentsMode = 'lean';
|
|
140
|
-
}
|
|
141
|
-
let minimalArtifacts = [];
|
|
142
|
-
if (minimalMode) {
|
|
143
|
-
minimalArtifacts = await scaffoldMinimalProject(cwd, {
|
|
144
|
-
projectName: path.basename(cwd),
|
|
145
|
-
description: 'Essential project scaffolding generated by Rulebook minimal mode.',
|
|
146
|
-
license: 'MIT',
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
const detectedHookStatus = {
|
|
150
|
-
preCommit: detection.gitHooks?.preCommitExists ?? false,
|
|
151
|
-
prePush: detection.gitHooks?.prePushExists ?? false,
|
|
152
|
-
};
|
|
153
|
-
const hookLanguages = detection.languages.length > 0
|
|
154
|
-
? detection.languages
|
|
155
|
-
: config.languages.map((language) => ({
|
|
156
|
-
language: language,
|
|
157
|
-
confidence: 1,
|
|
158
|
-
indicators: [],
|
|
159
|
-
}));
|
|
160
|
-
let hooksInstalled = false;
|
|
161
|
-
if (config.installGitHooks) {
|
|
162
|
-
const hookSpinner = ora('Installing Git hooks (pre-commit & pre-push)...').start();
|
|
163
|
-
try {
|
|
164
|
-
await installGitHooks({ languages: hookLanguages, cwd });
|
|
165
|
-
hookSpinner.succeed('Git hooks installed successfully');
|
|
166
|
-
hooksInstalled = true;
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
hookSpinner.fail('Failed to install Git hooks');
|
|
170
|
-
console.error(chalk.red(' ➤'), error instanceof Error ? error.message : error);
|
|
171
|
-
console.log(chalk.yellow(' ⚠ Skipping automatic hook installation. You can rerun "rulebook init" later to retry or install manually.'));
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
else if (!detectedHookStatus.preCommit || !detectedHookStatus.prePush) {
|
|
175
|
-
console.log(chalk.gray('\nℹ Git hooks were not installed automatically. Run "rulebook init" again if you want to add them later.'));
|
|
176
|
-
}
|
|
177
|
-
const gitHooksActive = hooksInstalled || (detectedHookStatus.preCommit && detectedHookStatus.prePush);
|
|
178
|
-
config.installGitHooks = gitHooksActive;
|
|
179
|
-
// Check .rulesignore
|
|
180
|
-
const rulesIgnore = await parseRulesIgnore(cwd);
|
|
181
|
-
if (rulesIgnore.patterns.length > 0) {
|
|
182
|
-
console.log(chalk.yellow('\n📋 Found .rulesignore with patterns:'));
|
|
183
|
-
for (const pattern of rulesIgnore.patterns) {
|
|
184
|
-
console.log(` - ${pattern}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
// Save project configuration to .rulebook
|
|
188
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
189
|
-
const configManager = createConfigManager(cwd);
|
|
190
|
-
// Migrate old directory structure to new consolidated structure
|
|
191
|
-
const dirMigrationSpinner = ora('Migrating directory structure...').start();
|
|
192
|
-
await configManager.migrateDirectoryStructure(cwd);
|
|
193
|
-
dirMigrationSpinner.succeed('Directory structure migrated');
|
|
194
|
-
// Ensure .rulebook/memory/ directory exists for per-project memory persistence
|
|
195
|
-
await ensureDir(path.join(cwd, '.rulebook', 'memory'));
|
|
196
|
-
// Ensure .gitignore has .rulebook entries (keep specs/ and tasks/ tracked)
|
|
197
|
-
await configManager.ensureGitignore();
|
|
198
|
-
// Auto-detect and enable skills based on project detection (v2.0)
|
|
199
|
-
let enabledSkills = [];
|
|
200
|
-
try {
|
|
201
|
-
const { SkillsManager, getDefaultTemplatesPath } = await import('../core/skills-manager.js');
|
|
202
|
-
const skillsManager = new SkillsManager(getDefaultTemplatesPath(), cwd);
|
|
203
|
-
// Build a RulebookConfig-like object for skill detection
|
|
204
|
-
const rulebookConfigForSkills = {
|
|
205
|
-
languages: config.languages,
|
|
206
|
-
frameworks: config.frameworks,
|
|
207
|
-
modules: config.modules,
|
|
208
|
-
services: config.services,
|
|
209
|
-
};
|
|
210
|
-
enabledSkills = await skillsManager.autoDetectSkills(rulebookConfigForSkills);
|
|
211
|
-
if (enabledSkills.length > 0) {
|
|
212
|
-
console.log(chalk.green('\n✓ Auto-detected skills:'));
|
|
213
|
-
for (const skillId of enabledSkills) {
|
|
214
|
-
console.log(chalk.gray(` - ${skillId}`));
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
// Skills system not available or error - continue without skills
|
|
220
|
-
}
|
|
221
|
-
// Load existing config to preserve ralph and memory settings
|
|
222
|
-
const existingConfig = await configManager.loadConfig();
|
|
223
|
-
await configManager.updateConfig({
|
|
224
|
-
languages: config.languages,
|
|
225
|
-
frameworks: config.frameworks,
|
|
226
|
-
modules: config.modules,
|
|
227
|
-
services: config.services,
|
|
228
|
-
modular: config.modular ?? true,
|
|
229
|
-
rulebookDir: config.rulebookDir || '.rulebook',
|
|
230
|
-
...(config.agentsMode ? { agentsMode: config.agentsMode } : {}),
|
|
231
|
-
skills: enabledSkills.length > 0 ? { enabled: enabledSkills } : undefined,
|
|
232
|
-
ralph: existingConfig.ralph,
|
|
233
|
-
memory: existingConfig.memory,
|
|
234
|
-
});
|
|
235
|
-
// --package: generate only the specified package's AGENTS.md and exit
|
|
236
|
-
if (options.package) {
|
|
237
|
-
const packageRoot = path.join(cwd, options.package);
|
|
238
|
-
const { generatePackageAgentsMd } = await import('../core/generator.js');
|
|
239
|
-
await generatePackageAgentsMd(packageRoot, config, cwd);
|
|
240
|
-
console.log(chalk.green(`\n✅ AGENTS.md generated for package: ${options.package}`));
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
// Generate or merge AGENTS.md
|
|
244
|
-
const agentsPath = path.join(cwd, 'AGENTS.md');
|
|
245
|
-
let finalContent;
|
|
246
|
-
if (detection.existingAgents) {
|
|
247
|
-
// Migrate flat layout to specs/ subdirectory if needed
|
|
248
|
-
{
|
|
249
|
-
const { hasFlatLayout, migrateFlatToSpecs } = await import('../core/migrator.js');
|
|
250
|
-
const rulebookDirForMigration = config.rulebookDir || '.rulebook';
|
|
251
|
-
if (await hasFlatLayout(cwd, rulebookDirForMigration)) {
|
|
252
|
-
const { migratedFiles } = await migrateFlatToSpecs(cwd, rulebookDirForMigration);
|
|
253
|
-
if (migratedFiles.length > 0) {
|
|
254
|
-
console.log(chalk.gray(` Migrated ${migratedFiles.length} file(s) to /${rulebookDirForMigration}/specs/`));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
// Always merge — preserve existing customizations
|
|
259
|
-
const mergeSpinner = ora('Merging with existing AGENTS.md...').start();
|
|
260
|
-
finalContent = await mergeFullAgents(detection.existingAgents, config, cwd);
|
|
261
|
-
const backupPath = await createBackup(agentsPath);
|
|
262
|
-
mergeSpinner.succeed(`Backup created: ${path.basename(backupPath)}`);
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
const genSpinner = ora('Generating AGENTS.md...').start();
|
|
266
|
-
finalContent = await generateFullAgents(config, cwd);
|
|
267
|
-
genSpinner.succeed('AGENTS.md generated');
|
|
268
|
-
}
|
|
269
|
-
// Write AGENTS.md
|
|
270
|
-
await writeFile(agentsPath, finalContent);
|
|
271
|
-
console.log(chalk.green(`\n✅ AGENTS.md written to ${agentsPath}`));
|
|
272
|
-
// Install canonical rules based on complexity tier (v5)
|
|
273
|
-
{
|
|
274
|
-
const { installRule, projectRules } = await import('../core/rule-engine.js');
|
|
275
|
-
const { getTemplatesDir } = await import('../core/generator.js');
|
|
276
|
-
const templatesDir = getTemplatesDir();
|
|
277
|
-
// Tier 1 rules — always installed
|
|
278
|
-
const tier1Rules = [
|
|
279
|
-
'no-shortcuts',
|
|
280
|
-
'git-safety',
|
|
281
|
-
'sequential-editing',
|
|
282
|
-
'research-first',
|
|
283
|
-
'follow-task-sequence',
|
|
284
|
-
'incremental-implementation',
|
|
285
|
-
];
|
|
286
|
-
// Tier 2 rules — installed for medium+ complexity
|
|
287
|
-
const tier2Rules = [
|
|
288
|
-
'task-decomposition',
|
|
289
|
-
'incremental-tests',
|
|
290
|
-
'no-deferred',
|
|
291
|
-
'session-workflow',
|
|
292
|
-
];
|
|
293
|
-
const rulesToInstall = [...tier1Rules];
|
|
294
|
-
if (complexity.recommendations.tier2Rules) {
|
|
295
|
-
rulesToInstall.push(...tier2Rules);
|
|
296
|
-
}
|
|
297
|
-
let installedCount = 0;
|
|
298
|
-
for (const name of rulesToInstall) {
|
|
299
|
-
const result = await installRule(cwd, name, templatesDir);
|
|
300
|
-
if (result)
|
|
301
|
-
installedCount++;
|
|
302
|
-
}
|
|
303
|
-
if (installedCount > 0) {
|
|
304
|
-
console.log(chalk.gray(` • Installed ${installedCount} canonical rules to .rulebook/rules/`));
|
|
305
|
-
}
|
|
306
|
-
// Project rules to detected tools
|
|
307
|
-
const ruleResult = await projectRules(cwd, {
|
|
308
|
-
claudeCode: existsSync(path.join(cwd, '.claude')) || existsSync(path.join(cwd, 'CLAUDE.md')),
|
|
309
|
-
cursor: detection.cursor?.detected,
|
|
310
|
-
gemini: detection.geminiCli?.detected,
|
|
311
|
-
windsurf: detection.windsurf?.detected,
|
|
312
|
-
copilot: detection.githubCopilot?.detected,
|
|
313
|
-
continueDev: detection.continueDev?.detected,
|
|
314
|
-
});
|
|
315
|
-
const totalProjected = ruleResult.claudeCode.length +
|
|
316
|
-
ruleResult.cursor.length +
|
|
317
|
-
ruleResult.gemini.length +
|
|
318
|
-
ruleResult.copilot.length +
|
|
319
|
-
ruleResult.windsurf.length +
|
|
320
|
-
ruleResult.continueDev.length;
|
|
321
|
-
if (totalProjected > 0) {
|
|
322
|
-
console.log(chalk.gray(` • Projected rules to ${totalProjected} tool-specific files`));
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
// Show Cursor MDC feedback
|
|
326
|
-
if (detection.cursor?.detected) {
|
|
327
|
-
if (detection.cursor.hasMdcRules) {
|
|
328
|
-
console.log(chalk.gray(' • Cursor .mdc rules updated in .cursor/rules/'));
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
console.log(chalk.gray(' • Cursor .mdc rules generated in .cursor/rules/'));
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
// Show multi-tool config feedback
|
|
335
|
-
if (detection.geminiCli?.detected) {
|
|
336
|
-
console.log(chalk.gray(' • Gemini CLI config generated: GEMINI.md'));
|
|
337
|
-
}
|
|
338
|
-
if (detection.continueDev?.detected) {
|
|
339
|
-
console.log(chalk.gray(' • Continue.dev rules generated in .continue/rules/'));
|
|
340
|
-
}
|
|
341
|
-
if (detection.windsurf?.detected) {
|
|
342
|
-
console.log(chalk.gray(' • Windsurf rules generated: .windsurfrules'));
|
|
343
|
-
}
|
|
344
|
-
if (detection.githubCopilot?.detected) {
|
|
345
|
-
console.log(chalk.gray(' • GitHub Copilot instructions generated in .github/'));
|
|
346
|
-
}
|
|
347
|
-
// Generate workflows if requested
|
|
348
|
-
if (config.generateWorkflows) {
|
|
349
|
-
const workflowSpinner = ora('Generating GitHub Actions workflows...').start();
|
|
350
|
-
const workflows = await generateWorkflows(config, cwd, {
|
|
351
|
-
mode: minimalMode ? 'minimal' : 'full',
|
|
352
|
-
});
|
|
353
|
-
workflowSpinner.succeed(`Generated ${workflows.length} workflow files`);
|
|
354
|
-
for (const workflow of workflows) {
|
|
355
|
-
console.log(chalk.gray(` - ${path.relative(cwd, workflow)}`));
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
// Generate or update .gitignore
|
|
359
|
-
const gitignoreSpinner = ora('Generating/updating .gitignore...').start();
|
|
360
|
-
const { generateGitignore } = await import('../core/gitignore-generator.js');
|
|
361
|
-
const gitignoreResult = await generateGitignore(cwd, detection.languages);
|
|
362
|
-
if (gitignoreResult.created) {
|
|
363
|
-
gitignoreSpinner.succeed('.gitignore created');
|
|
364
|
-
}
|
|
365
|
-
else if (gitignoreResult.updated) {
|
|
366
|
-
gitignoreSpinner.succeed('.gitignore updated with missing patterns');
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
gitignoreSpinner.info('.gitignore already contains all necessary patterns');
|
|
370
|
-
}
|
|
371
|
-
// Generate IDE-specific files
|
|
372
|
-
if (!minimalMode && config.ides.length > 0) {
|
|
373
|
-
const ideSpinner = ora('Generating IDE-specific files...').start();
|
|
374
|
-
const ideFiles = await generateIDEFiles(config, cwd);
|
|
375
|
-
if (ideFiles.length > 0) {
|
|
376
|
-
ideSpinner.succeed(`Generated ${ideFiles.length} IDE configuration files`);
|
|
377
|
-
for (const file of ideFiles) {
|
|
378
|
-
console.log(chalk.gray(` - ${path.relative(cwd, file)}`));
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
else {
|
|
382
|
-
ideSpinner.info('IDE files already exist (skipped)');
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
// Generate AI CLI configuration files (CLAUDE.md, CODEX.md, GEMINI.md)
|
|
386
|
-
if (!minimalMode) {
|
|
387
|
-
const cliSpinner = ora('Generating AI CLI configuration files...').start();
|
|
388
|
-
const cliFiles = await generateAICLIFiles(config, cwd);
|
|
389
|
-
if (cliFiles.length > 0) {
|
|
390
|
-
cliSpinner.succeed(`Generated ${cliFiles.length} AI CLI configuration files`);
|
|
391
|
-
for (const file of cliFiles) {
|
|
392
|
-
console.log(chalk.gray(` - ${path.relative(cwd, file)}`));
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
else {
|
|
396
|
-
cliSpinner.info('AI CLI files already exist (skipped)');
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
// Auto-setup Claude Code integration (MCP + skills)
|
|
400
|
-
if (!minimalMode) {
|
|
401
|
-
const claudeSpinner = ora('Checking Claude Code integration...').start();
|
|
402
|
-
try {
|
|
403
|
-
const { setupClaudeCodeIntegration } = await import('../core/claude-mcp.js');
|
|
404
|
-
const result = await setupClaudeCodeIntegration(cwd);
|
|
405
|
-
if (result.detected) {
|
|
406
|
-
claudeSpinner.succeed('Claude Code integration configured');
|
|
407
|
-
if (result.mcpConfigured) {
|
|
408
|
-
console.log(chalk.gray(' • MCP server added to .mcp.json'));
|
|
409
|
-
}
|
|
410
|
-
if (result.skillsInstalled.length > 0) {
|
|
411
|
-
console.log(chalk.gray(` • ${result.skillsInstalled.length} skills installed to .claude/commands/`));
|
|
412
|
-
}
|
|
413
|
-
if (result.agentTeamsEnabled) {
|
|
414
|
-
console.log(chalk.gray(' • Multi-agent teams enabled in .claude/settings.json'));
|
|
415
|
-
}
|
|
416
|
-
if (result.agentDefinitionsInstalled.length > 0) {
|
|
417
|
-
console.log(chalk.gray(` • ${result.agentDefinitionsInstalled.length} agent definitions installed to .claude/agents/`));
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
else {
|
|
421
|
-
claudeSpinner.info('Claude Code not detected (skipped)');
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
catch {
|
|
425
|
-
claudeSpinner.info('Claude Code integration skipped');
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
// Install Ralph shell scripts
|
|
429
|
-
try {
|
|
430
|
-
const { installRalphScripts } = await import('../core/ralph-scripts.js');
|
|
431
|
-
const scripts = await installRalphScripts(cwd);
|
|
432
|
-
if (scripts.length > 0) {
|
|
433
|
-
console.log(chalk.gray(` • ${scripts.length} Ralph scripts installed to .rulebook/scripts/`));
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
catch {
|
|
437
|
-
// Skip if Ralph scripts installation fails
|
|
438
|
-
}
|
|
439
|
-
// Create PLANS.md for session continuity
|
|
440
|
-
try {
|
|
441
|
-
const { initPlans } = await import('../core/plans-manager.js');
|
|
442
|
-
const created = await initPlans(cwd);
|
|
443
|
-
if (created) {
|
|
444
|
-
console.log(chalk.gray(' • PLANS.md created for session continuity'));
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
catch {
|
|
448
|
-
// Non-blocking
|
|
449
|
-
}
|
|
450
|
-
// Create AGENTS.override.md (never overwrites existing)
|
|
451
|
-
try {
|
|
452
|
-
const { initOverride } = await import('../core/override-manager.js');
|
|
453
|
-
const created = await initOverride(cwd);
|
|
454
|
-
if (created) {
|
|
455
|
-
console.log(chalk.gray(' • AGENTS.override.md created (add project-specific rules here)'));
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
catch {
|
|
459
|
-
// Non-blocking
|
|
460
|
-
}
|
|
461
|
-
// --add-sequential-thinking: inject into mcp.json if not already present
|
|
462
|
-
if (options.addSequentialThinking) {
|
|
463
|
-
try {
|
|
464
|
-
await addSequentialThinkingMcp(cwd);
|
|
465
|
-
console.log(chalk.gray(' • sequential-thinking MCP added to mcp.json'));
|
|
466
|
-
}
|
|
467
|
-
catch {
|
|
468
|
-
console.log(chalk.yellow(' ⚠ Could not add sequential-thinking MCP'));
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
if (minimalMode && minimalArtifacts.length > 0) {
|
|
472
|
-
console.log(chalk.green('\n✅ Essentials created:'));
|
|
473
|
-
for (const artifact of minimalArtifacts) {
|
|
474
|
-
console.log(chalk.gray(` - ${path.relative(cwd, artifact)}`));
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
// Clean up any accidental nested .rulebook/.rulebook directory
|
|
478
|
-
try {
|
|
479
|
-
const fsPromises = await import('fs/promises');
|
|
480
|
-
const accidentalDir = path.join(cwd, '.rulebook', '.rulebook');
|
|
481
|
-
if (existsSync(accidentalDir)) {
|
|
482
|
-
await fsPromises.rm(accidentalDir, { recursive: true, force: true });
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
catch {
|
|
486
|
-
// Ignore cleanup errors
|
|
487
|
-
}
|
|
488
|
-
// Detect if this looks like a workspace root and hint the user
|
|
489
|
-
const wsConfig = WorkspaceManager.findWorkspaceConfig(cwd);
|
|
490
|
-
if (wsConfig && wsConfig.projects.length > 1) {
|
|
491
|
-
console.log(chalk.yellow(`\n⚠ Workspace detected: "${wsConfig.name}" with ${wsConfig.projects.length} projects.`));
|
|
492
|
-
console.log(chalk.yellow(' The MCP server will auto-detect workspace mode. For explicit setup, run:'));
|
|
493
|
-
console.log(chalk.yellow(' rulebook workspace init'));
|
|
494
|
-
console.log(chalk.yellow(' Then run `rulebook init` inside each sub-project individually for best results.\n'));
|
|
495
|
-
}
|
|
496
|
-
console.log(chalk.bold.green('\n✨ Rulebook initialization complete!\n'));
|
|
497
|
-
console.log(chalk.gray('Next steps:'));
|
|
498
|
-
console.log(chalk.gray(' 1. Review AGENTS.md'));
|
|
499
|
-
console.log(chalk.gray(' 2. Review generated workflows in .github/workflows/'));
|
|
500
|
-
console.log(chalk.gray(' 3. Create .rulesignore if needed'));
|
|
501
|
-
console.log(chalk.gray(' 4. Set up /docs structure'));
|
|
502
|
-
console.log(chalk.gray(' 5. Run your AI assistant with the new rules\n'));
|
|
503
|
-
}
|
|
504
|
-
catch (error) {
|
|
505
|
-
console.error(chalk.red('\n❌ Error:'), error);
|
|
506
|
-
process.exit(1);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
export async function validateCommand() {
|
|
510
|
-
try {
|
|
511
|
-
const cwd = process.cwd();
|
|
512
|
-
console.log(chalk.bold.blue('\n🔍 Rulebook Project Validator\n'));
|
|
513
|
-
const spinner = ora('Validating project structure...').start();
|
|
514
|
-
// Import validator
|
|
515
|
-
const { validateProject } = await import('../core/validator.js');
|
|
516
|
-
const result = await validateProject(cwd);
|
|
517
|
-
spinner.stop();
|
|
518
|
-
// Display results
|
|
519
|
-
if (result.valid) {
|
|
520
|
-
console.log(chalk.green(`\n✅ Validation passed! Score: ${result.score}/100\n`));
|
|
521
|
-
}
|
|
522
|
-
else {
|
|
523
|
-
console.log(chalk.red(`\n❌ Validation failed. Score: ${result.score}/100\n`));
|
|
524
|
-
}
|
|
525
|
-
// Show errors
|
|
526
|
-
if (result.errors.length > 0) {
|
|
527
|
-
console.log(chalk.red.bold('Errors:'));
|
|
528
|
-
for (const error of result.errors) {
|
|
529
|
-
console.log(chalk.red(` ❌ ${error.category}: ${error.message}`));
|
|
530
|
-
if (error.file) {
|
|
531
|
-
console.log(chalk.gray(` File: ${error.file}`));
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
console.log('');
|
|
535
|
-
}
|
|
536
|
-
// Show warnings
|
|
537
|
-
if (result.warnings.length > 0) {
|
|
538
|
-
console.log(chalk.yellow.bold('Warnings:'));
|
|
539
|
-
for (const warning of result.warnings) {
|
|
540
|
-
console.log(chalk.yellow(` ⚠️ ${warning.category}: ${warning.message}`));
|
|
541
|
-
if (warning.file) {
|
|
542
|
-
console.log(chalk.gray(` File: ${warning.file}`));
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
console.log('');
|
|
546
|
-
}
|
|
547
|
-
// Summary
|
|
548
|
-
if (result.valid && result.warnings.length === 0) {
|
|
549
|
-
console.log(chalk.green('🎉 Perfect! Your project follows all rulebook standards.\n'));
|
|
550
|
-
}
|
|
551
|
-
else if (result.valid) {
|
|
552
|
-
console.log(chalk.yellow(`✅ Project is valid but has ${result.warnings.length} warnings to address.\n`));
|
|
553
|
-
}
|
|
554
|
-
else {
|
|
555
|
-
console.log(chalk.red(`❌ Project has ${result.errors.length} errors that must be fixed.\n`));
|
|
556
|
-
console.log(chalk.gray('Run "rulebook init" to set up missing standards.\n'));
|
|
557
|
-
}
|
|
558
|
-
process.exit(result.valid ? 0 : 1);
|
|
559
|
-
}
|
|
560
|
-
catch (error) {
|
|
561
|
-
console.error(chalk.red('\n❌ Validation error:'), error);
|
|
562
|
-
process.exit(1);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
export async function workflowsCommand() {
|
|
566
|
-
try {
|
|
567
|
-
const cwd = process.cwd();
|
|
568
|
-
console.log(chalk.bold.blue('\n📦 Generating GitHub Actions Workflows\n'));
|
|
569
|
-
// Detect project
|
|
570
|
-
const spinner = ora('Detecting project languages...').start();
|
|
571
|
-
const detection = await detectProject(cwd);
|
|
572
|
-
spinner.succeed('Detection complete');
|
|
573
|
-
if (detection.languages.length === 0) {
|
|
574
|
-
console.log(chalk.yellow('\n⚠ No languages detected. Cannot generate workflows.'));
|
|
575
|
-
console.log(chalk.gray('Ensure you have project files (Cargo.toml, package.json, etc.)\n'));
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
const config = {
|
|
579
|
-
languages: detection.languages.map((l) => l.language),
|
|
580
|
-
modules: [],
|
|
581
|
-
ides: [],
|
|
582
|
-
projectType: 'application',
|
|
583
|
-
coverageThreshold: 95,
|
|
584
|
-
strictDocs: true,
|
|
585
|
-
generateWorkflows: true,
|
|
586
|
-
};
|
|
587
|
-
const workflowSpinner = ora('Generating workflows...').start();
|
|
588
|
-
const workflows = await generateWorkflows(config, cwd);
|
|
589
|
-
workflowSpinner.succeed(`Generated ${workflows.length} workflow files`);
|
|
590
|
-
console.log(chalk.green('\n✅ Workflows generated:\n'));
|
|
591
|
-
for (const workflow of workflows) {
|
|
592
|
-
console.log(chalk.gray(` - ${path.relative(cwd, workflow)}`));
|
|
593
|
-
}
|
|
594
|
-
console.log(chalk.bold.green('\n✨ Done!\n'));
|
|
595
|
-
}
|
|
596
|
-
catch (error) {
|
|
597
|
-
console.error(chalk.red('\n❌ Error:'), error);
|
|
598
|
-
process.exit(1);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
export async function checkDepsCommand() {
|
|
602
|
-
try {
|
|
603
|
-
const cwd = process.cwd();
|
|
604
|
-
console.log(chalk.bold.blue('\n🔍 Checking Dependencies\n'));
|
|
605
|
-
const spinner = ora('Analyzing dependencies...').start();
|
|
606
|
-
const { checkDependencies } = await import('../core/dependency-checker.js');
|
|
607
|
-
const result = await checkDependencies(cwd);
|
|
608
|
-
spinner.succeed('Analysis complete');
|
|
609
|
-
console.log('');
|
|
610
|
-
console.log(chalk.bold('Dependency Summary:'));
|
|
611
|
-
console.log(chalk.gray(` Total: ${result.total}`));
|
|
612
|
-
console.log(chalk.green(` Up-to-date: ${result.upToDate}`));
|
|
613
|
-
if (result.outdated.length > 0) {
|
|
614
|
-
console.log(chalk.yellow(` Outdated: ${result.outdated.length}`));
|
|
615
|
-
}
|
|
616
|
-
if (result.vulnerable.length > 0) {
|
|
617
|
-
console.log(chalk.red(` Vulnerable: ${result.vulnerable.length}`));
|
|
618
|
-
}
|
|
619
|
-
// Show outdated
|
|
620
|
-
if (result.outdated.length > 0) {
|
|
621
|
-
console.log('');
|
|
622
|
-
console.log(chalk.yellow.bold('Outdated Dependencies:'));
|
|
623
|
-
for (const dep of result.outdated.slice(0, 10)) {
|
|
624
|
-
console.log(chalk.yellow(` ↑ ${dep.name}: ${dep.current} → ${dep.latest}`));
|
|
625
|
-
}
|
|
626
|
-
if (result.outdated.length > 10) {
|
|
627
|
-
console.log(chalk.gray(` ... and ${result.outdated.length - 10} more`));
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
// Show vulnerable
|
|
631
|
-
if (result.vulnerable.length > 0) {
|
|
632
|
-
console.log('');
|
|
633
|
-
console.log(chalk.red.bold('Vulnerable Dependencies:'));
|
|
634
|
-
for (const vuln of result.vulnerable.slice(0, 5)) {
|
|
635
|
-
console.log(chalk.red(` ❌ ${vuln.name} (${vuln.severity}): ${vuln.title}`));
|
|
636
|
-
}
|
|
637
|
-
if (result.vulnerable.length > 5) {
|
|
638
|
-
console.log(chalk.gray(` ... and ${result.vulnerable.length - 5} more`));
|
|
639
|
-
}
|
|
640
|
-
console.log('');
|
|
641
|
-
console.log(chalk.red.bold('⚠️ SECURITY: Update vulnerable dependencies immediately!'));
|
|
642
|
-
}
|
|
643
|
-
console.log('');
|
|
644
|
-
if (result.outdated.length === 0 && result.vulnerable.length === 0) {
|
|
645
|
-
console.log(chalk.green('✅ All dependencies are up-to-date and secure!\n'));
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
catch (error) {
|
|
649
|
-
console.error(chalk.red('\n❌ Error checking dependencies:'), error);
|
|
650
|
-
process.exit(1);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
export async function checkCoverageCommand(options) {
|
|
654
|
-
try {
|
|
655
|
-
const cwd = process.cwd();
|
|
656
|
-
const threshold = options.threshold || 95;
|
|
657
|
-
console.log(chalk.bold.blue('\n📊 Checking Test Coverage\n'));
|
|
658
|
-
const spinner = ora('Running coverage analysis...').start();
|
|
659
|
-
const { checkCoverage } = await import('../core/coverage-checker.js');
|
|
660
|
-
const result = await checkCoverage(cwd, threshold);
|
|
661
|
-
spinner.succeed('Coverage analysis complete');
|
|
662
|
-
console.log('');
|
|
663
|
-
console.log(chalk.bold('Coverage Summary:'));
|
|
664
|
-
console.log(chalk.gray(` Threshold: ${threshold}%`));
|
|
665
|
-
console.log(result.meetsThreshold
|
|
666
|
-
? chalk.green(` Actual: ${result.percentage.toFixed(2)}% ✅`)
|
|
667
|
-
: chalk.red(` Actual: ${result.percentage.toFixed(2)}% ❌`));
|
|
668
|
-
if (result.details) {
|
|
669
|
-
console.log('');
|
|
670
|
-
console.log(chalk.bold('Details:'));
|
|
671
|
-
if (result.details.lines) {
|
|
672
|
-
console.log(chalk.gray(` Lines: ${result.details.lines.toFixed(2)}%`));
|
|
673
|
-
}
|
|
674
|
-
if (result.details.statements) {
|
|
675
|
-
console.log(chalk.gray(` Statements: ${result.details.statements.toFixed(2)}%`));
|
|
676
|
-
}
|
|
677
|
-
if (result.details.functions) {
|
|
678
|
-
console.log(chalk.gray(` Functions: ${result.details.functions.toFixed(2)}%`));
|
|
679
|
-
}
|
|
680
|
-
if (result.details.branches) {
|
|
681
|
-
console.log(chalk.gray(` Branches: ${result.details.branches.toFixed(2)}%`));
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
console.log('');
|
|
685
|
-
if (result.meetsThreshold) {
|
|
686
|
-
console.log(chalk.green(`✅ Coverage meets threshold of ${threshold}%!\n`));
|
|
687
|
-
}
|
|
688
|
-
else {
|
|
689
|
-
console.log(chalk.red(`❌ Coverage ${result.percentage.toFixed(2)}% is below threshold ${threshold}%\n`));
|
|
690
|
-
console.log(chalk.gray('Add more tests to increase coverage.\n'));
|
|
691
|
-
process.exit(1);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
catch (error) {
|
|
695
|
-
console.error(chalk.red('\n❌ Error checking coverage:'), error);
|
|
696
|
-
process.exit(1);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
export async function generateDocsCommand(options) {
|
|
700
|
-
try {
|
|
701
|
-
const cwd = process.cwd();
|
|
702
|
-
console.log(chalk.bold.blue('\n📚 Generate Documentation Structure\n'));
|
|
703
|
-
let config;
|
|
704
|
-
if (options.yes) {
|
|
705
|
-
config = {
|
|
706
|
-
projectName: path.basename(cwd),
|
|
707
|
-
description: 'A modern software project',
|
|
708
|
-
author: 'Your Name',
|
|
709
|
-
email: '',
|
|
710
|
-
license: 'MIT',
|
|
711
|
-
};
|
|
712
|
-
console.log(chalk.blue('Using defaults...\n'));
|
|
713
|
-
}
|
|
714
|
-
else {
|
|
715
|
-
const { promptDocsConfig } = await import('./docs-prompts.js');
|
|
716
|
-
config = await promptDocsConfig();
|
|
717
|
-
}
|
|
718
|
-
const spinner = ora('Generating documentation structure...').start();
|
|
719
|
-
const { generateDocsStructure } = await import('../core/docs-generator.js');
|
|
720
|
-
const generatedFiles = await generateDocsStructure(config, cwd);
|
|
721
|
-
spinner.succeed(`Generated ${generatedFiles.length} files`);
|
|
722
|
-
console.log('');
|
|
723
|
-
console.log(chalk.green('✅ Files created:\n'));
|
|
724
|
-
for (const file of generatedFiles) {
|
|
725
|
-
console.log(chalk.gray(` - ${path.relative(cwd, file)}`));
|
|
726
|
-
}
|
|
727
|
-
console.log('');
|
|
728
|
-
console.log(chalk.bold.green('✨ Documentation structure ready!\n'));
|
|
729
|
-
console.log(chalk.gray('Next steps:'));
|
|
730
|
-
console.log(chalk.gray(' 1. Review and customize generated files'));
|
|
731
|
-
console.log(chalk.gray(' 2. Add your project-specific content'));
|
|
732
|
-
console.log(chalk.gray(' 3. Update ROADMAP.md with your milestones'));
|
|
733
|
-
console.log(chalk.gray(' 4. Document architecture in ARCHITECTURE.md\n'));
|
|
734
|
-
}
|
|
735
|
-
catch (error) {
|
|
736
|
-
console.error(chalk.red('\n❌ Error generating docs:'), error);
|
|
737
|
-
process.exit(1);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
export async function versionCommand(options) {
|
|
741
|
-
try {
|
|
742
|
-
const cwd = process.cwd();
|
|
743
|
-
console.log(chalk.bold.blue('\n📦 Version Bump\n'));
|
|
744
|
-
const { bumpProjectVersion } = await import('../core/version-bumper.js');
|
|
745
|
-
const spinner = ora('Bumping version...').start();
|
|
746
|
-
const result = await bumpProjectVersion(cwd, options.type);
|
|
747
|
-
spinner.succeed(`Version bumped: ${result.oldVersion} → ${result.newVersion}`);
|
|
748
|
-
console.log('');
|
|
749
|
-
console.log(chalk.green('✅ Files updated:\n'));
|
|
750
|
-
result.filesUpdated.forEach((file) => {
|
|
751
|
-
console.log(chalk.gray(` - ${file}`));
|
|
752
|
-
});
|
|
753
|
-
console.log('');
|
|
754
|
-
console.log(chalk.yellow('Next steps:'));
|
|
755
|
-
console.log(chalk.gray(' 1. Review changes'));
|
|
756
|
-
console.log(chalk.gray(' 2. Update CHANGELOG.md (or run: rulebook changelog)'));
|
|
757
|
-
console.log(chalk.gray(` 3. Commit: git add . && git commit -m "chore: Release version ${result.newVersion}"`));
|
|
758
|
-
console.log(chalk.gray(` 4. Tag: git tag -a v${result.newVersion} -m "Release v${result.newVersion}"`));
|
|
759
|
-
}
|
|
760
|
-
catch (error) {
|
|
761
|
-
console.error(chalk.red('\n❌ Error:'), error.message);
|
|
762
|
-
process.exit(1);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
export async function changelogCommand(options) {
|
|
766
|
-
try {
|
|
767
|
-
const cwd = process.cwd();
|
|
768
|
-
console.log(chalk.bold.blue('\n📝 Changelog Generation\n'));
|
|
769
|
-
const { generateChangelog, getCurrentVersion } = await import('../core/changelog-generator.js');
|
|
770
|
-
const version = options.version || (await getCurrentVersion(cwd));
|
|
771
|
-
if (!version) {
|
|
772
|
-
console.error(chalk.red('❌ Could not determine version'));
|
|
773
|
-
console.log(chalk.gray(' Specify version with --version flag'));
|
|
774
|
-
process.exit(1);
|
|
775
|
-
}
|
|
776
|
-
const spinner = ora('Generating changelog from commits...').start();
|
|
777
|
-
const section = await generateChangelog(cwd, version);
|
|
778
|
-
spinner.succeed(`Changelog generated for version ${version}`);
|
|
779
|
-
console.log('');
|
|
780
|
-
console.log(chalk.green('✅ Changelog sections:\n'));
|
|
781
|
-
if (section.breaking.length > 0) {
|
|
782
|
-
console.log(chalk.red(' Breaking Changes: ') + section.breaking.length);
|
|
783
|
-
}
|
|
784
|
-
if (section.added.length > 0) {
|
|
785
|
-
console.log(chalk.green(' Added: ') + section.added.length);
|
|
786
|
-
}
|
|
787
|
-
if (section.changed.length > 0) {
|
|
788
|
-
console.log(chalk.blue(' Changed: ') + section.changed.length);
|
|
789
|
-
}
|
|
790
|
-
if (section.fixed.length > 0) {
|
|
791
|
-
console.log(chalk.yellow(' Fixed: ') + section.fixed.length);
|
|
792
|
-
}
|
|
793
|
-
console.log('');
|
|
794
|
-
console.log(chalk.gray('CHANGELOG.md has been updated'));
|
|
795
|
-
console.log(chalk.gray('Review and edit as needed before committing'));
|
|
796
|
-
}
|
|
797
|
-
catch (error) {
|
|
798
|
-
console.error(chalk.red('\n❌ Error:'), error.message);
|
|
799
|
-
process.exit(1);
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
export async function healthCommand() {
|
|
803
|
-
try {
|
|
804
|
-
const cwd = process.cwd();
|
|
805
|
-
console.log(chalk.bold.blue('\n🏥 Project Health Check\n'));
|
|
806
|
-
const { calculateHealthScore } = await import('../core/health-scorer.js');
|
|
807
|
-
const spinner = ora('Analyzing project health...').start();
|
|
808
|
-
const health = await calculateHealthScore(cwd);
|
|
809
|
-
spinner.succeed('Health analysis complete');
|
|
810
|
-
console.log('');
|
|
811
|
-
console.log(chalk.bold(`Overall Health Score: ${health.overall}/100 (${health.grade})`));
|
|
812
|
-
console.log('');
|
|
813
|
-
console.log(chalk.bold('Category Scores:\n'));
|
|
814
|
-
console.log(` 📝 Documentation: ${health.categories.documentation}/100`);
|
|
815
|
-
console.log(` 🧪 Testing: ${health.categories.testing}/100`);
|
|
816
|
-
console.log(` 🎨 Code Quality: ${health.categories.quality}/100`);
|
|
817
|
-
console.log(` 🔒 Security: ${health.categories.security}/100`);
|
|
818
|
-
console.log(` 🔄 CI/CD: ${health.categories.cicd}/100`);
|
|
819
|
-
console.log(` 📦 Dependencies: ${health.categories.dependencies}/100`);
|
|
820
|
-
console.log(` 🤖 AGENTS.md: ${health.categories.agentsMd}/100`);
|
|
821
|
-
console.log(` 🔁 Ralph: ${health.categories.ralph}/100`);
|
|
822
|
-
console.log(` 🧠 Memory: ${health.categories.memory}/100`);
|
|
823
|
-
console.log('');
|
|
824
|
-
if (health.recommendations.length > 0) {
|
|
825
|
-
console.log(chalk.bold.yellow('Recommendations:\n'));
|
|
826
|
-
health.recommendations.forEach((rec) => {
|
|
827
|
-
console.log(chalk.yellow(` ${rec}`));
|
|
828
|
-
});
|
|
829
|
-
console.log('');
|
|
830
|
-
}
|
|
831
|
-
if (health.overall >= 90) {
|
|
832
|
-
console.log(chalk.green('🌟 Excellent! Your project is in great shape!'));
|
|
833
|
-
}
|
|
834
|
-
else if (health.overall >= 70) {
|
|
835
|
-
console.log(chalk.blue('👍 Good project health. A few improvements suggested.'));
|
|
836
|
-
}
|
|
837
|
-
else {
|
|
838
|
-
console.log(chalk.yellow('⚠️ Project health needs improvement.'));
|
|
839
|
-
console.log(chalk.gray(' Run: rulebook fix'));
|
|
840
|
-
console.log(chalk.gray(' To auto-fix common issues.'));
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
catch (error) {
|
|
844
|
-
console.error(chalk.red('\n❌ Error:'), error.message);
|
|
845
|
-
process.exit(1);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
export async function fixCommand() {
|
|
849
|
-
try {
|
|
850
|
-
const cwd = process.cwd();
|
|
851
|
-
console.log(chalk.bold.blue('\n🔧 Auto-Fix Common Issues\n'));
|
|
852
|
-
const { autoFixProject, autoFixLint } = await import('../core/auto-fixer.js');
|
|
853
|
-
const spinner = ora('Analyzing and fixing issues...').start();
|
|
854
|
-
const result = await autoFixProject(cwd);
|
|
855
|
-
spinner.succeed('Auto-fix complete');
|
|
856
|
-
console.log('');
|
|
857
|
-
if (result.applied.length > 0) {
|
|
858
|
-
console.log(chalk.green('✅ Fixed:\n'));
|
|
859
|
-
result.applied.forEach((fix) => {
|
|
860
|
-
console.log(chalk.gray(` - ${fix}`));
|
|
861
|
-
});
|
|
862
|
-
console.log('');
|
|
863
|
-
}
|
|
864
|
-
if (result.skipped.length > 0) {
|
|
865
|
-
console.log(chalk.blue('ℹ️ Skipped:\n'));
|
|
866
|
-
result.skipped.forEach((skip) => {
|
|
867
|
-
console.log(chalk.gray(` - ${skip}`));
|
|
868
|
-
});
|
|
869
|
-
console.log('');
|
|
870
|
-
}
|
|
871
|
-
if (result.failed.length > 0) {
|
|
872
|
-
console.log(chalk.yellow('⚠️ Failed:\n'));
|
|
873
|
-
result.failed.forEach((fail) => {
|
|
874
|
-
console.log(chalk.gray(` - ${fail}`));
|
|
875
|
-
});
|
|
876
|
-
console.log('');
|
|
877
|
-
}
|
|
878
|
-
// Try to fix lint errors
|
|
879
|
-
console.log(chalk.blue('🎨 Attempting to fix lint errors...\n'));
|
|
880
|
-
const lintFixed = await autoFixLint(cwd);
|
|
881
|
-
if (lintFixed) {
|
|
882
|
-
console.log(chalk.green('✅ Lint errors fixed'));
|
|
883
|
-
}
|
|
884
|
-
else {
|
|
885
|
-
console.log(chalk.gray('ℹ️ No lint auto-fix available'));
|
|
886
|
-
}
|
|
887
|
-
console.log('');
|
|
888
|
-
console.log(chalk.bold.green('✨ Auto-fix complete!\n'));
|
|
889
|
-
console.log(chalk.gray('Run: rulebook health'));
|
|
890
|
-
console.log(chalk.gray('To check updated health score.'));
|
|
891
|
-
}
|
|
892
|
-
catch (error) {
|
|
893
|
-
console.error(chalk.red('\n❌ Error:'), error.message);
|
|
894
|
-
process.exit(1);
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
export async function watcherCommand() {
|
|
898
|
-
try {
|
|
899
|
-
const cwd = process.cwd();
|
|
900
|
-
const { startWatcher } = await import('../core/watcher.js');
|
|
901
|
-
console.log(chalk.bold.blue('\n🚀 Starting Modern Console Watcher\n'));
|
|
902
|
-
console.log(chalk.gray('Full-screen interface with system monitoring'));
|
|
903
|
-
console.log(chalk.gray('Press Ctrl+C or F10 to exit\n'));
|
|
904
|
-
await startWatcher(cwd);
|
|
905
|
-
}
|
|
906
|
-
catch (error) {
|
|
907
|
-
console.error(chalk.red('\n❌ Watcher error:'), error);
|
|
908
|
-
process.exit(1);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
export async function agentCommand(options) {
|
|
912
|
-
try {
|
|
913
|
-
const cwd = process.cwd();
|
|
914
|
-
const { startAgent } = await import('../core/agent-manager.js');
|
|
915
|
-
console.log(chalk.bold.blue('\n🤖 Starting Rulebook Agent\n'));
|
|
916
|
-
const agentOptions = {
|
|
917
|
-
dryRun: options.dryRun || false,
|
|
918
|
-
tool: options.tool,
|
|
919
|
-
maxIterations: options.iterations || 10,
|
|
920
|
-
watchMode: options.watch || false,
|
|
921
|
-
};
|
|
922
|
-
if (agentOptions.dryRun) {
|
|
923
|
-
console.log(chalk.yellow('🔍 DRY RUN MODE - No actual changes will be made\n'));
|
|
924
|
-
}
|
|
925
|
-
await startAgent(cwd, agentOptions);
|
|
926
|
-
}
|
|
927
|
-
catch (error) {
|
|
928
|
-
console.error(chalk.red('\n❌ Agent error:'), error);
|
|
929
|
-
process.exit(1);
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
export async function configCommand(options) {
|
|
933
|
-
try {
|
|
934
|
-
const cwd = process.cwd();
|
|
935
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
936
|
-
const configManager = createConfigManager(cwd);
|
|
937
|
-
if (options.show) {
|
|
938
|
-
const summary = await configManager.getConfigSummary();
|
|
939
|
-
console.log(chalk.bold.blue('\n⚙️ Rulebook Configuration\n'));
|
|
940
|
-
console.log(chalk.white(`Version: ${summary.version}`));
|
|
941
|
-
console.log(chalk.white(`Project ID: ${summary.projectId}`));
|
|
942
|
-
console.log(chalk.white(`Coverage Threshold: ${summary.coverageThreshold}%`));
|
|
943
|
-
console.log(chalk.white(`CLI Tools: ${summary.cliTools.join(', ') || 'None detected'}`));
|
|
944
|
-
console.log(chalk.white(`Enabled Features: ${summary.enabledFeatures.join(', ')}`));
|
|
945
|
-
}
|
|
946
|
-
else if (options.feature && typeof options.enable === 'boolean') {
|
|
947
|
-
await configManager.toggleFeature(options.feature, options.enable);
|
|
948
|
-
console.log(chalk.green(`✅ Feature '${options.feature}' ${options.enable ? 'enabled' : 'disabled'}`));
|
|
949
|
-
}
|
|
950
|
-
else if (options.set) {
|
|
951
|
-
const [key, value] = options.set.split('=');
|
|
952
|
-
if (!key || !value) {
|
|
953
|
-
console.error(chalk.red('Invalid set format. Use: --set key=value'));
|
|
954
|
-
process.exit(1);
|
|
955
|
-
}
|
|
956
|
-
// Handle different value types
|
|
957
|
-
let parsedValue = value;
|
|
958
|
-
if (value === 'true')
|
|
959
|
-
parsedValue = true;
|
|
960
|
-
else if (value === 'false')
|
|
961
|
-
parsedValue = false;
|
|
962
|
-
else if (!isNaN(Number(value)))
|
|
963
|
-
parsedValue = Number(value);
|
|
964
|
-
await configManager.updateConfig({ [key]: parsedValue });
|
|
965
|
-
console.log(chalk.green(`✅ Configuration updated: ${key} = ${value}`));
|
|
966
|
-
}
|
|
967
|
-
else {
|
|
968
|
-
console.log(chalk.bold.blue('\n⚙️ Rulebook Configuration\n'));
|
|
969
|
-
console.log(chalk.gray('Available commands:'));
|
|
970
|
-
console.log(chalk.gray(' --show Show current configuration'));
|
|
971
|
-
console.log(chalk.gray(' --set key=value Set configuration value'));
|
|
972
|
-
console.log(chalk.gray(' --feature name --enable Enable a feature'));
|
|
973
|
-
console.log(chalk.gray(' --feature name --disable Disable a feature'));
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
catch (error) {
|
|
977
|
-
console.error(chalk.red('\n❌ Config error:'), error);
|
|
978
|
-
process.exit(1);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
/**
|
|
982
|
-
* Resolve a TaskManager for a specific project.
|
|
983
|
-
*
|
|
984
|
-
* Resolution order:
|
|
985
|
-
* 1. Explicit --project flag → find workspace config from cwd, route to named project
|
|
986
|
-
* 2. Auto-detect → walk up from cwd to find workspace, match cwd to a project
|
|
987
|
-
* 3. Fallback → single-project from cwd (no workspace)
|
|
988
|
-
*/
|
|
989
|
-
async function resolveTaskManager(cwd, options) {
|
|
990
|
-
const { createTaskManager } = await import('../core/task-manager.js');
|
|
991
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
992
|
-
const { isAbsolute, resolve } = await import('path');
|
|
993
|
-
// Helper: build TaskManager from a resolved project root
|
|
994
|
-
const buildFromProjectRoot = async (projectRoot, label) => {
|
|
995
|
-
const configManager = createConfigManager(projectRoot);
|
|
996
|
-
const config = await configManager.loadConfig();
|
|
997
|
-
const rulebookDir = config.rulebookDir || '.rulebook';
|
|
998
|
-
return { taskManager: createTaskManager(projectRoot, rulebookDir), projectLabel: label };
|
|
999
|
-
};
|
|
1000
|
-
// 1. Explicit --project flag
|
|
1001
|
-
if (options?.project) {
|
|
1002
|
-
const ws = WorkspaceManager.findWorkspaceFromCwd(cwd);
|
|
1003
|
-
if (!ws) {
|
|
1004
|
-
console.error(chalk.red('No workspace found. Run `rulebook workspace init` first.'));
|
|
1005
|
-
process.exit(1);
|
|
1006
|
-
}
|
|
1007
|
-
const project = ws.config.projects.find((p) => p.name === options.project);
|
|
1008
|
-
if (!project) {
|
|
1009
|
-
console.error(chalk.red(`Project "${options.project}" not found in workspace.`));
|
|
1010
|
-
console.error(chalk.gray(`Available: ${ws.config.projects.map((p) => p.name).join(', ')}`));
|
|
1011
|
-
process.exit(1);
|
|
1012
|
-
}
|
|
1013
|
-
const projectRoot = isAbsolute(project.path) ? project.path : resolve(ws.root, project.path);
|
|
1014
|
-
return buildFromProjectRoot(projectRoot, project.name);
|
|
1015
|
-
}
|
|
1016
|
-
// 2. Auto-detect: walk up from cwd to find workspace and match project
|
|
1017
|
-
const resolved = WorkspaceManager.resolveProjectFromCwd(cwd);
|
|
1018
|
-
if (resolved) {
|
|
1019
|
-
const project = resolved.config.projects.find((p) => p.name === resolved.projectName);
|
|
1020
|
-
if (project) {
|
|
1021
|
-
const projectRoot = isAbsolute(project.path)
|
|
1022
|
-
? project.path
|
|
1023
|
-
: resolve(resolved.root, project.path);
|
|
1024
|
-
return buildFromProjectRoot(projectRoot, project.name);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
// 3. Fallback: single-project from cwd
|
|
1028
|
-
const configManager = createConfigManager(cwd);
|
|
1029
|
-
const config = await configManager.loadConfig();
|
|
1030
|
-
const rulebookDir = config.rulebookDir || '.rulebook';
|
|
1031
|
-
return { taskManager: createTaskManager(cwd, rulebookDir) };
|
|
1032
|
-
}
|
|
1033
|
-
// Task management commands using Rulebook task system
|
|
1034
|
-
export async function taskCreateCommand(taskId, wsOptions) {
|
|
1035
|
-
try {
|
|
1036
|
-
const cwd = process.cwd();
|
|
1037
|
-
const { taskManager, projectLabel } = await resolveTaskManager(cwd, wsOptions);
|
|
1038
|
-
await taskManager.createTask(taskId);
|
|
1039
|
-
const prefix = projectLabel ? `[${projectLabel}] ` : '';
|
|
1040
|
-
console.log(chalk.green(`✅ ${prefix}Task ${taskId} created successfully`));
|
|
1041
|
-
console.log(chalk.yellow('\n⚠️ Remember to:'));
|
|
1042
|
-
console.log(chalk.gray(' 1. Fill in proposal.md (minimum 20 characters in "Why" section)'));
|
|
1043
|
-
console.log(chalk.gray(' 3. Add tasks to tasks.md'));
|
|
1044
|
-
console.log(chalk.gray(' 4. Create spec deltas in specs/*/spec.md'));
|
|
1045
|
-
console.log(chalk.gray(' 5. Validate with: rulebook task validate ' + taskId));
|
|
1046
|
-
}
|
|
1047
|
-
catch (error) {
|
|
1048
|
-
console.error(chalk.red(`❌ Failed to create task: ${error.message}`));
|
|
1049
|
-
process.exit(1);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
export async function taskListCommand(includeArchived = false, wsOptions) {
|
|
1053
|
-
try {
|
|
1054
|
-
const cwd = process.cwd();
|
|
1055
|
-
// --all-projects: list tasks from every workspace project
|
|
1056
|
-
if (wsOptions?.allProjects) {
|
|
1057
|
-
const ws = WorkspaceManager.findWorkspaceFromCwd(cwd);
|
|
1058
|
-
if (!ws) {
|
|
1059
|
-
console.error(chalk.red('No workspace found. Run `rulebook workspace init` first.'));
|
|
1060
|
-
process.exit(1);
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
console.log(chalk.bold.blue(`\n📋 Workspace Tasks (${ws.config.name})\n`));
|
|
1064
|
-
const { createTaskManager } = await import('../core/task-manager.js');
|
|
1065
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
1066
|
-
const { isAbsolute, resolve } = await import('path');
|
|
1067
|
-
let totalTasks = 0;
|
|
1068
|
-
for (const project of ws.config.projects) {
|
|
1069
|
-
const projectRoot = isAbsolute(project.path)
|
|
1070
|
-
? project.path
|
|
1071
|
-
: resolve(ws.root, project.path);
|
|
1072
|
-
try {
|
|
1073
|
-
const configManager = createConfigManager(projectRoot);
|
|
1074
|
-
const config = await configManager.loadConfig();
|
|
1075
|
-
const rulebookDir = config.rulebookDir || '.rulebook';
|
|
1076
|
-
const taskManager = createTaskManager(projectRoot, rulebookDir);
|
|
1077
|
-
const tasks = await taskManager.listTasks(includeArchived);
|
|
1078
|
-
if (tasks.length > 0) {
|
|
1079
|
-
console.log(chalk.bold.cyan(` [${project.name}]`));
|
|
1080
|
-
for (const task of tasks) {
|
|
1081
|
-
const statusColor = task.status === 'completed'
|
|
1082
|
-
? chalk.green
|
|
1083
|
-
: task.status === 'in-progress'
|
|
1084
|
-
? chalk.yellow
|
|
1085
|
-
: task.status === 'blocked'
|
|
1086
|
-
? chalk.red
|
|
1087
|
-
: chalk.gray;
|
|
1088
|
-
console.log(` ${statusColor(task.status.padEnd(12))} ${chalk.white(task.id)} - ${chalk.gray(task.title)}`);
|
|
1089
|
-
}
|
|
1090
|
-
console.log('');
|
|
1091
|
-
totalTasks += tasks.length;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
catch {
|
|
1095
|
-
console.log(chalk.bold.cyan(` [${project.name}]`));
|
|
1096
|
-
console.log(chalk.gray(' No tasks or no .rulebook config'));
|
|
1097
|
-
console.log('');
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
console.log(chalk.gray(`${totalTasks} task(s) across ${ws.config.projects.length} project(s)`));
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
// Single project (optionally targeted via --project)
|
|
1104
|
-
const { taskManager, projectLabel } = await resolveTaskManager(cwd, wsOptions);
|
|
1105
|
-
const tasks = await taskManager.listTasks(includeArchived);
|
|
1106
|
-
if (tasks.length === 0) {
|
|
1107
|
-
console.log(chalk.gray('No tasks found'));
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
const header = projectLabel
|
|
1111
|
-
? `\n📋 Rulebook Tasks [${projectLabel}]\n`
|
|
1112
|
-
: '\n📋 Rulebook Tasks\n';
|
|
1113
|
-
console.log(chalk.bold.blue(header));
|
|
1114
|
-
const activeTasks = tasks.filter((t) => !t.archivedAt);
|
|
1115
|
-
const archivedTasks = tasks.filter((t) => t.archivedAt);
|
|
1116
|
-
if (activeTasks.length > 0) {
|
|
1117
|
-
console.log(chalk.bold('Active Tasks:'));
|
|
1118
|
-
for (const task of activeTasks) {
|
|
1119
|
-
const statusColor = task.status === 'completed'
|
|
1120
|
-
? chalk.green
|
|
1121
|
-
: task.status === 'in-progress'
|
|
1122
|
-
? chalk.yellow
|
|
1123
|
-
: task.status === 'blocked'
|
|
1124
|
-
? chalk.red
|
|
1125
|
-
: chalk.gray;
|
|
1126
|
-
console.log(` ${statusColor(task.status.padEnd(12))} ${chalk.white(task.id)} - ${chalk.gray(task.title)}`);
|
|
1127
|
-
}
|
|
1128
|
-
console.log('');
|
|
1129
|
-
}
|
|
1130
|
-
if (includeArchived && archivedTasks.length > 0) {
|
|
1131
|
-
console.log(chalk.bold('Archived Tasks:'));
|
|
1132
|
-
for (const task of archivedTasks) {
|
|
1133
|
-
console.log(` ${chalk.gray('archived'.padEnd(12))} ${chalk.white(task.id)} - ${chalk.gray(task.title)} ${chalk.dim(`(${task.archivedAt})`)}`);
|
|
1134
|
-
}
|
|
1135
|
-
console.log('');
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
catch (error) {
|
|
1139
|
-
console.error(chalk.red(`❌ Failed to list tasks: ${error.message}`));
|
|
1140
|
-
process.exit(1);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
export async function taskShowCommand(taskId, wsOptions) {
|
|
1144
|
-
try {
|
|
1145
|
-
const cwd = process.cwd();
|
|
1146
|
-
const { taskManager } = await resolveTaskManager(cwd, wsOptions);
|
|
1147
|
-
const task = await taskManager.showTask(taskId);
|
|
1148
|
-
if (!task) {
|
|
1149
|
-
console.error(chalk.red(`❌ Task ${taskId} not found`));
|
|
1150
|
-
process.exit(1);
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
|
-
console.log(chalk.bold.blue(`\n📋 Task: ${task.id}\n`));
|
|
1154
|
-
console.log(chalk.white(`Title: ${task.title}`));
|
|
1155
|
-
console.log(chalk.gray(`Status: ${task.status}`));
|
|
1156
|
-
console.log(chalk.gray(`Created: ${task.createdAt}`));
|
|
1157
|
-
console.log(chalk.gray(`Updated: ${task.updatedAt}`));
|
|
1158
|
-
if (task.archivedAt) {
|
|
1159
|
-
console.log(chalk.gray(`Archived: ${task.archivedAt}`));
|
|
1160
|
-
}
|
|
1161
|
-
console.log('');
|
|
1162
|
-
if (task.proposal) {
|
|
1163
|
-
console.log(chalk.bold('Proposal:'));
|
|
1164
|
-
console.log(chalk.gray(task.proposal.substring(0, 500) + (task.proposal.length > 500 ? '...' : '')));
|
|
1165
|
-
console.log('');
|
|
1166
|
-
}
|
|
1167
|
-
if (task.specs && Object.keys(task.specs).length > 0) {
|
|
1168
|
-
console.log(chalk.bold('Specs:'));
|
|
1169
|
-
for (const [module, spec] of Object.entries(task.specs)) {
|
|
1170
|
-
console.log(chalk.gray(` ${module}/spec.md (${spec.length} chars)`));
|
|
1171
|
-
}
|
|
1172
|
-
console.log('');
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
catch (error) {
|
|
1176
|
-
console.error(chalk.red(`❌ Failed to show task: ${error.message}`));
|
|
1177
|
-
process.exit(1);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
export async function taskValidateCommand(taskId, wsOptions) {
|
|
1181
|
-
try {
|
|
1182
|
-
const cwd = process.cwd();
|
|
1183
|
-
const { taskManager } = await resolveTaskManager(cwd, wsOptions);
|
|
1184
|
-
const validation = await taskManager.validateTask(taskId);
|
|
1185
|
-
if (validation.valid) {
|
|
1186
|
-
console.log(chalk.green(`✅ Task ${taskId} is valid`));
|
|
1187
|
-
if (validation.warnings.length > 0) {
|
|
1188
|
-
console.log(chalk.yellow('\n⚠️ Warnings:'));
|
|
1189
|
-
for (const warning of validation.warnings) {
|
|
1190
|
-
console.log(chalk.yellow(` - ${warning}`));
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
else {
|
|
1195
|
-
console.log(chalk.red(`❌ Task ${taskId} validation failed\n`));
|
|
1196
|
-
console.log(chalk.red('Errors:'));
|
|
1197
|
-
for (const error of validation.errors) {
|
|
1198
|
-
console.log(chalk.red(` - ${error}`));
|
|
1199
|
-
}
|
|
1200
|
-
if (validation.warnings.length > 0) {
|
|
1201
|
-
console.log(chalk.yellow('\n⚠️ Warnings:'));
|
|
1202
|
-
for (const warning of validation.warnings) {
|
|
1203
|
-
console.log(chalk.yellow(` - ${warning}`));
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
process.exit(1);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
catch (error) {
|
|
1210
|
-
console.error(chalk.red(`❌ Failed to validate task: ${error.message}`));
|
|
1211
|
-
process.exit(1);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
export async function taskArchiveCommand(taskId, skipValidation = false, wsOptions) {
|
|
1215
|
-
try {
|
|
1216
|
-
const cwd = process.cwd();
|
|
1217
|
-
const { taskManager, projectLabel } = await resolveTaskManager(cwd, wsOptions);
|
|
1218
|
-
await taskManager.archiveTask(taskId, skipValidation);
|
|
1219
|
-
const prefix = projectLabel ? `[${projectLabel}] ` : '';
|
|
1220
|
-
console.log(chalk.green(`✅ ${prefix}Task ${taskId} archived successfully`));
|
|
1221
|
-
}
|
|
1222
|
-
catch (error) {
|
|
1223
|
-
console.error(chalk.red(`❌ Failed to archive task: ${error.message}`));
|
|
1224
|
-
process.exit(1);
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
/**
|
|
1228
|
-
* Initialize MCP configuration in .rulebook file
|
|
1229
|
-
* Adds mcp block to .rulebook and creates/updates .cursor/mcp.json
|
|
1230
|
-
*/
|
|
1231
|
-
export async function mcpInitCommand(options) {
|
|
1232
|
-
const { findRulebookConfig } = await import('../mcp/rulebook-server.js');
|
|
1233
|
-
const { existsSync, readFileSync, writeFileSync, statSync } = await import('fs');
|
|
1234
|
-
const { join, dirname } = await import('path');
|
|
1235
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
1236
|
-
try {
|
|
1237
|
-
const cwd = process.cwd();
|
|
1238
|
-
// --- Workspace mode ---
|
|
1239
|
-
if (options?.workspace) {
|
|
1240
|
-
const wsConfig = WorkspaceManager.findWorkspaceConfig(cwd);
|
|
1241
|
-
if (!wsConfig) {
|
|
1242
|
-
console.error(chalk.red('No workspace found. Run `rulebook workspace init` first.'));
|
|
1243
|
-
process.exit(1);
|
|
1244
|
-
}
|
|
1245
|
-
const mcpArgs = ['-y', '@hivehub/rulebook@latest', 'mcp-server', '--workspace'];
|
|
1246
|
-
const mcpEntry = { command: 'npx', args: mcpArgs };
|
|
1247
|
-
// Write to .cursor/mcp.json if .cursor exists
|
|
1248
|
-
const cursorDir = join(cwd, '.cursor');
|
|
1249
|
-
if (existsSync(cursorDir)) {
|
|
1250
|
-
const mcpJsonPath = join(cursorDir, 'mcp.json');
|
|
1251
|
-
let mcpConfig = { mcpServers: {} };
|
|
1252
|
-
if (existsSync(mcpJsonPath)) {
|
|
1253
|
-
mcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf8'));
|
|
1254
|
-
}
|
|
1255
|
-
mcpConfig.mcpServers = mcpConfig.mcpServers ?? {};
|
|
1256
|
-
mcpConfig.mcpServers.rulebook = mcpEntry;
|
|
1257
|
-
writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
1258
|
-
console.log(chalk.green('✓ Workspace MCP initialized'));
|
|
1259
|
-
console.log(chalk.gray(` • Updated .cursor/mcp.json with --workspace flag`));
|
|
1260
|
-
}
|
|
1261
|
-
// Write to .mcp.json (Claude Code format)
|
|
1262
|
-
const mcpJsonPath = join(cwd, '.mcp.json');
|
|
1263
|
-
let mcpConfig = { mcpServers: {} };
|
|
1264
|
-
if (existsSync(mcpJsonPath)) {
|
|
1265
|
-
mcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf8'));
|
|
1266
|
-
}
|
|
1267
|
-
mcpConfig.mcpServers = mcpConfig.mcpServers ?? {};
|
|
1268
|
-
mcpConfig.mcpServers.rulebook = mcpEntry;
|
|
1269
|
-
writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
1270
|
-
console.log(chalk.green('✓ Workspace MCP initialized'));
|
|
1271
|
-
console.log(chalk.gray(` • Workspace: ${wsConfig.name} (${wsConfig.projects.length} projects)`));
|
|
1272
|
-
console.log(chalk.gray(` • Updated .mcp.json with --workspace flag`));
|
|
1273
|
-
console.log(chalk.gray(` • MCP server will manage all projects automatically`));
|
|
1274
|
-
return;
|
|
1275
|
-
}
|
|
1276
|
-
// --- Single-project mode (original behavior) ---
|
|
1277
|
-
let rulebookPath = findRulebookConfig(cwd);
|
|
1278
|
-
if (!rulebookPath) {
|
|
1279
|
-
rulebookPath = join(cwd, '.rulebook');
|
|
1280
|
-
const configManager = createConfigManager(cwd);
|
|
1281
|
-
await configManager.initializeConfig();
|
|
1282
|
-
}
|
|
1283
|
-
const projectRoot = dirname(rulebookPath);
|
|
1284
|
-
let configFilePath = rulebookPath;
|
|
1285
|
-
if (existsSync(rulebookPath)) {
|
|
1286
|
-
const stats = statSync(rulebookPath);
|
|
1287
|
-
if (stats.isDirectory()) {
|
|
1288
|
-
configFilePath = join(rulebookPath, 'rulebook.json');
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
let config = {};
|
|
1292
|
-
if (existsSync(configFilePath)) {
|
|
1293
|
-
const raw = readFileSync(configFilePath, 'utf8');
|
|
1294
|
-
config = JSON.parse(raw);
|
|
1295
|
-
}
|
|
1296
|
-
config.mcp = config.mcp ?? {};
|
|
1297
|
-
if (config.mcp.enabled === undefined)
|
|
1298
|
-
config.mcp.enabled = true;
|
|
1299
|
-
if (!config.mcp.tasksDir)
|
|
1300
|
-
config.mcp.tasksDir = '.rulebook/tasks';
|
|
1301
|
-
if (!config.mcp.archiveDir)
|
|
1302
|
-
config.mcp.archiveDir = '.rulebook/archive';
|
|
1303
|
-
writeFileSync(configFilePath, JSON.stringify(config, null, 2) + '\n');
|
|
1304
|
-
const cursorDir = join(projectRoot, '.cursor');
|
|
1305
|
-
if (existsSync(cursorDir)) {
|
|
1306
|
-
const mcpJsonPath = join(cursorDir, 'mcp.json');
|
|
1307
|
-
let mcpConfig = { mcpServers: {} };
|
|
1308
|
-
if (existsSync(mcpJsonPath)) {
|
|
1309
|
-
const raw = readFileSync(mcpJsonPath, 'utf8');
|
|
1310
|
-
mcpConfig = JSON.parse(raw);
|
|
1311
|
-
}
|
|
1312
|
-
mcpConfig.mcpServers = mcpConfig.mcpServers ?? {};
|
|
1313
|
-
mcpConfig.mcpServers.rulebook = {
|
|
1314
|
-
command: 'npx',
|
|
1315
|
-
args: ['-y', '@hivehub/rulebook@latest', 'mcp-server'],
|
|
1316
|
-
};
|
|
1317
|
-
writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
1318
|
-
console.log(chalk.green('✓ Rulebook MCP initialized'));
|
|
1319
|
-
console.log(chalk.gray(` • Updated .rulebook with MCP configuration`));
|
|
1320
|
-
console.log(chalk.gray(` • Updated .cursor/mcp.json`));
|
|
1321
|
-
console.log(chalk.gray(` • MCP server will find .rulebook automatically`));
|
|
1322
|
-
}
|
|
1323
|
-
else {
|
|
1324
|
-
console.log(chalk.green('✓ Rulebook MCP initialized'));
|
|
1325
|
-
console.log(chalk.gray(` • Updated .rulebook with MCP configuration`));
|
|
1326
|
-
console.log(chalk.gray(` • To use with Cursor, create .cursor/mcp.json manually`));
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
catch (error) {
|
|
1330
|
-
console.error(chalk.red(`\n❌ Failed to initialize MCP: ${error.message}`));
|
|
1331
|
-
console.error(error.stack);
|
|
1332
|
-
process.exit(1);
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
export async function mcpServerCommand() {
|
|
1336
|
-
try {
|
|
1337
|
-
const { startRulebookMcpServer } = await import('../mcp/rulebook-server.js');
|
|
1338
|
-
// CRITICAL: In stdio mode, stdout MUST contain ONLY JSON-RPC 2.0 messages
|
|
1339
|
-
// stdout must contain ONLY JSON-RPC 2.0 messages for MCP protocol
|
|
1340
|
-
// All logs must go to stderr
|
|
1341
|
-
// Use environment variable for debug: RULEBOOK_MCP_DEBUG=1
|
|
1342
|
-
if (process.env.RULEBOOK_MCP_DEBUG === '1') {
|
|
1343
|
-
console.error(chalk.gray('[rulebook-mcp] Starting MCP server with stdio transport'));
|
|
1344
|
-
console.error(chalk.gray('[rulebook-mcp] Server will find .rulebook automatically'));
|
|
1345
|
-
}
|
|
1346
|
-
await startRulebookMcpServer();
|
|
1347
|
-
}
|
|
1348
|
-
catch (error) {
|
|
1349
|
-
// Errors always go to stderr
|
|
1350
|
-
console.error(chalk.red(`\n❌ Failed to start MCP server: ${error.message}`));
|
|
1351
|
-
console.error(error.stack);
|
|
1352
|
-
process.exit(1);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
// Legacy tasks command (deprecated - use task commands instead)
|
|
1356
|
-
export async function tasksCommand(options) {
|
|
1357
|
-
console.log(chalk.yellow('⚠️ The `tasks` command is deprecated. Use `rulebook task` commands instead.'));
|
|
1358
|
-
console.log(chalk.gray(' - rulebook task list'));
|
|
1359
|
-
console.log(chalk.gray(' - rulebook task show <task-id>'));
|
|
1360
|
-
console.log(chalk.gray(' - rulebook task create <task-id>'));
|
|
1361
|
-
console.log(chalk.gray(' - rulebook task validate <task-id>'));
|
|
1362
|
-
console.log(chalk.gray(' - rulebook task archive <task-id>'));
|
|
1363
|
-
if (options.tree || options.current || options.status) {
|
|
1364
|
-
console.log(chalk.red('\n❌ Legacy commands are no longer supported.'));
|
|
1365
|
-
console.log(chalk.yellow('Please use the Rulebook task system.'));
|
|
1366
|
-
process.exit(1);
|
|
1367
|
-
}
|
|
1368
|
-
// Fallback to list tasks
|
|
1369
|
-
await taskListCommand(false);
|
|
1370
|
-
}
|
|
1371
|
-
export async function updateCommand(options) {
|
|
1372
|
-
try {
|
|
1373
|
-
const cwd = process.cwd();
|
|
1374
|
-
// Auto-detect workspace: if inside a workspace, update ALL projects
|
|
1375
|
-
const ws = WorkspaceManager.findWorkspaceFromCwd(cwd);
|
|
1376
|
-
if (ws && ws.config.projects.length > 1) {
|
|
1377
|
-
console.log(chalk.bold.blue('\n🔄 Rulebook Workspace Update\n'));
|
|
1378
|
-
console.log(chalk.gray(`Workspace "${ws.config.name}" detected — updating ${ws.config.projects.length} projects\n`));
|
|
1379
|
-
const { isAbsolute, resolve, join } = await import('path');
|
|
1380
|
-
const fsPromises = await import('fs/promises');
|
|
1381
|
-
let updatedCount = 0;
|
|
1382
|
-
// Build workspace project list for the template
|
|
1383
|
-
const projectListMd = ws.config.projects
|
|
1384
|
-
.map((p) => {
|
|
1385
|
-
const root = isAbsolute(p.path) ? p.path : resolve(ws.root, p.path);
|
|
1386
|
-
return `- **${p.name}** → \`${root}\``;
|
|
1387
|
-
})
|
|
1388
|
-
.join('\n');
|
|
1389
|
-
// Load WORKSPACE.md template
|
|
1390
|
-
const { getDefaultTemplatesPath } = await import('../core/skills-manager.js');
|
|
1391
|
-
let workspaceTplContent = '';
|
|
1392
|
-
try {
|
|
1393
|
-
const tplPath = join(getDefaultTemplatesPath(), 'core', 'WORKSPACE.md');
|
|
1394
|
-
workspaceTplContent = await fsPromises.readFile(tplPath, 'utf-8');
|
|
1395
|
-
}
|
|
1396
|
-
catch {
|
|
1397
|
-
// Template not available — skip
|
|
1398
|
-
}
|
|
1399
|
-
for (const project of ws.config.projects) {
|
|
1400
|
-
const projectRoot = isAbsolute(project.path)
|
|
1401
|
-
? project.path
|
|
1402
|
-
: resolve(ws.root, project.path);
|
|
1403
|
-
console.log(chalk.bold.cyan(`\n━━━ [${project.name}] ${projectRoot} ━━━\n`));
|
|
1404
|
-
try {
|
|
1405
|
-
await updateSingleProject(projectRoot, options);
|
|
1406
|
-
// Inject WORKSPACE.md spec into this project's .rulebook/specs/
|
|
1407
|
-
if (workspaceTplContent) {
|
|
1408
|
-
const specsDir = join(projectRoot, '.rulebook', 'specs');
|
|
1409
|
-
await fsPromises.mkdir(specsDir, { recursive: true });
|
|
1410
|
-
const rendered = workspaceTplContent
|
|
1411
|
-
.replace('{{DEFAULT_PROJECT}}', ws.config.defaultProject ?? ws.config.projects[0]?.name ?? '')
|
|
1412
|
-
.replace('{{WORKSPACE_PROJECTS}}', projectListMd);
|
|
1413
|
-
await fsPromises.writeFile(join(specsDir, 'WORKSPACE.md'), rendered, 'utf-8');
|
|
1414
|
-
}
|
|
1415
|
-
updatedCount++;
|
|
1416
|
-
}
|
|
1417
|
-
catch (error) {
|
|
1418
|
-
console.error(chalk.red(` ❌ Failed to update ${project.name}: ${error.message}`));
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
console.log(chalk.bold.green(`\n✅ Workspace update complete — ${updatedCount}/${ws.config.projects.length} projects updated\n`));
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
// Single project update
|
|
1425
|
-
console.log(chalk.bold.blue('\n🔄 Rulebook Update Tool\n'));
|
|
1426
|
-
console.log(chalk.gray('This will update your AGENTS.md and .rulebook to the latest version\n'));
|
|
1427
|
-
await updateSingleProject(cwd, options);
|
|
1428
|
-
}
|
|
1429
|
-
catch (error) {
|
|
1430
|
-
console.error(chalk.red('\n❌ Update failed:'), error);
|
|
1431
|
-
process.exit(1);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
/** Update a single project at the given root directory. */
|
|
1435
|
-
async function updateSingleProject(cwd, options) {
|
|
1436
|
-
// Detect project
|
|
1437
|
-
const spinner = ora('Detecting project structure...').start();
|
|
1438
|
-
const detection = await detectProject(cwd);
|
|
1439
|
-
spinner.succeed('Project detection complete');
|
|
1440
|
-
// Show detected languages
|
|
1441
|
-
if (detection.languages.length > 0) {
|
|
1442
|
-
console.log(chalk.green('\n✓ Detected languages:'));
|
|
1443
|
-
for (const lang of detection.languages) {
|
|
1444
|
-
console.log(` - ${lang.language} (${(lang.confidence * 100).toFixed(0)}% confidence)`);
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
// Check for existing AGENTS.md
|
|
1448
|
-
if (!detection.existingAgents) {
|
|
1449
|
-
console.log(chalk.yellow('\n⚠ No AGENTS.md found. Use "rulebook init" instead.'));
|
|
1450
|
-
process.exit(0);
|
|
1451
|
-
}
|
|
1452
|
-
console.log(chalk.green(`\n✓ Found existing AGENTS.md with ${detection.existingAgents.blocks.length} blocks`));
|
|
1453
|
-
// Get existing blocks to preserve user customizations
|
|
1454
|
-
const existingBlocks = detection.existingAgents.blocks.map((b) => b.name);
|
|
1455
|
-
console.log(chalk.gray(` Existing blocks: ${existingBlocks.join(', ')}`));
|
|
1456
|
-
let inquirerModule = null;
|
|
1457
|
-
if (!options.yes) {
|
|
1458
|
-
inquirerModule = (await import('inquirer')).default;
|
|
1459
|
-
const { confirm } = await inquirerModule.prompt([
|
|
1460
|
-
{
|
|
1461
|
-
type: 'confirm',
|
|
1462
|
-
name: 'confirm',
|
|
1463
|
-
message: 'Update AGENTS.md and .rulebook with latest templates?',
|
|
1464
|
-
default: true,
|
|
1465
|
-
},
|
|
1466
|
-
]);
|
|
1467
|
-
if (!confirm) {
|
|
1468
|
-
console.log(chalk.yellow('\nUpdate cancelled'));
|
|
1469
|
-
process.exit(0);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
const hasPreCommit = detection.gitHooks?.preCommitExists ?? false;
|
|
1473
|
-
const hasPrePush = detection.gitHooks?.prePushExists ?? false;
|
|
1474
|
-
const missingHooks = !hasPreCommit || !hasPrePush;
|
|
1475
|
-
let installHooksOnUpdate = false;
|
|
1476
|
-
let hooksInstalledOnUpdate = false;
|
|
1477
|
-
if (missingHooks) {
|
|
1478
|
-
if (options.yes) {
|
|
1479
|
-
console.log(chalk.yellow('\n⚠ Git hooks are missing. Re-run "rulebook update" without --yes to install automated hooks or install them manually.'));
|
|
1480
|
-
}
|
|
1481
|
-
else {
|
|
1482
|
-
if (!inquirerModule) {
|
|
1483
|
-
inquirerModule = (await import('inquirer')).default;
|
|
1484
|
-
}
|
|
1485
|
-
const { installHooks } = await inquirerModule.prompt([
|
|
1486
|
-
{
|
|
1487
|
-
type: 'confirm',
|
|
1488
|
-
name: 'installHooks',
|
|
1489
|
-
message: `Install Git hooks for automated quality checks? Missing: ${hasPreCommit ? '' : 'pre-commit '}${hasPrePush ? '' : 'pre-push'}`.trim(),
|
|
1490
|
-
default: true,
|
|
1491
|
-
},
|
|
1492
|
-
]);
|
|
1493
|
-
installHooksOnUpdate = installHooks;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
if (missingHooks && !installHooksOnUpdate && !options.yes) {
|
|
1497
|
-
console.log(chalk.gray('\nℹ Git hooks were not installed during update. Re-run "rulebook update" later or install them manually if you change your mind.'));
|
|
1498
|
-
}
|
|
1499
|
-
const agentsPath = path.join(cwd, 'AGENTS.md');
|
|
1500
|
-
// Load existing config using ConfigManager
|
|
1501
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
1502
|
-
const configManager = createConfigManager(cwd);
|
|
1503
|
-
const existingConfig = await configManager.loadConfig();
|
|
1504
|
-
let existingMode;
|
|
1505
|
-
let existingLightMode;
|
|
1506
|
-
if (existingConfig) {
|
|
1507
|
-
if (existingConfig && (existingConfig.mode === 'minimal' || existingConfig.mode === 'full')) {
|
|
1508
|
-
existingMode = existingConfig.mode;
|
|
1509
|
-
}
|
|
1510
|
-
if (existingConfig && existingConfig.lightMode !== undefined) {
|
|
1511
|
-
existingLightMode = existingConfig.lightMode;
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
const minimalMode = options.minimal ?? existingMode === 'minimal';
|
|
1515
|
-
const lightMode = options.light !== undefined ? options.light : (existingLightMode ?? false);
|
|
1516
|
-
const leanMode = options.lean ?? existingConfig?.agentsMode === 'lean';
|
|
1517
|
-
// Build config from detected project
|
|
1518
|
-
const config = {
|
|
1519
|
-
languages: detection.languages.map((l) => l.language),
|
|
1520
|
-
modules: minimalMode ? [] : detection.modules.filter((m) => m.detected).map((m) => m.module),
|
|
1521
|
-
frameworks: detection.frameworks.filter((f) => f.detected).map((f) => f.framework),
|
|
1522
|
-
ides: [], // Preserve existing IDE choices
|
|
1523
|
-
projectType: 'application',
|
|
1524
|
-
coverageThreshold: 95,
|
|
1525
|
-
strictDocs: true,
|
|
1526
|
-
generateWorkflows: false, // Don't regenerate workflows on update
|
|
1527
|
-
includeGitWorkflow: true,
|
|
1528
|
-
gitPushMode: 'manual',
|
|
1529
|
-
installGitHooks: installHooksOnUpdate,
|
|
1530
|
-
minimal: minimalMode,
|
|
1531
|
-
lightMode: lightMode,
|
|
1532
|
-
...(leanMode ? { agentsMode: 'lean' } : {}),
|
|
1533
|
-
};
|
|
1534
|
-
if (minimalMode) {
|
|
1535
|
-
config.ides = [];
|
|
1536
|
-
config.generateWorkflows = true;
|
|
1537
|
-
}
|
|
1538
|
-
let minimalArtifacts = [];
|
|
1539
|
-
if (minimalMode) {
|
|
1540
|
-
minimalArtifacts = await scaffoldMinimalProject(cwd, {
|
|
1541
|
-
projectName: path.basename(cwd),
|
|
1542
|
-
description: 'Essential project scaffolding refreshed via Rulebook minimal mode.',
|
|
1543
|
-
license: 'MIT',
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
|
-
// Generate Rulebook commands if Cursor is detected
|
|
1547
|
-
// This ensures commands are available for all Cursor projects
|
|
1548
|
-
const cursorRulesPath = path.join(cwd, '.cursorrules');
|
|
1549
|
-
const cursorCommandsDir = path.join(cwd, '.cursor', 'commands');
|
|
1550
|
-
const usesCursor = existsSync(cursorRulesPath) || existsSync(cursorCommandsDir);
|
|
1551
|
-
// Deprecated notice: .cursorrules is superseded by .cursor/rules/*.mdc in Cursor v0.45+
|
|
1552
|
-
if (existsSync(cursorRulesPath)) {
|
|
1553
|
-
console.log(chalk.yellow(' ⚠ .cursorrules is deprecated as of Cursor v0.45. Use .cursor/rules/*.mdc instead.'));
|
|
1554
|
-
}
|
|
1555
|
-
if (usesCursor) {
|
|
1556
|
-
// Check if commands already exist to avoid duplicate generation
|
|
1557
|
-
const existingCommandsDir = path.join(cwd, '.cursor', 'commands');
|
|
1558
|
-
if (existsSync(existingCommandsDir)) {
|
|
1559
|
-
const { readdir } = await import('fs/promises');
|
|
1560
|
-
const existingFiles = await readdir(existingCommandsDir);
|
|
1561
|
-
const hasRulebookCommands = existingFiles.some((file) => file.startsWith('rulebook-task-'));
|
|
1562
|
-
if (!hasRulebookCommands) {
|
|
1563
|
-
const { generateCursorCommands } = await import('../core/workflow-generator.js');
|
|
1564
|
-
const generatedCommands = await generateCursorCommands(cwd);
|
|
1565
|
-
if (generatedCommands.length > 0) {
|
|
1566
|
-
console.log(chalk.green(` Generated ${generatedCommands.length} Rulebook command(s) in .cursor/commands/`));
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
else {
|
|
1571
|
-
// Directory doesn't exist, create it and generate commands
|
|
1572
|
-
const { generateCursorCommands } = await import('../core/workflow-generator.js');
|
|
1573
|
-
const generatedCommands = await generateCursorCommands(cwd);
|
|
1574
|
-
if (generatedCommands.length > 0) {
|
|
1575
|
-
console.log(chalk.green(` Generated ${generatedCommands.length} Rulebook command(s) in .cursor/commands/`));
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
// Migration already done via configManager.loadConfig() -> migrateConfig() -> migrateDirectoryStructure()
|
|
1580
|
-
// No need to call it again here
|
|
1581
|
-
// Load existing config to preserve skills and ralph settings (already loaded above)
|
|
1582
|
-
const existingSkills = existingConfig.skills?.enabled || [];
|
|
1583
|
-
const existingRalph = existingConfig.ralph;
|
|
1584
|
-
// Auto-detect skills based on project detection (v2.0)
|
|
1585
|
-
let detectedSkills = [];
|
|
1586
|
-
try {
|
|
1587
|
-
const { SkillsManager, getDefaultTemplatesPath } = await import('../core/skills-manager.js');
|
|
1588
|
-
const skillsManager = new SkillsManager(getDefaultTemplatesPath(), cwd);
|
|
1589
|
-
// Build a RulebookConfig-like object for skill detection
|
|
1590
|
-
const rulebookConfigForSkills = {
|
|
1591
|
-
languages: config.languages,
|
|
1592
|
-
frameworks: config.frameworks,
|
|
1593
|
-
modules: config.modules,
|
|
1594
|
-
services: config.services,
|
|
1595
|
-
};
|
|
1596
|
-
detectedSkills = await skillsManager.autoDetectSkills(rulebookConfigForSkills);
|
|
1597
|
-
// Merge with existing skills (keep existing, add new detected)
|
|
1598
|
-
const mergedSkills = [...new Set([...existingSkills, ...detectedSkills])];
|
|
1599
|
-
if (detectedSkills.length > existingSkills.length) {
|
|
1600
|
-
const newSkills = detectedSkills.filter((s) => !existingSkills.includes(s));
|
|
1601
|
-
if (newSkills.length > 0) {
|
|
1602
|
-
console.log(chalk.green('\n✓ New skills detected:'));
|
|
1603
|
-
for (const skillId of newSkills) {
|
|
1604
|
-
console.log(chalk.gray(` - ${skillId}`));
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
detectedSkills = mergedSkills;
|
|
1609
|
-
}
|
|
1610
|
-
catch {
|
|
1611
|
-
// Skills system not available or error - preserve existing skills
|
|
1612
|
-
detectedSkills = existingSkills;
|
|
1613
|
-
}
|
|
1614
|
-
await configManager.updateConfig({
|
|
1615
|
-
languages: config.languages,
|
|
1616
|
-
frameworks: config.frameworks,
|
|
1617
|
-
modules: config.modules,
|
|
1618
|
-
services: config.services,
|
|
1619
|
-
modular: config.modular ?? true,
|
|
1620
|
-
rulebookDir: config.rulebookDir || '.rulebook',
|
|
1621
|
-
skills: detectedSkills.length > 0 ? { enabled: detectedSkills } : undefined,
|
|
1622
|
-
ralph: existingRalph,
|
|
1623
|
-
memory: existingConfig.memory,
|
|
1624
|
-
});
|
|
1625
|
-
// Ensure .rulebook is in .gitignore with exceptions for specs/tasks
|
|
1626
|
-
await configManager.ensureGitignore();
|
|
1627
|
-
// Migrate flat layout to specs/ subdirectory if needed
|
|
1628
|
-
{
|
|
1629
|
-
const { hasFlatLayout, migrateFlatToSpecs } = await import('../core/migrator.js');
|
|
1630
|
-
const rulebookDirForMigration = config.rulebookDir || '.rulebook';
|
|
1631
|
-
if (await hasFlatLayout(cwd, rulebookDirForMigration)) {
|
|
1632
|
-
const migrationSpinner = ora('Migrating rulebook files to specs/ subdirectory...').start();
|
|
1633
|
-
const { migratedFiles } = await migrateFlatToSpecs(cwd, rulebookDirForMigration);
|
|
1634
|
-
if (migratedFiles.length > 0) {
|
|
1635
|
-
migrationSpinner.succeed(`Migrated ${migratedFiles.length} file(s) to /${rulebookDirForMigration}/specs/`);
|
|
1636
|
-
}
|
|
1637
|
-
else {
|
|
1638
|
-
migrationSpinner.info('No files to migrate');
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
// Migrate legacy archive directory from tasks/archive to .rulebook/archive
|
|
1643
|
-
{
|
|
1644
|
-
const { createTaskManager } = await import('../core/task-manager.js');
|
|
1645
|
-
const rulebookDirForArchive = config.rulebookDir || '.rulebook';
|
|
1646
|
-
const tm = createTaskManager(cwd, rulebookDirForArchive);
|
|
1647
|
-
const migrated = await tm.migrateArchive();
|
|
1648
|
-
if (migrated) {
|
|
1649
|
-
console.log(chalk.gray(' • Migrated task archive to .rulebook/archive/'));
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
// Merge with existing AGENTS.md (with migration support)
|
|
1653
|
-
const mergeSpinner = ora('Updating AGENTS.md with latest templates...').start();
|
|
1654
|
-
config.modular = config.modular ?? true; // Enable modular by default
|
|
1655
|
-
const mergedContent = await mergeFullAgents(detection.existingAgents, config, cwd);
|
|
1656
|
-
await writeFile(agentsPath, mergedContent);
|
|
1657
|
-
mergeSpinner.succeed('AGENTS.md updated');
|
|
1658
|
-
// Install + project canonical rules to all detected tools (v5 rule engine)
|
|
1659
|
-
// On update: if .rulebook/rules/ is empty (v4 project), auto-install based on complexity
|
|
1660
|
-
{
|
|
1661
|
-
const { projectRules, installRule, loadCanonicalRules } = await import('../core/rule-engine.js');
|
|
1662
|
-
const existingRules = await loadCanonicalRules(cwd);
|
|
1663
|
-
if (existingRules.length === 0) {
|
|
1664
|
-
// v4 project upgrading to v5 — install canonical rules based on complexity
|
|
1665
|
-
const { assessComplexity } = await import('../core/complexity-detector.js');
|
|
1666
|
-
const { getTemplatesDir } = await import('../core/generator.js');
|
|
1667
|
-
const complexity = assessComplexity(cwd);
|
|
1668
|
-
const templatesDir = getTemplatesDir();
|
|
1669
|
-
const tier1 = [
|
|
1670
|
-
'no-shortcuts',
|
|
1671
|
-
'git-safety',
|
|
1672
|
-
'sequential-editing',
|
|
1673
|
-
'research-first',
|
|
1674
|
-
'follow-task-sequence',
|
|
1675
|
-
'incremental-implementation',
|
|
1676
|
-
'knowledge-base-usage',
|
|
1677
|
-
];
|
|
1678
|
-
const tier2 = ['task-decomposition', 'incremental-tests', 'no-deferred', 'session-workflow'];
|
|
1679
|
-
const toInstall = [...tier1];
|
|
1680
|
-
if (complexity.recommendations.tier2Rules) {
|
|
1681
|
-
toInstall.push(...tier2);
|
|
1682
|
-
}
|
|
1683
|
-
let installed = 0;
|
|
1684
|
-
for (const name of toInstall) {
|
|
1685
|
-
const result = await installRule(cwd, name, templatesDir);
|
|
1686
|
-
if (result)
|
|
1687
|
-
installed++;
|
|
1688
|
-
}
|
|
1689
|
-
if (installed > 0) {
|
|
1690
|
-
console.log(chalk.gray(` • Installed ${installed} v5 canonical rules (${complexity.tier} project, ${complexity.metrics.estimatedLoc.toLocaleString()} LOC)`));
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
const ruleResult = await projectRules(cwd, {
|
|
1694
|
-
claudeCode: existsSync(path.join(cwd, '.claude')) || existsSync(path.join(cwd, 'CLAUDE.md')),
|
|
1695
|
-
cursor: detection.cursor?.detected,
|
|
1696
|
-
gemini: detection.geminiCli?.detected,
|
|
1697
|
-
windsurf: detection.windsurf?.detected,
|
|
1698
|
-
copilot: detection.githubCopilot?.detected,
|
|
1699
|
-
continueDev: detection.continueDev?.detected,
|
|
1700
|
-
});
|
|
1701
|
-
const totalProjected = ruleResult.claudeCode.length +
|
|
1702
|
-
ruleResult.cursor.length +
|
|
1703
|
-
ruleResult.gemini.length +
|
|
1704
|
-
ruleResult.copilot.length +
|
|
1705
|
-
ruleResult.windsurf.length +
|
|
1706
|
-
ruleResult.continueDev.length;
|
|
1707
|
-
if (totalProjected > 0) {
|
|
1708
|
-
console.log(chalk.gray(` • Projected ${totalProjected} canonical rules to detected tools`));
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
// Show multi-tool config feedback (update command)
|
|
1712
|
-
if (detection.geminiCli?.detected) {
|
|
1713
|
-
console.log(chalk.gray(' • Gemini CLI config updated: GEMINI.md'));
|
|
1714
|
-
}
|
|
1715
|
-
if (detection.continueDev?.detected) {
|
|
1716
|
-
console.log(chalk.gray(' • Continue.dev rules updated in .continue/rules/'));
|
|
1717
|
-
}
|
|
1718
|
-
if (detection.windsurf?.detected) {
|
|
1719
|
-
console.log(chalk.gray(' • Windsurf rules updated: .windsurfrules'));
|
|
1720
|
-
}
|
|
1721
|
-
if (detection.githubCopilot?.detected) {
|
|
1722
|
-
console.log(chalk.gray(' • GitHub Copilot instructions updated in .github/'));
|
|
1723
|
-
}
|
|
1724
|
-
if (installHooksOnUpdate) {
|
|
1725
|
-
const hookLanguages = detection.languages.length > 0
|
|
1726
|
-
? detection.languages
|
|
1727
|
-
: config.languages.map((language) => ({
|
|
1728
|
-
language: language,
|
|
1729
|
-
confidence: 1,
|
|
1730
|
-
indicators: [],
|
|
1731
|
-
}));
|
|
1732
|
-
const hookSpinner = ora('Installing Git hooks (pre-commit & pre-push)...').start();
|
|
1733
|
-
try {
|
|
1734
|
-
await installGitHooks({ languages: hookLanguages, cwd });
|
|
1735
|
-
hookSpinner.succeed('Git hooks installed successfully');
|
|
1736
|
-
hooksInstalledOnUpdate = true;
|
|
1737
|
-
}
|
|
1738
|
-
catch (error) {
|
|
1739
|
-
hookSpinner.fail('Failed to install Git hooks');
|
|
1740
|
-
console.error(chalk.red(' ➤'), error instanceof Error ? error.message : error);
|
|
1741
|
-
console.log(chalk.yellow(' ⚠ Skipping automatic hook installation. You can rerun "rulebook update" later to retry or install manually.'));
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
const gitHooksActiveAfterUpdate = hooksInstalledOnUpdate || (hasPreCommit && hasPrePush);
|
|
1745
|
-
config.installGitHooks = gitHooksActiveAfterUpdate;
|
|
1746
|
-
// Update .rulebook config
|
|
1747
|
-
const configSpinner = ora('Updating .rulebook configuration...').start();
|
|
1748
|
-
const rulebookFeatures = {
|
|
1749
|
-
watcher: false,
|
|
1750
|
-
agent: false,
|
|
1751
|
-
logging: true,
|
|
1752
|
-
telemetry: false,
|
|
1753
|
-
notifications: false,
|
|
1754
|
-
dryRun: false,
|
|
1755
|
-
gitHooks: gitHooksActiveAfterUpdate,
|
|
1756
|
-
repl: false,
|
|
1757
|
-
templates: true,
|
|
1758
|
-
context: minimalMode ? false : true,
|
|
1759
|
-
health: true,
|
|
1760
|
-
plugins: false,
|
|
1761
|
-
parallel: minimalMode ? false : true,
|
|
1762
|
-
smartContinue: minimalMode ? false : true,
|
|
1763
|
-
};
|
|
1764
|
-
const rulebookConfig = {
|
|
1765
|
-
version: getRulebookVersion(),
|
|
1766
|
-
installedAt: detection.existingAgents.content?.match(/Generated at: (.+)/)?.[1] ||
|
|
1767
|
-
new Date().toISOString(),
|
|
1768
|
-
updatedAt: new Date().toISOString(),
|
|
1769
|
-
projectId: path.basename(cwd),
|
|
1770
|
-
mode: minimalMode ? 'minimal' : 'full',
|
|
1771
|
-
features: rulebookFeatures,
|
|
1772
|
-
coverageThreshold: existingConfig.coverageThreshold ?? 95,
|
|
1773
|
-
language: existingConfig.language ?? 'en',
|
|
1774
|
-
outputLanguage: existingConfig.outputLanguage ?? 'en',
|
|
1775
|
-
cliTools: existingConfig.cliTools ?? [],
|
|
1776
|
-
maxParallelTasks: existingConfig.maxParallelTasks ?? 5,
|
|
1777
|
-
timeouts: existingConfig.timeouts ?? {
|
|
1778
|
-
taskExecution: 3600000,
|
|
1779
|
-
cliResponse: 180000,
|
|
1780
|
-
testRun: 600000,
|
|
1781
|
-
},
|
|
1782
|
-
...(existingConfig.memory ? { memory: existingConfig.memory } : {}),
|
|
1783
|
-
...(existingConfig.ralph ? { ralph: existingConfig.ralph } : {}),
|
|
1784
|
-
...(existingConfig.skills ? { skills: existingConfig.skills } : {}),
|
|
1785
|
-
...(leanMode
|
|
1786
|
-
? { agentsMode: 'lean' }
|
|
1787
|
-
: existingConfig.agentsMode
|
|
1788
|
-
? { agentsMode: existingConfig.agentsMode }
|
|
1789
|
-
: {}),
|
|
1790
|
-
};
|
|
1791
|
-
await configManager.saveConfig(rulebookConfig);
|
|
1792
|
-
configSpinner.succeed('.rulebook configuration updated');
|
|
1793
|
-
// Auto-setup Claude Code integration (MCP + skills)
|
|
1794
|
-
const claudeSpinner = ora('Checking Claude Code integration...').start();
|
|
1795
|
-
try {
|
|
1796
|
-
const { setupClaudeCodeIntegration } = await import('../core/claude-mcp.js');
|
|
1797
|
-
const result = await setupClaudeCodeIntegration(cwd);
|
|
1798
|
-
if (result.detected) {
|
|
1799
|
-
claudeSpinner.succeed('Claude Code integration updated');
|
|
1800
|
-
if (result.mcpConfigured) {
|
|
1801
|
-
console.log(chalk.gray(' • MCP server added to .mcp.json'));
|
|
1802
|
-
}
|
|
1803
|
-
if (result.skillsInstalled.length > 0) {
|
|
1804
|
-
console.log(chalk.gray(` • ${result.skillsInstalled.length} skills updated in .claude/commands/`));
|
|
1805
|
-
}
|
|
1806
|
-
if (result.agentTeamsEnabled) {
|
|
1807
|
-
console.log(chalk.gray(' • Multi-agent teams enabled in .claude/settings.json'));
|
|
1808
|
-
}
|
|
1809
|
-
if (result.agentDefinitionsInstalled.length > 0) {
|
|
1810
|
-
console.log(chalk.gray(` • ${result.agentDefinitionsInstalled.length} agent definitions updated in .claude/agents/`));
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
else {
|
|
1814
|
-
claudeSpinner.info('Claude Code not detected (skipped)');
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
catch {
|
|
1818
|
-
claudeSpinner.info('Claude Code integration skipped');
|
|
1819
|
-
}
|
|
1820
|
-
// Install/update Ralph shell scripts
|
|
1821
|
-
try {
|
|
1822
|
-
const { installRalphScripts } = await import('../core/ralph-scripts.js');
|
|
1823
|
-
const scripts = await installRalphScripts(cwd);
|
|
1824
|
-
if (scripts.length > 0) {
|
|
1825
|
-
console.log(chalk.gray(` • ${scripts.length} Ralph scripts updated in .rulebook/scripts/`));
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
catch {
|
|
1829
|
-
// Skip if Ralph scripts installation fails
|
|
1830
|
-
}
|
|
1831
|
-
// Ensure PLANS.md exists (create if missing, never overwrite)
|
|
1832
|
-
try {
|
|
1833
|
-
const { initPlans } = await import('../core/plans-manager.js');
|
|
1834
|
-
await initPlans(cwd);
|
|
1835
|
-
}
|
|
1836
|
-
catch {
|
|
1837
|
-
// Non-blocking
|
|
1838
|
-
}
|
|
1839
|
-
// Migrate memory directory if old structure exists
|
|
1840
|
-
try {
|
|
1841
|
-
await migrateMemoryDirectory();
|
|
1842
|
-
}
|
|
1843
|
-
catch {
|
|
1844
|
-
// Silently skip if migration fails
|
|
1845
|
-
}
|
|
1846
|
-
// Install plugin in Claude Code
|
|
1847
|
-
try {
|
|
1848
|
-
await setupClaudeCodePlugin();
|
|
1849
|
-
}
|
|
1850
|
-
catch {
|
|
1851
|
-
// Silently skip if plugin installation fails
|
|
1852
|
-
}
|
|
1853
|
-
// Clean up any accidental duplicate directories
|
|
1854
|
-
try {
|
|
1855
|
-
const fsPromises = await import('fs/promises');
|
|
1856
|
-
const accidentalDir = path.join(cwd, '.rulebook', '.rulebook');
|
|
1857
|
-
if (existsSync(accidentalDir)) {
|
|
1858
|
-
await fsPromises.rm(accidentalDir, { recursive: true, force: true });
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
catch {
|
|
1862
|
-
// Ignore cleanup errors
|
|
1863
|
-
}
|
|
1864
|
-
// Success message
|
|
1865
|
-
console.log(chalk.bold.green('\n✅ Update complete!\n'));
|
|
1866
|
-
console.log(chalk.white('Updated components:'));
|
|
1867
|
-
console.log(chalk.green(' ✓ AGENTS.md - Merged with latest templates'));
|
|
1868
|
-
console.log(chalk.green(` ✓ .rulebook - Updated to v${getRulebookVersion()}`));
|
|
1869
|
-
console.log(chalk.white('\nWhat was updated:'));
|
|
1870
|
-
console.log(chalk.gray(` - ${detection.languages.length} language templates`));
|
|
1871
|
-
console.log(chalk.gray(` - ${detection.modules.filter((m) => m.detected).length} MCP modules`));
|
|
1872
|
-
console.log(chalk.gray(' - Git workflow rules'));
|
|
1873
|
-
console.log(chalk.gray(' - Rulebook task management'));
|
|
1874
|
-
console.log(chalk.gray(' - Pre-commit command standardization'));
|
|
1875
|
-
console.log(chalk.yellow('\n⚠ Review the updated AGENTS.md to ensure your custom rules are preserved'));
|
|
1876
|
-
console.log(chalk.white('\nNext steps:'));
|
|
1877
|
-
console.log(chalk.gray(' 1. Review AGENTS.md changes'));
|
|
1878
|
-
console.log(chalk.gray(' 2. Test that your project still builds'));
|
|
1879
|
-
console.log(chalk.gray(' 3. Run quality checks (lint, test, build)'));
|
|
1880
|
-
console.log(chalk.gray(' 4. Commit the updated files\n'));
|
|
1881
|
-
if (minimalMode && minimalArtifacts.length > 0) {
|
|
1882
|
-
console.log(chalk.green('Essentials ensured:'));
|
|
1883
|
-
for (const artifact of minimalArtifacts) {
|
|
1884
|
-
console.log(chalk.gray(` - ${path.relative(cwd, artifact)}`));
|
|
1885
|
-
}
|
|
1886
|
-
console.log('');
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
// ============================================
|
|
1890
|
-
// Skills Commands (v2.0)
|
|
1891
|
-
// ============================================
|
|
1892
|
-
/**
|
|
1893
|
-
* List all available skills
|
|
1894
|
-
*/
|
|
1895
|
-
export async function skillListCommand(options) {
|
|
1896
|
-
try {
|
|
1897
|
-
const cwd = process.cwd();
|
|
1898
|
-
const spinner = ora('Discovering skills...').start();
|
|
1899
|
-
const skillsManager = new SkillsManager(getDefaultTemplatesPath(), cwd);
|
|
1900
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
1901
|
-
const configManager = createConfigManager(cwd);
|
|
1902
|
-
let skills;
|
|
1903
|
-
if (options.category) {
|
|
1904
|
-
skills = await skillsManager.getSkillsByCategory(options.category);
|
|
1905
|
-
}
|
|
1906
|
-
else {
|
|
1907
|
-
skills = await skillsManager.getSkills();
|
|
1908
|
-
}
|
|
1909
|
-
// Get enabled status from config
|
|
1910
|
-
let enabledIds = new Set();
|
|
1911
|
-
try {
|
|
1912
|
-
const config = await configManager.loadConfig();
|
|
1913
|
-
enabledIds = new Set(config.skills?.enabled || []);
|
|
1914
|
-
}
|
|
1915
|
-
catch {
|
|
1916
|
-
// No config file, all skills disabled
|
|
1917
|
-
}
|
|
1918
|
-
// Filter by enabled status if requested
|
|
1919
|
-
if (options.enabled) {
|
|
1920
|
-
skills = skills.filter((s) => enabledIds.has(s.id));
|
|
1921
|
-
}
|
|
1922
|
-
spinner.succeed(`Found ${skills.length} skill(s)`);
|
|
1923
|
-
if (skills.length === 0) {
|
|
1924
|
-
console.log(chalk.yellow('\nNo skills found matching criteria.'));
|
|
1925
|
-
return;
|
|
1926
|
-
}
|
|
1927
|
-
// Group by category
|
|
1928
|
-
const byCategory = new Map();
|
|
1929
|
-
for (const skill of skills) {
|
|
1930
|
-
const cat = skill.category;
|
|
1931
|
-
if (!byCategory.has(cat)) {
|
|
1932
|
-
byCategory.set(cat, []);
|
|
1933
|
-
}
|
|
1934
|
-
byCategory.get(cat).push(skill);
|
|
1935
|
-
}
|
|
1936
|
-
console.log(chalk.bold.blue('\n📦 Available Skills\n'));
|
|
1937
|
-
for (const [category, categorySkills] of byCategory) {
|
|
1938
|
-
console.log(chalk.bold.white(`${category.toUpperCase()}`));
|
|
1939
|
-
for (const skill of categorySkills) {
|
|
1940
|
-
const enabled = enabledIds.has(skill.id);
|
|
1941
|
-
const status = enabled ? chalk.green('✓') : chalk.gray('○');
|
|
1942
|
-
const name = enabled ? chalk.green(skill.metadata.name) : chalk.white(skill.metadata.name);
|
|
1943
|
-
console.log(` ${status} ${name}`);
|
|
1944
|
-
console.log(chalk.gray(` ${skill.metadata.description}`));
|
|
1945
|
-
console.log(chalk.gray(` ID: ${skill.id}`));
|
|
1946
|
-
}
|
|
1947
|
-
console.log('');
|
|
1948
|
-
}
|
|
1949
|
-
console.log(chalk.gray('Use "rulebook skill add <skill-id>" to enable a skill'));
|
|
1950
|
-
console.log(chalk.gray('Use "rulebook skill remove <skill-id>" to disable a skill'));
|
|
1951
|
-
}
|
|
1952
|
-
catch (error) {
|
|
1953
|
-
console.error(chalk.red('\n❌ Failed to list skills:'), error);
|
|
1954
|
-
process.exit(1);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
/**
|
|
1958
|
-
* Add (enable) a skill
|
|
1959
|
-
*/
|
|
1960
|
-
export async function skillAddCommand(skillId) {
|
|
1961
|
-
try {
|
|
1962
|
-
const cwd = process.cwd();
|
|
1963
|
-
const spinner = ora(`Adding skill: ${skillId}...`).start();
|
|
1964
|
-
const skillsManager = new SkillsManager(getDefaultTemplatesPath(), cwd);
|
|
1965
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
1966
|
-
const configManager = createConfigManager(cwd);
|
|
1967
|
-
// Check if skill exists
|
|
1968
|
-
const skill = await skillsManager.getSkillById(skillId);
|
|
1969
|
-
if (!skill) {
|
|
1970
|
-
spinner.fail(`Skill not found: ${skillId}`);
|
|
1971
|
-
// Search for similar skills
|
|
1972
|
-
const allSkills = await skillsManager.getSkills();
|
|
1973
|
-
const similar = allSkills.filter((s) => s.id.includes(skillId.toLowerCase()) ||
|
|
1974
|
-
s.metadata.name.toLowerCase().includes(skillId.toLowerCase()));
|
|
1975
|
-
if (similar.length > 0) {
|
|
1976
|
-
console.log(chalk.yellow('\nDid you mean one of these?'));
|
|
1977
|
-
for (const s of similar.slice(0, 5)) {
|
|
1978
|
-
console.log(chalk.gray(` - ${s.id} (${s.metadata.name})`));
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
console.log(chalk.gray('\nUse "rulebook skill list" to see all available skills'));
|
|
1982
|
-
process.exit(1);
|
|
1983
|
-
}
|
|
1984
|
-
// Load config and enable skill
|
|
1985
|
-
let config = await configManager.loadConfig();
|
|
1986
|
-
config = await skillsManager.enableSkill(skillId, config);
|
|
1987
|
-
// Validate for conflicts
|
|
1988
|
-
const validation = await skillsManager.validateSkills(config);
|
|
1989
|
-
if (validation.conflicts.length > 0) {
|
|
1990
|
-
spinner.warn(`Skill enabled with conflicts`);
|
|
1991
|
-
console.log(chalk.yellow('\n⚠️ Conflicts detected:'));
|
|
1992
|
-
for (const conflict of validation.conflicts) {
|
|
1993
|
-
console.log(chalk.yellow(` - ${conflict.skillA} conflicts with ${conflict.skillB}`));
|
|
1994
|
-
console.log(chalk.gray(` ${conflict.reason}`));
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
else {
|
|
1998
|
-
spinner.succeed(`Skill added: ${skill.metadata.name}`);
|
|
1999
|
-
}
|
|
2000
|
-
// Save config
|
|
2001
|
-
await configManager.saveConfig(config);
|
|
2002
|
-
console.log(chalk.green(`\n✓ Skill "${skill.metadata.name}" is now enabled`));
|
|
2003
|
-
console.log(chalk.gray(` Category: ${skill.category}`));
|
|
2004
|
-
console.log(chalk.gray(` Description: ${skill.metadata.description}`));
|
|
2005
|
-
if (validation.warnings.length > 0) {
|
|
2006
|
-
console.log(chalk.yellow('\n⚠️ Warnings:'));
|
|
2007
|
-
for (const warning of validation.warnings) {
|
|
2008
|
-
console.log(chalk.yellow(` - ${warning}`));
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
console.log(chalk.gray('\nRun "rulebook update" to regenerate AGENTS.md with the new skill'));
|
|
2012
|
-
}
|
|
2013
|
-
catch (error) {
|
|
2014
|
-
console.error(chalk.red('\n❌ Failed to add skill:'), error);
|
|
2015
|
-
process.exit(1);
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
/**
|
|
2019
|
-
* Remove (disable) a skill
|
|
2020
|
-
*/
|
|
2021
|
-
export async function skillRemoveCommand(skillId) {
|
|
2022
|
-
try {
|
|
2023
|
-
const cwd = process.cwd();
|
|
2024
|
-
const spinner = ora(`Removing skill: ${skillId}...`).start();
|
|
2025
|
-
const skillsManager = new SkillsManager(getDefaultTemplatesPath(), cwd);
|
|
2026
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2027
|
-
const configManager = createConfigManager(cwd);
|
|
2028
|
-
// Check if skill exists
|
|
2029
|
-
const skill = await skillsManager.getSkillById(skillId);
|
|
2030
|
-
if (!skill) {
|
|
2031
|
-
spinner.fail(`Skill not found: ${skillId}`);
|
|
2032
|
-
console.log(chalk.gray('Use "rulebook skill list" to see all available skills'));
|
|
2033
|
-
process.exit(1);
|
|
2034
|
-
}
|
|
2035
|
-
// Load config
|
|
2036
|
-
let config = await configManager.loadConfig();
|
|
2037
|
-
// Check if skill is enabled
|
|
2038
|
-
if (!config.skills?.enabled?.includes(skillId)) {
|
|
2039
|
-
spinner.fail(`Skill "${skillId}" is not currently enabled`);
|
|
2040
|
-
process.exit(1);
|
|
2041
|
-
}
|
|
2042
|
-
// Disable skill
|
|
2043
|
-
config = await skillsManager.disableSkill(skillId, config);
|
|
2044
|
-
await configManager.saveConfig(config);
|
|
2045
|
-
spinner.succeed(`Skill removed: ${skill.metadata.name}`);
|
|
2046
|
-
console.log(chalk.green(`\n✓ Skill "${skill.metadata.name}" is now disabled`));
|
|
2047
|
-
console.log(chalk.gray('\nRun "rulebook update" to regenerate AGENTS.md without this skill'));
|
|
2048
|
-
}
|
|
2049
|
-
catch (error) {
|
|
2050
|
-
console.error(chalk.red('\n❌ Failed to remove skill:'), error);
|
|
2051
|
-
process.exit(1);
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
/**
|
|
2055
|
-
* Show skill details
|
|
2056
|
-
*/
|
|
2057
|
-
export async function skillShowCommand(skillId) {
|
|
2058
|
-
try {
|
|
2059
|
-
const cwd = process.cwd();
|
|
2060
|
-
const spinner = ora(`Loading skill: ${skillId}...`).start();
|
|
2061
|
-
const skillsManager = new SkillsManager(getDefaultTemplatesPath(), cwd);
|
|
2062
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2063
|
-
const configManager = createConfigManager(cwd);
|
|
2064
|
-
const skill = await skillsManager.getSkillById(skillId);
|
|
2065
|
-
if (!skill) {
|
|
2066
|
-
spinner.fail(`Skill not found: ${skillId}`);
|
|
2067
|
-
// Search for similar skills
|
|
2068
|
-
const allSkills = await skillsManager.getSkills();
|
|
2069
|
-
const similar = allSkills.filter((s) => s.id.includes(skillId.toLowerCase()) ||
|
|
2070
|
-
s.metadata.name.toLowerCase().includes(skillId.toLowerCase()));
|
|
2071
|
-
if (similar.length > 0) {
|
|
2072
|
-
console.log(chalk.yellow('\nDid you mean one of these?'));
|
|
2073
|
-
for (const s of similar.slice(0, 5)) {
|
|
2074
|
-
console.log(chalk.gray(` - ${s.id} (${s.metadata.name})`));
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
process.exit(1);
|
|
2078
|
-
}
|
|
2079
|
-
spinner.stop();
|
|
2080
|
-
// Check if enabled
|
|
2081
|
-
let enabled = false;
|
|
2082
|
-
try {
|
|
2083
|
-
const config = await configManager.loadConfig();
|
|
2084
|
-
enabled = config.skills?.enabled?.includes(skillId) || false;
|
|
2085
|
-
}
|
|
2086
|
-
catch {
|
|
2087
|
-
// No config
|
|
2088
|
-
}
|
|
2089
|
-
console.log(chalk.bold.blue(`\n📦 ${skill.metadata.name}\n`));
|
|
2090
|
-
console.log(chalk.white(`ID: ${skill.id}`));
|
|
2091
|
-
console.log(chalk.white(`Category: ${skill.category}`));
|
|
2092
|
-
console.log(chalk.white(`Status: ${enabled ? chalk.green('Enabled') : chalk.gray('Disabled')}`));
|
|
2093
|
-
if (skill.metadata.version) {
|
|
2094
|
-
console.log(chalk.white(`Version: ${skill.metadata.version}`));
|
|
2095
|
-
}
|
|
2096
|
-
if (skill.metadata.author) {
|
|
2097
|
-
console.log(chalk.white(`Author: ${skill.metadata.author}`));
|
|
2098
|
-
}
|
|
2099
|
-
console.log(chalk.white(`\nDescription:`));
|
|
2100
|
-
console.log(chalk.gray(` ${skill.metadata.description}`));
|
|
2101
|
-
if (skill.metadata.tags && skill.metadata.tags.length > 0) {
|
|
2102
|
-
console.log(chalk.white(`\nTags: ${skill.metadata.tags.join(', ')}`));
|
|
2103
|
-
}
|
|
2104
|
-
if (skill.metadata.dependencies && skill.metadata.dependencies.length > 0) {
|
|
2105
|
-
console.log(chalk.white(`\nDependencies:`));
|
|
2106
|
-
for (const dep of skill.metadata.dependencies) {
|
|
2107
|
-
console.log(chalk.gray(` - ${dep}`));
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
if (skill.metadata.conflicts && skill.metadata.conflicts.length > 0) {
|
|
2111
|
-
console.log(chalk.yellow(`\nConflicts with:`));
|
|
2112
|
-
for (const conflict of skill.metadata.conflicts) {
|
|
2113
|
-
console.log(chalk.yellow(` - ${conflict}`));
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
// Show preview of content
|
|
2117
|
-
console.log(chalk.white(`\nContent Preview:`));
|
|
2118
|
-
const preview = skill.content.slice(0, 500);
|
|
2119
|
-
console.log(chalk.gray(preview + (skill.content.length > 500 ? '...' : '')));
|
|
2120
|
-
console.log(chalk.gray(`\nPath: ${skill.path}`));
|
|
2121
|
-
}
|
|
2122
|
-
catch (error) {
|
|
2123
|
-
console.error(chalk.red('\n❌ Failed to show skill:'), error);
|
|
2124
|
-
process.exit(1);
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
/**
|
|
2128
|
-
* Search for skills
|
|
2129
|
-
*/
|
|
2130
|
-
export async function skillSearchCommand(query) {
|
|
2131
|
-
try {
|
|
2132
|
-
const cwd = process.cwd();
|
|
2133
|
-
const spinner = ora(`Searching for: ${query}...`).start();
|
|
2134
|
-
const skillsManager = new SkillsManager(getDefaultTemplatesPath(), cwd);
|
|
2135
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2136
|
-
const configManager = createConfigManager(cwd);
|
|
2137
|
-
const skills = await skillsManager.searchSkills(query);
|
|
2138
|
-
spinner.succeed(`Found ${skills.length} result(s)`);
|
|
2139
|
-
if (skills.length === 0) {
|
|
2140
|
-
console.log(chalk.yellow(`\nNo skills found matching "${query}"`));
|
|
2141
|
-
console.log(chalk.gray('Try a different search term or use "rulebook skill list"'));
|
|
2142
|
-
return;
|
|
2143
|
-
}
|
|
2144
|
-
// Get enabled status
|
|
2145
|
-
let enabledIds = new Set();
|
|
2146
|
-
try {
|
|
2147
|
-
const config = await configManager.loadConfig();
|
|
2148
|
-
enabledIds = new Set(config.skills?.enabled || []);
|
|
2149
|
-
}
|
|
2150
|
-
catch {
|
|
2151
|
-
// No config
|
|
2152
|
-
}
|
|
2153
|
-
console.log(chalk.bold.blue(`\n🔍 Search Results for "${query}"\n`));
|
|
2154
|
-
for (const skill of skills) {
|
|
2155
|
-
const enabled = enabledIds.has(skill.id);
|
|
2156
|
-
const status = enabled ? chalk.green('✓') : chalk.gray('○');
|
|
2157
|
-
const name = enabled ? chalk.green(skill.metadata.name) : chalk.white(skill.metadata.name);
|
|
2158
|
-
console.log(`${status} ${name} (${skill.category})`);
|
|
2159
|
-
console.log(chalk.gray(` ${skill.metadata.description}`));
|
|
2160
|
-
console.log(chalk.gray(` ID: ${skill.id}\n`));
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
catch (error) {
|
|
2164
|
-
console.error(chalk.red('\n❌ Search failed:'), error);
|
|
2165
|
-
process.exit(1);
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
// ============================================
|
|
2169
|
-
// Memory Commands (v2.2)
|
|
2170
|
-
// ============================================
|
|
2171
|
-
export async function memorySearchCommand(query, options) {
|
|
2172
|
-
const ora = (await import('ora')).default;
|
|
2173
|
-
const chalk = (await import('chalk')).default;
|
|
2174
|
-
const spinner = ora('Searching memories...').start();
|
|
2175
|
-
try {
|
|
2176
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2177
|
-
const cwd = process.cwd();
|
|
2178
|
-
const configManager = createConfigManager(cwd);
|
|
2179
|
-
const config = await configManager.loadConfig();
|
|
2180
|
-
if (!config.memory?.enabled) {
|
|
2181
|
-
spinner.fail('Memory system is not enabled. Run: rulebook config --set memory.enabled=true');
|
|
2182
|
-
return;
|
|
2183
|
-
}
|
|
2184
|
-
const { createMemoryManager } = await import('../memory/memory-manager.js');
|
|
2185
|
-
const manager = createMemoryManager(cwd, config.memory);
|
|
2186
|
-
const results = await manager.searchMemories({
|
|
2187
|
-
query,
|
|
2188
|
-
mode: options.mode || 'hybrid',
|
|
2189
|
-
type: options.type,
|
|
2190
|
-
limit: options.limit ? parseInt(options.limit) : 20,
|
|
2191
|
-
});
|
|
2192
|
-
spinner.succeed(`Found ${results.length} memories`);
|
|
2193
|
-
if (results.length === 0) {
|
|
2194
|
-
console.log(chalk.yellow('\nNo memories found for that query.'));
|
|
2195
|
-
}
|
|
2196
|
-
else {
|
|
2197
|
-
console.log('');
|
|
2198
|
-
for (const r of results) {
|
|
2199
|
-
const typeColor = r.type === 'bugfix' ? chalk.red : r.type === 'feature' ? chalk.green : chalk.blue;
|
|
2200
|
-
console.log(` ${typeColor(r.type.padEnd(12))} ${chalk.white(r.title)} ${chalk.gray(`[${r.matchType}] ${r.score.toFixed(3)}`)}`);
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
await manager.close();
|
|
2204
|
-
}
|
|
2205
|
-
catch (error) {
|
|
2206
|
-
spinner.fail('Search failed');
|
|
2207
|
-
console.error(chalk.red(String(error)));
|
|
2208
|
-
process.exit(1);
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
export async function memorySaveCommand(text, options) {
|
|
2212
|
-
const ora = (await import('ora')).default;
|
|
2213
|
-
const chalk = (await import('chalk')).default;
|
|
2214
|
-
const spinner = ora('Saving memory...').start();
|
|
2215
|
-
try {
|
|
2216
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2217
|
-
const cwd = process.cwd();
|
|
2218
|
-
const configManager = createConfigManager(cwd);
|
|
2219
|
-
const config = await configManager.loadConfig();
|
|
2220
|
-
if (!config.memory?.enabled) {
|
|
2221
|
-
spinner.fail('Memory system is not enabled. Run: rulebook config --set memory.enabled=true');
|
|
2222
|
-
return;
|
|
2223
|
-
}
|
|
2224
|
-
const { createMemoryManager } = await import('../memory/memory-manager.js');
|
|
2225
|
-
const { classifyMemory } = await import('../memory/memory-hooks.js');
|
|
2226
|
-
const manager = createMemoryManager(cwd, config.memory);
|
|
2227
|
-
const type = (options.type || classifyMemory(text));
|
|
2228
|
-
const title = options.title || text.slice(0, 80);
|
|
2229
|
-
const tags = options.tags ? options.tags.split(',').map((t) => t.trim()) : [];
|
|
2230
|
-
const memory = await manager.saveMemory({ type, title, content: text, tags });
|
|
2231
|
-
spinner.succeed(`Memory saved: ${chalk.cyan(memory.id)}`);
|
|
2232
|
-
console.log(chalk.gray(` Type: ${memory.type} | Title: ${memory.title}`));
|
|
2233
|
-
await manager.close();
|
|
2234
|
-
}
|
|
2235
|
-
catch (error) {
|
|
2236
|
-
spinner.fail('Save failed');
|
|
2237
|
-
console.error(chalk.red(String(error)));
|
|
2238
|
-
process.exit(1);
|
|
2239
|
-
}
|
|
2240
|
-
}
|
|
2241
|
-
export async function memoryListCommand(options) {
|
|
2242
|
-
const ora = (await import('ora')).default;
|
|
2243
|
-
const chalk = (await import('chalk')).default;
|
|
2244
|
-
const spinner = ora('Loading memories...').start();
|
|
2245
|
-
try {
|
|
2246
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2247
|
-
const cwd = process.cwd();
|
|
2248
|
-
const configManager = createConfigManager(cwd);
|
|
2249
|
-
const config = await configManager.loadConfig();
|
|
2250
|
-
if (!config.memory?.enabled) {
|
|
2251
|
-
spinner.fail('Memory system is not enabled.');
|
|
2252
|
-
return;
|
|
2253
|
-
}
|
|
2254
|
-
const { createMemoryManager } = await import('../memory/memory-manager.js');
|
|
2255
|
-
const manager = createMemoryManager(cwd, config.memory);
|
|
2256
|
-
const stats = await manager.getStats();
|
|
2257
|
-
const exported = await manager.exportMemories('json');
|
|
2258
|
-
const memories = JSON.parse(exported).slice(0, options.limit ? parseInt(options.limit) : 20);
|
|
2259
|
-
spinner.succeed(`${stats.memoryCount} memories total`);
|
|
2260
|
-
if (memories.length === 0) {
|
|
2261
|
-
console.log(chalk.yellow('\nNo memories stored yet.'));
|
|
2262
|
-
}
|
|
2263
|
-
else {
|
|
2264
|
-
console.log('');
|
|
2265
|
-
for (const m of memories) {
|
|
2266
|
-
const date = new Date(m.createdAt).toLocaleDateString();
|
|
2267
|
-
const typeColor = m.type === 'bugfix' ? chalk.red : m.type === 'feature' ? chalk.green : chalk.blue;
|
|
2268
|
-
console.log(` ${chalk.gray(date)} ${typeColor(m.type.padEnd(12))} ${chalk.white(m.title)}`);
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
await manager.close();
|
|
2272
|
-
}
|
|
2273
|
-
catch (error) {
|
|
2274
|
-
spinner.fail('Failed to list memories');
|
|
2275
|
-
console.error(chalk.red(String(error)));
|
|
2276
|
-
process.exit(1);
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
export async function memoryStatsCommand() {
|
|
2280
|
-
const ora = (await import('ora')).default;
|
|
2281
|
-
const chalk = (await import('chalk')).default;
|
|
2282
|
-
const spinner = ora('Loading stats...').start();
|
|
2283
|
-
try {
|
|
2284
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2285
|
-
const cwd = process.cwd();
|
|
2286
|
-
const configManager = createConfigManager(cwd);
|
|
2287
|
-
const config = await configManager.loadConfig();
|
|
2288
|
-
if (!config.memory?.enabled) {
|
|
2289
|
-
spinner.fail('Memory system is not enabled.');
|
|
2290
|
-
return;
|
|
2291
|
-
}
|
|
2292
|
-
const { createMemoryManager } = await import('../memory/memory-manager.js');
|
|
2293
|
-
const manager = createMemoryManager(cwd, config.memory);
|
|
2294
|
-
const stats = await manager.getStats();
|
|
2295
|
-
spinner.succeed('Memory statistics');
|
|
2296
|
-
const sizeMB = (stats.dbSizeBytes / 1024 / 1024).toFixed(2);
|
|
2297
|
-
const maxMB = (stats.maxSizeBytes / 1024 / 1024).toFixed(0);
|
|
2298
|
-
const usage = stats.usagePercent.toFixed(1);
|
|
2299
|
-
const bar = '█'.repeat(Math.floor(stats.usagePercent / 5)) +
|
|
2300
|
-
'░'.repeat(20 - Math.floor(stats.usagePercent / 5));
|
|
2301
|
-
console.log(`\n Memories: ${chalk.cyan(stats.memoryCount)}`);
|
|
2302
|
-
console.log(` Sessions: ${chalk.cyan(stats.sessionCount)}`);
|
|
2303
|
-
console.log(` DB Size: ${chalk.cyan(sizeMB + ' MB')} / ${maxMB} MB`);
|
|
2304
|
-
console.log(` Usage: [${stats.usagePercent > 80 ? chalk.red(bar) : chalk.green(bar)}] ${usage}%`);
|
|
2305
|
-
console.log(` Health: ${stats.indexHealth === 'good' ? chalk.green(stats.indexHealth) : chalk.yellow(stats.indexHealth)}`);
|
|
2306
|
-
await manager.close();
|
|
2307
|
-
}
|
|
2308
|
-
catch (error) {
|
|
2309
|
-
spinner.fail('Failed to load stats');
|
|
2310
|
-
console.error(chalk.red(String(error)));
|
|
2311
|
-
process.exit(1);
|
|
2312
|
-
}
|
|
2313
|
-
}
|
|
2314
|
-
export async function memoryVerifyCommand() {
|
|
2315
|
-
const ora = (await import('ora')).default;
|
|
2316
|
-
const chalk = (await import('chalk')).default;
|
|
2317
|
-
const spinner = ora('Verifying memory system...').start();
|
|
2318
|
-
try {
|
|
2319
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2320
|
-
const cwd = process.cwd();
|
|
2321
|
-
const configManager = createConfigManager(cwd);
|
|
2322
|
-
const config = await configManager.loadConfig();
|
|
2323
|
-
const memoryEnabled = config.memory?.enabled ?? false;
|
|
2324
|
-
const dbPathRelative = config.memory?.dbPath ?? '.rulebook/memory/memory.db';
|
|
2325
|
-
const dbPathAbsolute = path.join(cwd, dbPathRelative);
|
|
2326
|
-
spinner.succeed('Memory verification');
|
|
2327
|
-
// Check enabled status
|
|
2328
|
-
console.log(`\n ${memoryEnabled ? chalk.green('✓') : chalk.red('✗')} Memory enabled: ${memoryEnabled}`);
|
|
2329
|
-
// Check DB path
|
|
2330
|
-
console.log(` ${chalk.green('✓')} DB path: ${dbPathRelative}`);
|
|
2331
|
-
// Check if file exists on disk
|
|
2332
|
-
const fileExists = existsSync(dbPathAbsolute);
|
|
2333
|
-
if (fileExists) {
|
|
2334
|
-
const { statSync } = await import('fs');
|
|
2335
|
-
const fileStat = statSync(dbPathAbsolute);
|
|
2336
|
-
const sizeKB = (fileStat.size / 1024).toFixed(1);
|
|
2337
|
-
console.log(` ${chalk.green('✓')} File exists: YES (${sizeKB} KB)`);
|
|
2338
|
-
}
|
|
2339
|
-
else {
|
|
2340
|
-
console.log(` ${chalk.red('✗')} File exists: NO`);
|
|
2341
|
-
}
|
|
2342
|
-
// If memory is enabled and file exists, show record count
|
|
2343
|
-
if (memoryEnabled && fileExists) {
|
|
2344
|
-
try {
|
|
2345
|
-
const { createMemoryManager } = await import('../memory/memory-manager.js');
|
|
2346
|
-
const manager = createMemoryManager(cwd, config.memory);
|
|
2347
|
-
const stats = await manager.getStats();
|
|
2348
|
-
console.log(` ${chalk.green('✓')} Record count: ${stats.memoryCount} memories`);
|
|
2349
|
-
await manager.close();
|
|
2350
|
-
}
|
|
2351
|
-
catch (error) {
|
|
2352
|
-
console.log(` ${chalk.yellow('!')} Record count: unable to read (${String(error)})`);
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
else if (!memoryEnabled) {
|
|
2356
|
-
console.log(` ${chalk.yellow('!')} Enable memory with: ${chalk.bold('rulebook config --feature memory --enable')}`);
|
|
2357
|
-
}
|
|
2358
|
-
console.log('');
|
|
2359
|
-
}
|
|
2360
|
-
catch (error) {
|
|
2361
|
-
spinner.fail('Memory verification failed');
|
|
2362
|
-
console.error(chalk.red(String(error)));
|
|
2363
|
-
process.exit(1);
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
|
-
export async function memoryCleanupCommand(options) {
|
|
2367
|
-
const ora = (await import('ora')).default;
|
|
2368
|
-
const chalk = (await import('chalk')).default;
|
|
2369
|
-
const spinner = ora('Running cleanup...').start();
|
|
2370
|
-
try {
|
|
2371
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2372
|
-
const cwd = process.cwd();
|
|
2373
|
-
const configManager = createConfigManager(cwd);
|
|
2374
|
-
const config = await configManager.loadConfig();
|
|
2375
|
-
if (!config.memory?.enabled) {
|
|
2376
|
-
spinner.fail('Memory system is not enabled.');
|
|
2377
|
-
return;
|
|
2378
|
-
}
|
|
2379
|
-
const { createMemoryManager } = await import('../memory/memory-manager.js');
|
|
2380
|
-
const manager = createMemoryManager(cwd, config.memory);
|
|
2381
|
-
const result = await manager.cleanup(options.force || false);
|
|
2382
|
-
if (result.evictedCount > 0) {
|
|
2383
|
-
const freedMB = (result.freedBytes / 1024 / 1024).toFixed(2);
|
|
2384
|
-
spinner.succeed(`Cleaned up ${result.evictedCount} memories (freed ${freedMB} MB)`);
|
|
2385
|
-
}
|
|
2386
|
-
else {
|
|
2387
|
-
spinner.succeed('No cleanup needed');
|
|
2388
|
-
}
|
|
2389
|
-
await manager.close();
|
|
2390
|
-
}
|
|
2391
|
-
catch (error) {
|
|
2392
|
-
spinner.fail('Cleanup failed');
|
|
2393
|
-
console.error(chalk.red(String(error)));
|
|
2394
|
-
process.exit(1);
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
export async function memoryExportCommand(options) {
|
|
2398
|
-
const ora = (await import('ora')).default;
|
|
2399
|
-
const chalk = (await import('chalk')).default;
|
|
2400
|
-
const spinner = ora('Exporting memories...').start();
|
|
2401
|
-
try {
|
|
2402
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2403
|
-
const cwd = process.cwd();
|
|
2404
|
-
const configManager = createConfigManager(cwd);
|
|
2405
|
-
const config = await configManager.loadConfig();
|
|
2406
|
-
if (!config.memory?.enabled) {
|
|
2407
|
-
spinner.fail('Memory system is not enabled.');
|
|
2408
|
-
return;
|
|
2409
|
-
}
|
|
2410
|
-
const { createMemoryManager } = await import('../memory/memory-manager.js');
|
|
2411
|
-
const manager = createMemoryManager(cwd, config.memory);
|
|
2412
|
-
const format = (options.format || 'json');
|
|
2413
|
-
const exported = await manager.exportMemories(format);
|
|
2414
|
-
const count = format === 'json' ? JSON.parse(exported).length : exported.split('\n').length - 1;
|
|
2415
|
-
if (options.output) {
|
|
2416
|
-
const { writeFile } = await import('fs/promises');
|
|
2417
|
-
await writeFile(options.output, exported);
|
|
2418
|
-
spinner.succeed(`Exported ${count} memories to ${chalk.cyan(options.output)}`);
|
|
2419
|
-
}
|
|
2420
|
-
else {
|
|
2421
|
-
spinner.succeed(`Exported ${count} memories`);
|
|
2422
|
-
console.log(exported);
|
|
2423
|
-
}
|
|
2424
|
-
await manager.close();
|
|
2425
|
-
}
|
|
2426
|
-
catch (error) {
|
|
2427
|
-
spinner.fail('Export failed');
|
|
2428
|
-
console.error(chalk.red(String(error)));
|
|
2429
|
-
process.exit(1);
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
// Ralph Autonomous Loop Commands (v3.0)
|
|
2433
|
-
export async function ralphInitCommand() {
|
|
2434
|
-
const oraModule = await import('ora');
|
|
2435
|
-
const ora = oraModule.default;
|
|
2436
|
-
const spinner = ora('Initializing Ralph autonomous loop...').start();
|
|
2437
|
-
try {
|
|
2438
|
-
const cwd = process.cwd();
|
|
2439
|
-
const { Logger } = await import('../core/logger.js');
|
|
2440
|
-
const { RalphManager } = await import('../core/ralph-manager.js');
|
|
2441
|
-
const { PRDGenerator } = await import('../core/prd-generator.js');
|
|
2442
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2443
|
-
const logger = new Logger(cwd);
|
|
2444
|
-
const configManager = createConfigManager(cwd);
|
|
2445
|
-
const config = await configManager.loadConfig();
|
|
2446
|
-
// Create managers
|
|
2447
|
-
const ralphManager = new RalphManager(cwd, logger);
|
|
2448
|
-
const prdGenerator = new PRDGenerator(cwd, logger);
|
|
2449
|
-
// Initialize Ralph
|
|
2450
|
-
const maxIterations = config.ralph?.maxIterations || 10;
|
|
2451
|
-
const tool = (config.ralph?.tool || 'claude');
|
|
2452
|
-
await ralphManager.initialize(maxIterations, tool);
|
|
2453
|
-
// Generate PRD from rulebook tasks
|
|
2454
|
-
const prd = await prdGenerator.generatePRD(path.basename(cwd));
|
|
2455
|
-
// Save PRD
|
|
2456
|
-
const prdPath = path.join(cwd, '.rulebook', 'ralph', 'prd.json');
|
|
2457
|
-
await writeFile(prdPath, JSON.stringify(prd, null, 2));
|
|
2458
|
-
spinner.succeed(`Ralph initialized: ${prd.userStories.length} user stories loaded`);
|
|
2459
|
-
console.log(`\n 📋 PRD: ${prdPath}`);
|
|
2460
|
-
console.log(` 🔄 Max iterations: ${maxIterations}`);
|
|
2461
|
-
console.log(` 🤖 AI Tool: ${tool}`);
|
|
2462
|
-
console.log(`\n Run: ${chalk.bold('rulebook ralph run')}\n`);
|
|
2463
|
-
}
|
|
2464
|
-
catch (error) {
|
|
2465
|
-
spinner.fail('Ralph initialization failed');
|
|
2466
|
-
console.error(chalk.red(String(error)));
|
|
2467
|
-
process.exit(1);
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
export async function ralphRunCommand(options) {
|
|
2471
|
-
const oraModule = await import('ora');
|
|
2472
|
-
const ora = oraModule.default;
|
|
2473
|
-
const spinner = ora('Starting Ralph autonomous loop...').start();
|
|
2474
|
-
try {
|
|
2475
|
-
const cwd = process.cwd();
|
|
2476
|
-
const { Logger } = await import('../core/logger.js');
|
|
2477
|
-
const { RalphManager } = await import('../core/ralph-manager.js');
|
|
2478
|
-
const { RalphParser } = await import('../agents/ralph-parser.js');
|
|
2479
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2480
|
-
const { IterationTracker } = await import('../core/iteration-tracker.js');
|
|
2481
|
-
const childProcess = await import('child_process');
|
|
2482
|
-
const logger = new Logger(cwd);
|
|
2483
|
-
const configManager = createConfigManager(cwd);
|
|
2484
|
-
const config = await configManager.loadConfig();
|
|
2485
|
-
const ralphManager = new RalphManager(cwd, logger);
|
|
2486
|
-
const maxIterations = options.maxIterations || config.ralph?.maxIterations || 10;
|
|
2487
|
-
const tool = options.tool || config.ralph?.tool || 'claude';
|
|
2488
|
-
// Resolve parallel mode — CLI flag takes precedence over config
|
|
2489
|
-
const parallelWorkers = options.parallel ??
|
|
2490
|
-
(config.ralph?.parallel?.enabled ? config.ralph.parallel.maxWorkers : undefined);
|
|
2491
|
-
// Resolve plan checkpoint config — --plan-first CLI flag takes precedence
|
|
2492
|
-
const planCheckpointConfig = {
|
|
2493
|
-
enabled: options.planFirst ?? config.ralph?.planCheckpoint?.enabled ?? false,
|
|
2494
|
-
autoApproveAfterSeconds: config.ralph?.planCheckpoint?.autoApproveAfterSeconds ?? 0,
|
|
2495
|
-
requireApprovalForStories: config.ralph?.planCheckpoint?.requireApprovalForStories ?? 'all',
|
|
2496
|
-
};
|
|
2497
|
-
// Context compression config
|
|
2498
|
-
const compressionConfig = config.ralph?.contextCompression;
|
|
2499
|
-
const compressionEnabled = compressionConfig?.enabled !== false;
|
|
2500
|
-
const compressionRecentCount = compressionConfig?.recentCount ?? 3;
|
|
2501
|
-
const compressionThreshold = compressionConfig?.threshold ?? 5;
|
|
2502
|
-
const iterationTracker = new IterationTracker(cwd, logger);
|
|
2503
|
-
await iterationTracker.initialize();
|
|
2504
|
-
await ralphManager.initialize(maxIterations, tool);
|
|
2505
|
-
// Create git branch from PRD
|
|
2506
|
-
const prd = await ralphManager.loadPRD();
|
|
2507
|
-
if (prd?.branchName) {
|
|
2508
|
-
await ralphCreateBranch(cwd, prd.branchName);
|
|
2509
|
-
}
|
|
2510
|
-
// Handle Ctrl+C for graceful pause
|
|
2511
|
-
let interrupted = false;
|
|
2512
|
-
const handleInterrupt = async () => {
|
|
2513
|
-
interrupted = true;
|
|
2514
|
-
spinner.warn('Pausing after current iteration...');
|
|
2515
|
-
await ralphManager.pause();
|
|
2516
|
-
};
|
|
2517
|
-
process.on('SIGINT', handleInterrupt);
|
|
2518
|
-
// Sync task count from PRD (may have been saved after initialize)
|
|
2519
|
-
await ralphManager.refreshTaskCount();
|
|
2520
|
-
// ─── Parallel execution mode ───
|
|
2521
|
-
if (parallelWorkers && parallelWorkers > 1) {
|
|
2522
|
-
spinner.text = `Ralph parallel mode (${parallelWorkers} workers)...`;
|
|
2523
|
-
const batches = await ralphManager.getParallelBatches(parallelWorkers);
|
|
2524
|
-
spinner.stop();
|
|
2525
|
-
console.log(chalk.bold.cyan(`\n Parallel mode: ${batches.length} batch(es), max ${parallelWorkers} workers\n`));
|
|
2526
|
-
let iterationCount = 0;
|
|
2527
|
-
for (const batch of batches) {
|
|
2528
|
-
if (interrupted)
|
|
2529
|
-
break;
|
|
2530
|
-
console.log(chalk.bold(` ── Batch: ${batch.map((s) => s.id).join(', ')} (${batch.length} stories) ──`));
|
|
2531
|
-
// Run all stories in the batch concurrently
|
|
2532
|
-
const batchResults = await Promise.allSettled(batch.map(async (task) => {
|
|
2533
|
-
iterationCount++;
|
|
2534
|
-
const localIteration = iterationCount;
|
|
2535
|
-
const startTime = Date.now();
|
|
2536
|
-
// Build context (shared — read-only)
|
|
2537
|
-
let contextHistory = '';
|
|
2538
|
-
if (compressionEnabled) {
|
|
2539
|
-
contextHistory = await iterationTracker.buildCompressedContext(compressionRecentCount, compressionThreshold);
|
|
2540
|
-
}
|
|
2541
|
-
let plansContext = '';
|
|
2542
|
-
try {
|
|
2543
|
-
const { readPlans, plansExists } = await import('../core/plans-manager.js');
|
|
2544
|
-
if (plansExists(cwd)) {
|
|
2545
|
-
const plans = await readPlans(cwd);
|
|
2546
|
-
if (plans?.context && plans.context.trim()) {
|
|
2547
|
-
plansContext = plans.context.trim();
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
catch {
|
|
2552
|
-
// PLANS.md injection is optional
|
|
2553
|
-
}
|
|
2554
|
-
const prompt = ralphBuildPrompt(task, prd, contextHistory, plansContext);
|
|
2555
|
-
let agentOutput = '';
|
|
2556
|
-
try {
|
|
2557
|
-
agentOutput = await ralphExecuteAgent(tool, prompt, cwd, childProcess.spawn);
|
|
2558
|
-
}
|
|
2559
|
-
catch (agentError) {
|
|
2560
|
-
agentOutput = `Error executing agent: ${agentError.message || agentError}`;
|
|
2561
|
-
}
|
|
2562
|
-
const qualityResults = await ralphRunQualityGates(cwd, childProcess.spawn);
|
|
2563
|
-
const executionTime = Date.now() - startTime;
|
|
2564
|
-
const parsed = RalphParser.parseAgentOutput(agentOutput, localIteration, task.id, task.title, tool);
|
|
2565
|
-
const allGatesPass = qualityResults.type_check &&
|
|
2566
|
-
qualityResults.lint &&
|
|
2567
|
-
qualityResults.tests &&
|
|
2568
|
-
qualityResults.coverage_met;
|
|
2569
|
-
const passCount = Object.values(qualityResults).filter(Boolean).length;
|
|
2570
|
-
const status = allGatesPass
|
|
2571
|
-
? 'success'
|
|
2572
|
-
: passCount >= 2
|
|
2573
|
-
? 'partial'
|
|
2574
|
-
: 'failed';
|
|
2575
|
-
let gitCommit;
|
|
2576
|
-
if (allGatesPass) {
|
|
2577
|
-
gitCommit = await ralphGitCommit(cwd, task, localIteration, childProcess.spawn);
|
|
2578
|
-
await ralphManager.markStoryComplete(task.id);
|
|
2579
|
-
console.log(chalk.green(` [parallel] Story ${task.id} completed`));
|
|
2580
|
-
}
|
|
2581
|
-
else {
|
|
2582
|
-
console.log(chalk.yellow(` [parallel] Story ${task.id} not completed (quality gates failed)`));
|
|
2583
|
-
}
|
|
2584
|
-
const result = {
|
|
2585
|
-
iteration: localIteration,
|
|
2586
|
-
timestamp: new Date().toISOString(),
|
|
2587
|
-
task_id: task.id,
|
|
2588
|
-
task_title: task.title,
|
|
2589
|
-
status,
|
|
2590
|
-
ai_tool: tool,
|
|
2591
|
-
execution_time_ms: executionTime,
|
|
2592
|
-
quality_checks: qualityResults,
|
|
2593
|
-
output_summary: parsed.output_summary || `Iteration ${localIteration}: ${task.title}`,
|
|
2594
|
-
git_commit: gitCommit,
|
|
2595
|
-
learnings: parsed.learnings,
|
|
2596
|
-
errors: parsed.errors,
|
|
2597
|
-
metadata: {
|
|
2598
|
-
context_loss_count: parsed.metadata.context_loss_count,
|
|
2599
|
-
parsed_completion: parsed.metadata.parsed_completion,
|
|
2600
|
-
},
|
|
2601
|
-
};
|
|
2602
|
-
await ralphManager.recordIteration(result);
|
|
2603
|
-
return result;
|
|
2604
|
-
}));
|
|
2605
|
-
// Log rejected promises
|
|
2606
|
-
for (const [i, result] of batchResults.entries()) {
|
|
2607
|
-
if (result.status === 'rejected') {
|
|
2608
|
-
const story = batch[i];
|
|
2609
|
-
console.log(chalk.red(` [parallel] Story ${story.id} threw: ${result.reason}`));
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
// Check for pause
|
|
2613
|
-
const currentStatus = await ralphManager.getStatus();
|
|
2614
|
-
if (currentStatus?.paused)
|
|
2615
|
-
break;
|
|
2616
|
-
}
|
|
2617
|
-
// Cleanup and summary for parallel mode
|
|
2618
|
-
process.removeListener('SIGINT', handleInterrupt);
|
|
2619
|
-
const stats = await ralphManager.getTaskStats();
|
|
2620
|
-
console.log(`\n Parallel run complete: ${stats.completed}/${stats.total} tasks completed`);
|
|
2621
|
-
console.log(` Iterations: ${iterationCount}`);
|
|
2622
|
-
if (interrupted) {
|
|
2623
|
-
console.log(chalk.yellow(` Paused by user. Resume: ${chalk.bold('rulebook ralph resume')}`));
|
|
2624
|
-
}
|
|
2625
|
-
console.log(`\n View history: ${chalk.bold('rulebook ralph history')}\n`);
|
|
2626
|
-
return;
|
|
2627
|
-
}
|
|
2628
|
-
// ─── Sequential execution (default) ───
|
|
2629
|
-
spinner.text = 'Ralph loop running (Ctrl+C to pause)...';
|
|
2630
|
-
let iterationCount = 0;
|
|
2631
|
-
while (ralphManager.canContinue() && !interrupted) {
|
|
2632
|
-
iterationCount++;
|
|
2633
|
-
const task = await ralphManager.getNextTask();
|
|
2634
|
-
if (!task) {
|
|
2635
|
-
break;
|
|
2636
|
-
}
|
|
2637
|
-
spinner.stop();
|
|
2638
|
-
console.log(chalk.bold.cyan(`\n ── Iteration ${iterationCount}: ${task.title} ──\n`));
|
|
2639
|
-
const startTime = Date.now();
|
|
2640
|
-
// 0. Plan checkpoint — require approval before implementation
|
|
2641
|
-
if (planCheckpointConfig.enabled) {
|
|
2642
|
-
const checkpoint = await ralphManager.runCheckpoint(task, tool, planCheckpointConfig);
|
|
2643
|
-
if (!checkpoint.proceed) {
|
|
2644
|
-
console.log(chalk.yellow(` Plan rejected for ${task.id}. Skipping implementation.`));
|
|
2645
|
-
if (checkpoint.feedback) {
|
|
2646
|
-
console.log(chalk.gray(` Feedback: ${checkpoint.feedback}`));
|
|
2647
|
-
}
|
|
2648
|
-
spinner.start('Preparing next iteration...');
|
|
2649
|
-
continue;
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
// 1. Execute AI agent with task context
|
|
2653
|
-
let contextHistory = '';
|
|
2654
|
-
if (compressionEnabled) {
|
|
2655
|
-
contextHistory = await iterationTracker.buildCompressedContext(compressionRecentCount, compressionThreshold);
|
|
2656
|
-
}
|
|
2657
|
-
// Read PLANS.md context for session scratchpad injection
|
|
2658
|
-
let plansContext = '';
|
|
2659
|
-
try {
|
|
2660
|
-
const { readPlans, plansExists } = await import('../core/plans-manager.js');
|
|
2661
|
-
if (plansExists(cwd)) {
|
|
2662
|
-
const plans = await readPlans(cwd);
|
|
2663
|
-
if (plans?.context && plans.context.trim()) {
|
|
2664
|
-
plansContext = plans.context.trim();
|
|
2665
|
-
}
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
catch {
|
|
2669
|
-
// PLANS.md injection is optional — skip on error
|
|
2670
|
-
}
|
|
2671
|
-
const prompt = ralphBuildPrompt(task, prd, contextHistory, plansContext);
|
|
2672
|
-
let agentOutput = '';
|
|
2673
|
-
try {
|
|
2674
|
-
agentOutput = await ralphExecuteAgent(tool, prompt, cwd, childProcess.spawn);
|
|
2675
|
-
}
|
|
2676
|
-
catch (agentError) {
|
|
2677
|
-
agentOutput = `Error executing agent: ${agentError.message || agentError}`;
|
|
2678
|
-
console.log(chalk.red(` Agent error: ${agentError.message || agentError}`));
|
|
2679
|
-
}
|
|
2680
|
-
// 2. Run quality gates
|
|
2681
|
-
spinner.start('Running quality gates...');
|
|
2682
|
-
const qualityResults = await ralphRunQualityGates(cwd, childProcess.spawn);
|
|
2683
|
-
spinner.stop();
|
|
2684
|
-
// Print quality gate results
|
|
2685
|
-
const gateIcon = (pass) => (pass ? chalk.green('✓') : chalk.red('✗'));
|
|
2686
|
-
console.log(` ${gateIcon(qualityResults.type_check)} type-check`);
|
|
2687
|
-
console.log(` ${gateIcon(qualityResults.lint)} lint`);
|
|
2688
|
-
console.log(` ${gateIcon(qualityResults.tests)} tests`);
|
|
2689
|
-
console.log(` ${gateIcon(qualityResults.coverage_met)} coverage`);
|
|
2690
|
-
const executionTime = Date.now() - startTime;
|
|
2691
|
-
// 3. Parse agent output for learnings/errors
|
|
2692
|
-
const parsed = RalphParser.parseAgentOutput(agentOutput, iterationCount, task.id, task.title, tool);
|
|
2693
|
-
// 4. Determine status from real quality gates
|
|
2694
|
-
const allGatesPass = qualityResults.type_check &&
|
|
2695
|
-
qualityResults.lint &&
|
|
2696
|
-
qualityResults.tests &&
|
|
2697
|
-
qualityResults.coverage_met;
|
|
2698
|
-
const passCount = Object.values(qualityResults).filter(Boolean).length;
|
|
2699
|
-
const status = allGatesPass
|
|
2700
|
-
? 'success'
|
|
2701
|
-
: passCount >= 2
|
|
2702
|
-
? 'partial'
|
|
2703
|
-
: 'failed';
|
|
2704
|
-
// 5. Git commit if successful
|
|
2705
|
-
let gitCommit;
|
|
2706
|
-
if (allGatesPass) {
|
|
2707
|
-
gitCommit = await ralphGitCommit(cwd, task, iterationCount, childProcess.spawn);
|
|
2708
|
-
await ralphManager.markStoryComplete(task.id);
|
|
2709
|
-
console.log(chalk.green(`\n ✅ Story ${task.id} completed`));
|
|
2710
|
-
}
|
|
2711
|
-
else {
|
|
2712
|
-
console.log(chalk.yellow(`\n ⚠ Story ${task.id} not completed (quality gates failed)`));
|
|
2713
|
-
}
|
|
2714
|
-
// 6. Record iteration
|
|
2715
|
-
const result = {
|
|
2716
|
-
iteration: iterationCount,
|
|
2717
|
-
timestamp: new Date().toISOString(),
|
|
2718
|
-
task_id: task.id,
|
|
2719
|
-
task_title: task.title,
|
|
2720
|
-
status,
|
|
2721
|
-
ai_tool: tool,
|
|
2722
|
-
execution_time_ms: executionTime,
|
|
2723
|
-
quality_checks: qualityResults,
|
|
2724
|
-
output_summary: parsed.output_summary || `Iteration ${iterationCount}: ${task.title}`,
|
|
2725
|
-
git_commit: gitCommit,
|
|
2726
|
-
learnings: parsed.learnings,
|
|
2727
|
-
errors: parsed.errors,
|
|
2728
|
-
metadata: {
|
|
2729
|
-
context_loss_count: parsed.metadata.context_loss_count,
|
|
2730
|
-
parsed_completion: parsed.metadata.parsed_completion,
|
|
2731
|
-
},
|
|
2732
|
-
};
|
|
2733
|
-
await ralphManager.recordIteration(result);
|
|
2734
|
-
spinner.start('Preparing next iteration...');
|
|
2735
|
-
}
|
|
2736
|
-
// Cleanup
|
|
2737
|
-
process.removeListener('SIGINT', handleInterrupt);
|
|
2738
|
-
const stats = await ralphManager.getTaskStats();
|
|
2739
|
-
spinner.succeed(`Ralph loop complete: ${stats.completed}/${stats.total} tasks completed`);
|
|
2740
|
-
console.log(`\n ✅ Iterations: ${iterationCount}`);
|
|
2741
|
-
console.log(` 📊 Completed: ${stats.completed}/${stats.total}`);
|
|
2742
|
-
if (interrupted) {
|
|
2743
|
-
console.log(chalk.yellow(` ⏸ Paused by user. Resume: ${chalk.bold('rulebook ralph resume')}`));
|
|
2744
|
-
}
|
|
2745
|
-
console.log(`\n View history: ${chalk.bold('rulebook ralph history')}\n`);
|
|
2746
|
-
}
|
|
2747
|
-
catch (error) {
|
|
2748
|
-
spinner.fail('Ralph loop failed');
|
|
2749
|
-
console.error(chalk.red(String(error)));
|
|
2750
|
-
process.exit(1);
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
/**
|
|
2754
|
-
* Build prompt for AI agent from user story context
|
|
2755
|
-
*/
|
|
2756
|
-
function ralphBuildPrompt(task, prd, contextHistory, plansContext) {
|
|
2757
|
-
const criteria = (task.acceptanceCriteria || []).map((c) => `- ${c}`).join('\n');
|
|
2758
|
-
return [
|
|
2759
|
-
`You are working on project: ${prd?.project || 'unknown'}`,
|
|
2760
|
-
``,
|
|
2761
|
-
plansContext ? `## Session Context (PLANS.md)\n${plansContext}\n` : '',
|
|
2762
|
-
contextHistory && contextHistory !== 'No iteration history available.'
|
|
2763
|
-
? `## Iteration History\n${contextHistory}\n`
|
|
2764
|
-
: '',
|
|
2765
|
-
`## Current Task: ${task.title}`,
|
|
2766
|
-
`ID: ${task.id}`,
|
|
2767
|
-
``,
|
|
2768
|
-
`## Description`,
|
|
2769
|
-
task.description,
|
|
2770
|
-
``,
|
|
2771
|
-
`## Acceptance Criteria`,
|
|
2772
|
-
criteria,
|
|
2773
|
-
``,
|
|
2774
|
-
task.notes ? `## Notes\n${task.notes}\n` : '',
|
|
2775
|
-
`## Instructions`,
|
|
2776
|
-
`1. Implement the changes described above`,
|
|
2777
|
-
`2. Ensure all acceptance criteria are met`,
|
|
2778
|
-
`3. Run quality checks: type-check, lint, tests`,
|
|
2779
|
-
`4. Fix any issues found by quality checks`,
|
|
2780
|
-
`5. When done, summarize what was changed`,
|
|
2781
|
-
]
|
|
2782
|
-
.filter(Boolean)
|
|
2783
|
-
.join('\n');
|
|
2784
|
-
}
|
|
2785
|
-
/**
|
|
2786
|
-
* Execute AI agent and capture output
|
|
2787
|
-
*/
|
|
2788
|
-
async function ralphExecuteAgent(tool, prompt, cwd, spawn) {
|
|
2789
|
-
// Claude: use -p (print mode) with --dangerously-skip-permissions to allow file edits
|
|
2790
|
-
// Prompt is passed via stdin to avoid shell escaping issues and arg length limits
|
|
2791
|
-
const toolCommands = {
|
|
2792
|
-
claude: {
|
|
2793
|
-
cmd: 'claude',
|
|
2794
|
-
args: ['-p', '--dangerously-skip-permissions', '--verbose'],
|
|
2795
|
-
stdinPrompt: true,
|
|
2796
|
-
},
|
|
2797
|
-
amp: { cmd: 'amp', args: ['-p', prompt], stdinPrompt: false },
|
|
2798
|
-
gemini: { cmd: 'gemini', args: ['-p', prompt], stdinPrompt: false },
|
|
2799
|
-
};
|
|
2800
|
-
const config = toolCommands[tool] || toolCommands.claude;
|
|
2801
|
-
return new Promise((resolve, reject) => {
|
|
2802
|
-
let output = '';
|
|
2803
|
-
let errorOutput = '';
|
|
2804
|
-
const proc = spawn(config.cmd, config.args, {
|
|
2805
|
-
cwd,
|
|
2806
|
-
shell: true,
|
|
2807
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2808
|
-
env: { ...process.env },
|
|
2809
|
-
});
|
|
2810
|
-
// For Claude, write prompt to stdin then close it
|
|
2811
|
-
if (config.stdinPrompt && proc.stdin) {
|
|
2812
|
-
proc.stdin.write(prompt);
|
|
2813
|
-
proc.stdin.end();
|
|
2814
|
-
}
|
|
2815
|
-
proc.stdout?.on('data', (data) => {
|
|
2816
|
-
const text = data.toString();
|
|
2817
|
-
output += text;
|
|
2818
|
-
process.stdout.write(text);
|
|
2819
|
-
});
|
|
2820
|
-
proc.stderr?.on('data', (data) => {
|
|
2821
|
-
errorOutput += data.toString();
|
|
2822
|
-
});
|
|
2823
|
-
proc.on('close', (code) => {
|
|
2824
|
-
if (code === 0 || output.length > 0) {
|
|
2825
|
-
resolve(output || errorOutput);
|
|
2826
|
-
}
|
|
2827
|
-
else {
|
|
2828
|
-
reject(new Error(`Agent ${tool} exited with code ${code}: ${errorOutput.slice(0, 500)}`));
|
|
2829
|
-
}
|
|
2830
|
-
});
|
|
2831
|
-
proc.on('error', (err) => {
|
|
2832
|
-
reject(new Error(`Failed to start ${tool}: ${err.message}`));
|
|
2833
|
-
});
|
|
2834
|
-
// 10 minute timeout per iteration
|
|
2835
|
-
setTimeout(() => {
|
|
2836
|
-
proc.kill('SIGTERM');
|
|
2837
|
-
resolve(output || 'Agent execution timed out after 10 minutes');
|
|
2838
|
-
}, 600000);
|
|
2839
|
-
});
|
|
2840
|
-
}
|
|
2841
|
-
/**
|
|
2842
|
-
* Run quality gates and return results
|
|
2843
|
-
*/
|
|
2844
|
-
async function ralphRunQualityGates(cwd, spawn) {
|
|
2845
|
-
const runGate = (cmd, args) => {
|
|
2846
|
-
return new Promise((resolve) => {
|
|
2847
|
-
const proc = spawn(cmd, args, {
|
|
2848
|
-
cwd,
|
|
2849
|
-
shell: true,
|
|
2850
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2851
|
-
});
|
|
2852
|
-
proc.on('close', (code) => {
|
|
2853
|
-
resolve(code === 0);
|
|
2854
|
-
});
|
|
2855
|
-
proc.on('error', () => {
|
|
2856
|
-
resolve(false);
|
|
2857
|
-
});
|
|
2858
|
-
// 2 minute timeout per gate
|
|
2859
|
-
setTimeout(() => {
|
|
2860
|
-
proc.kill('SIGTERM');
|
|
2861
|
-
resolve(false);
|
|
2862
|
-
}, 120000);
|
|
2863
|
-
});
|
|
2864
|
-
};
|
|
2865
|
-
// Detect monorepo to choose the right test command
|
|
2866
|
-
const { detectMonorepo } = await import('../core/detector.js');
|
|
2867
|
-
const monorepo = await detectMonorepo(cwd).catch(() => ({
|
|
2868
|
-
detected: false,
|
|
2869
|
-
tool: null,
|
|
2870
|
-
packages: [],
|
|
2871
|
-
}));
|
|
2872
|
-
let testCmd = ['npm', ['test']];
|
|
2873
|
-
if (monorepo.detected) {
|
|
2874
|
-
if (monorepo.tool === 'turborepo')
|
|
2875
|
-
testCmd = ['turbo', ['run', 'test']];
|
|
2876
|
-
else if (monorepo.tool === 'nx')
|
|
2877
|
-
testCmd = ['nx', ['run-many', '--target=test']];
|
|
2878
|
-
}
|
|
2879
|
-
// Run gates in parallel
|
|
2880
|
-
const [typeCheck, lint, tests] = await Promise.all([
|
|
2881
|
-
runGate('npm', ['run', 'type-check']),
|
|
2882
|
-
runGate('npm', ['run', 'lint']),
|
|
2883
|
-
runGate(testCmd[0], testCmd[1]),
|
|
2884
|
-
]);
|
|
2885
|
-
return {
|
|
2886
|
-
type_check: typeCheck,
|
|
2887
|
-
lint: lint,
|
|
2888
|
-
tests: tests,
|
|
2889
|
-
coverage_met: tests,
|
|
2890
|
-
};
|
|
2891
|
-
}
|
|
2892
|
-
/**
|
|
2893
|
-
* Create git branch from PRD branchName
|
|
2894
|
-
*/
|
|
2895
|
-
async function ralphCreateBranch(cwd, branchName) {
|
|
2896
|
-
const { readFileSync } = await import('fs');
|
|
2897
|
-
const { spawn } = await import('child_process');
|
|
2898
|
-
// Check if already on the branch
|
|
2899
|
-
try {
|
|
2900
|
-
const gitHeadPath = path.join(cwd, '.git', 'HEAD');
|
|
2901
|
-
const head = readFileSync(gitHeadPath, 'utf8').trim();
|
|
2902
|
-
const currentBranch = head.replace('ref: refs/heads/', '');
|
|
2903
|
-
if (currentBranch === branchName) {
|
|
2904
|
-
return;
|
|
2905
|
-
}
|
|
2906
|
-
}
|
|
2907
|
-
catch {
|
|
2908
|
-
return;
|
|
2909
|
-
}
|
|
2910
|
-
// Create or checkout branch
|
|
2911
|
-
await new Promise((resolve) => {
|
|
2912
|
-
const proc = spawn('git', ['checkout', '-B', branchName], {
|
|
2913
|
-
cwd,
|
|
2914
|
-
shell: true,
|
|
2915
|
-
stdio: 'pipe',
|
|
2916
|
-
});
|
|
2917
|
-
proc.on('close', () => resolve());
|
|
2918
|
-
proc.on('error', () => resolve());
|
|
2919
|
-
});
|
|
2920
|
-
}
|
|
2921
|
-
/**
|
|
2922
|
-
* Commit changes after successful iteration
|
|
2923
|
-
*/
|
|
2924
|
-
async function ralphGitCommit(cwd, task, iteration, spawn) {
|
|
2925
|
-
// Stage all changes
|
|
2926
|
-
await new Promise((resolve) => {
|
|
2927
|
-
const proc = spawn('git', ['add', '-A'], { cwd, shell: true, stdio: 'pipe' });
|
|
2928
|
-
proc.on('close', () => resolve());
|
|
2929
|
-
proc.on('error', () => resolve());
|
|
2930
|
-
});
|
|
2931
|
-
// Check if there are staged changes
|
|
2932
|
-
const hasChanges = await new Promise((resolve) => {
|
|
2933
|
-
let output = '';
|
|
2934
|
-
const proc = spawn('git', ['diff', '--cached', '--stat'], {
|
|
2935
|
-
cwd,
|
|
2936
|
-
shell: true,
|
|
2937
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2938
|
-
});
|
|
2939
|
-
proc.stdout?.on('data', (data) => {
|
|
2940
|
-
output += data.toString();
|
|
2941
|
-
});
|
|
2942
|
-
proc.on('close', () => resolve(output.trim().length > 0));
|
|
2943
|
-
proc.on('error', () => resolve(false));
|
|
2944
|
-
});
|
|
2945
|
-
if (!hasChanges) {
|
|
2946
|
-
return undefined;
|
|
2947
|
-
}
|
|
2948
|
-
// Commit with Ralph message
|
|
2949
|
-
const commitMsg = `ralph(${task.id}): ${task.title}\n\nIteration ${iteration} - Ralph autonomous loop`;
|
|
2950
|
-
const commitHash = await new Promise((resolve) => {
|
|
2951
|
-
let output = '';
|
|
2952
|
-
const proc = spawn('git', ['commit', '-m', commitMsg], {
|
|
2953
|
-
cwd,
|
|
2954
|
-
shell: true,
|
|
2955
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2956
|
-
});
|
|
2957
|
-
proc.stdout?.on('data', (data) => {
|
|
2958
|
-
output += data.toString();
|
|
2959
|
-
});
|
|
2960
|
-
proc.on('close', (code) => {
|
|
2961
|
-
if (code === 0) {
|
|
2962
|
-
const hashMatch = output.match(/\[[\w/.-]+ ([a-f0-9]+)\]/);
|
|
2963
|
-
resolve(hashMatch ? hashMatch[1] : undefined);
|
|
2964
|
-
}
|
|
2965
|
-
else {
|
|
2966
|
-
resolve(undefined);
|
|
2967
|
-
}
|
|
2968
|
-
});
|
|
2969
|
-
proc.on('error', () => resolve(undefined));
|
|
2970
|
-
});
|
|
2971
|
-
return commitHash;
|
|
2972
|
-
}
|
|
2973
|
-
export async function ralphStatusCommand() {
|
|
2974
|
-
const oraModule = await import('ora');
|
|
2975
|
-
const ora = oraModule.default;
|
|
2976
|
-
const spinner = ora('Loading Ralph status...').start();
|
|
2977
|
-
try {
|
|
2978
|
-
const cwd = process.cwd();
|
|
2979
|
-
const { Logger } = await import('../core/logger.js');
|
|
2980
|
-
const { RalphManager } = await import('../core/ralph-manager.js');
|
|
2981
|
-
const logger = new Logger(cwd);
|
|
2982
|
-
const ralphManager = new RalphManager(cwd, logger);
|
|
2983
|
-
const status = await ralphManager.getStatus();
|
|
2984
|
-
if (!status) {
|
|
2985
|
-
spinner.fail('Ralph not initialized');
|
|
2986
|
-
console.log(`\n Run: ${chalk.bold('rulebook ralph init')}\n`);
|
|
2987
|
-
return;
|
|
2988
|
-
}
|
|
2989
|
-
spinner.stop();
|
|
2990
|
-
// Show agentsMode from config
|
|
2991
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2992
|
-
const configManager = createConfigManager(cwd);
|
|
2993
|
-
const cfg = await configManager.loadConfig();
|
|
2994
|
-
const agentsMode = cfg.agentsMode ?? 'full';
|
|
2995
|
-
console.log(`\n ${chalk.bold('Ralph Loop Status')}`);
|
|
2996
|
-
console.log(` Iteration: ${status.current_iteration}/${status.max_iterations}`);
|
|
2997
|
-
console.log(` Tasks: ${status.completed_tasks}/${status.total_tasks}`);
|
|
2998
|
-
console.log(` Status: ${status.paused ? chalk.yellow('PAUSED') : chalk.green('RUNNING')}`);
|
|
2999
|
-
console.log(` AI Tool: ${status.tool}`);
|
|
3000
|
-
console.log(` Started: ${new Date(status.started_at).toLocaleString()}`);
|
|
3001
|
-
console.log(` Agents Mode: ${agentsMode === 'lean' ? chalk.cyan('lean') : chalk.gray('full')} (rulebook mode set lean|full)`);
|
|
3002
|
-
console.log();
|
|
3003
|
-
}
|
|
3004
|
-
catch (error) {
|
|
3005
|
-
spinner.fail('Failed to load status');
|
|
3006
|
-
console.error(chalk.red(String(error)));
|
|
3007
|
-
process.exit(1);
|
|
3008
|
-
}
|
|
3009
|
-
}
|
|
3010
|
-
export async function ralphHistoryCommand(options) {
|
|
3011
|
-
const oraModule = await import('ora');
|
|
3012
|
-
const ora = oraModule.default;
|
|
3013
|
-
const spinner = ora('Loading iteration history...').start();
|
|
3014
|
-
try {
|
|
3015
|
-
const cwd = process.cwd();
|
|
3016
|
-
const { Logger } = await import('../core/logger.js');
|
|
3017
|
-
const { IterationTracker } = await import('../core/iteration-tracker.js');
|
|
3018
|
-
const logger = new Logger(cwd);
|
|
3019
|
-
const tracker = new IterationTracker(cwd, logger);
|
|
3020
|
-
const limit = options.limit || 10;
|
|
3021
|
-
const history = await tracker.getHistory(limit);
|
|
3022
|
-
if (history.length === 0) {
|
|
3023
|
-
spinner.fail('No iteration history found');
|
|
3024
|
-
return;
|
|
3025
|
-
}
|
|
3026
|
-
spinner.stop();
|
|
3027
|
-
console.log(`\n ${chalk.bold('Recent Iterations')} (${history.length})\n`);
|
|
3028
|
-
for (const iter of history) {
|
|
3029
|
-
const statusIcon = iter.status === 'success'
|
|
3030
|
-
? chalk.green('✓')
|
|
3031
|
-
: iter.status === 'partial'
|
|
3032
|
-
? chalk.yellow('◐')
|
|
3033
|
-
: chalk.red('✗');
|
|
3034
|
-
console.log(` ${statusIcon} Iteration ${iter.iteration}: ${iter.task_title}`);
|
|
3035
|
-
console.log(` Status: ${iter.status} | Duration: ${(iter.duration_ms || 0) / 1000}s`);
|
|
3036
|
-
console.log(` Checks: type=${iter.quality_checks.type_check ? '✓' : '✗'} lint=${iter.quality_checks.lint ? '✓' : '✗'} tests=${iter.quality_checks.tests ? '✓' : '✗'}`);
|
|
3037
|
-
if (iter.git_commit) {
|
|
3038
|
-
console.log(` Commit: ${iter.git_commit}`);
|
|
3039
|
-
}
|
|
3040
|
-
console.log();
|
|
3041
|
-
}
|
|
3042
|
-
// Show statistics
|
|
3043
|
-
const stats = await tracker.getStatistics();
|
|
3044
|
-
console.log(` ${chalk.bold('Statistics')}`);
|
|
3045
|
-
console.log(` Total: ${stats.total_iterations} | Success: ${stats.successful_iterations} | Failed: ${stats.failed_iterations}`);
|
|
3046
|
-
console.log(` Success rate: ${(stats.success_rate * 100).toFixed(1)}%`);
|
|
3047
|
-
console.log(` Avg duration: ${stats.average_duration_ms}ms\n`);
|
|
3048
|
-
}
|
|
3049
|
-
catch (error) {
|
|
3050
|
-
spinner.fail('Failed to load history');
|
|
3051
|
-
console.error(chalk.red(String(error)));
|
|
3052
|
-
process.exit(1);
|
|
3053
|
-
}
|
|
3054
|
-
}
|
|
3055
|
-
export async function ralphPauseCommand() {
|
|
3056
|
-
const oraModule = await import('ora');
|
|
3057
|
-
const ora = oraModule.default;
|
|
3058
|
-
const spinner = ora('Pausing Ralph loop...').start();
|
|
3059
|
-
try {
|
|
3060
|
-
const cwd = process.cwd();
|
|
3061
|
-
const { Logger } = await import('../core/logger.js');
|
|
3062
|
-
const { RalphManager } = await import('../core/ralph-manager.js');
|
|
3063
|
-
const logger = new Logger(cwd);
|
|
3064
|
-
const ralphManager = new RalphManager(cwd, logger);
|
|
3065
|
-
const status = await ralphManager.getStatus();
|
|
3066
|
-
if (!status) {
|
|
3067
|
-
spinner.fail('Ralph not initialized');
|
|
3068
|
-
console.log(`\n Run: ${chalk.bold('rulebook ralph init')}\n`);
|
|
3069
|
-
return;
|
|
3070
|
-
}
|
|
3071
|
-
await ralphManager.pause();
|
|
3072
|
-
spinner.succeed('Ralph loop paused');
|
|
3073
|
-
console.log(`\n Resume with: ${chalk.bold('rulebook ralph resume')}\n`);
|
|
3074
|
-
}
|
|
3075
|
-
catch (error) {
|
|
3076
|
-
spinner.fail('Failed to pause');
|
|
3077
|
-
console.error(chalk.red(String(error)));
|
|
3078
|
-
process.exit(1);
|
|
3079
|
-
}
|
|
3080
|
-
}
|
|
3081
|
-
export async function ralphResumeCommand() {
|
|
3082
|
-
const oraModule = await import('ora');
|
|
3083
|
-
const ora = oraModule.default;
|
|
3084
|
-
const spinner = ora('Resuming Ralph loop...').start();
|
|
3085
|
-
try {
|
|
3086
|
-
const cwd = process.cwd();
|
|
3087
|
-
const { Logger } = await import('../core/logger.js');
|
|
3088
|
-
const { RalphManager } = await import('../core/ralph-manager.js');
|
|
3089
|
-
const logger = new Logger(cwd);
|
|
3090
|
-
const ralphManager = new RalphManager(cwd, logger);
|
|
3091
|
-
const status = await ralphManager.getStatus();
|
|
3092
|
-
if (!status) {
|
|
3093
|
-
spinner.fail('Ralph not initialized');
|
|
3094
|
-
console.log(`\n Run: ${chalk.bold('rulebook ralph init')}\n`);
|
|
3095
|
-
return;
|
|
3096
|
-
}
|
|
3097
|
-
await ralphManager.resume();
|
|
3098
|
-
spinner.succeed('Ralph loop resumed');
|
|
3099
|
-
console.log(`\n Continue loop: ${chalk.bold('rulebook ralph run')}\n`);
|
|
3100
|
-
}
|
|
3101
|
-
catch (error) {
|
|
3102
|
-
spinner.fail('Failed to resume');
|
|
3103
|
-
console.error(chalk.red(String(error)));
|
|
3104
|
-
process.exit(1);
|
|
3105
|
-
}
|
|
3106
|
-
}
|
|
3107
|
-
export async function setupClaudeCodePlugin() {
|
|
3108
|
-
const oraModule = await import('ora');
|
|
3109
|
-
const ora = oraModule.default;
|
|
3110
|
-
const spinner = ora('Setting up Claude Code plugin...').start();
|
|
3111
|
-
try {
|
|
3112
|
-
const { readFile } = await import('../utils/file-system.js');
|
|
3113
|
-
const os = await import('os');
|
|
3114
|
-
const fs = await import('fs/promises');
|
|
3115
|
-
const url = await import('url');
|
|
3116
|
-
// Get plugin info from .claude-plugin/plugin.json relative to this module
|
|
3117
|
-
const packageDir = path.dirname(url.fileURLToPath(import.meta.url));
|
|
3118
|
-
const pluginJsonPath = path.join(packageDir, '..', '..', '.claude-plugin', 'plugin.json');
|
|
3119
|
-
const pluginJson = JSON.parse(await readFile(pluginJsonPath));
|
|
3120
|
-
// Get Claude Code plugins directory
|
|
3121
|
-
const homeDir = os.homedir();
|
|
3122
|
-
const pluginsDir = path.join(homeDir, '.claude', 'plugins');
|
|
3123
|
-
const installedPluginsPath = path.join(pluginsDir, 'installed_plugins.json');
|
|
3124
|
-
// Create plugins directory if it doesn't exist
|
|
3125
|
-
await fs.mkdir(pluginsDir, { recursive: true });
|
|
3126
|
-
// Load or create installed_plugins.json
|
|
3127
|
-
let installedPlugins = {
|
|
3128
|
-
version: 2,
|
|
3129
|
-
plugins: {},
|
|
3130
|
-
};
|
|
3131
|
-
if (existsSync(installedPluginsPath)) {
|
|
3132
|
-
const content = await readFile(installedPluginsPath);
|
|
3133
|
-
installedPlugins = JSON.parse(content);
|
|
3134
|
-
}
|
|
3135
|
-
// Add rulebook plugin — keep exactly ONE entry per plugin key
|
|
3136
|
-
const pluginKey = `rulebook@hivehub`;
|
|
3137
|
-
const version = pluginJson.version || '3.2.1';
|
|
3138
|
-
const installPath = path.join(pluginsDir, 'cache', 'hivehub', 'rulebook', version);
|
|
3139
|
-
if (!installedPlugins.plugins[pluginKey]) {
|
|
3140
|
-
installedPlugins.plugins[pluginKey] = [];
|
|
3141
|
-
}
|
|
3142
|
-
const entries = installedPlugins.plugins[pluginKey];
|
|
3143
|
-
// Deduplicate: keep only the single latest entry, update it in-place
|
|
3144
|
-
if (entries.length > 0) {
|
|
3145
|
-
// Preserve original installedAt from the first entry
|
|
3146
|
-
const originalInstalledAt = entries[0].installedAt;
|
|
3147
|
-
// Replace all entries with a single updated one
|
|
3148
|
-
installedPlugins.plugins[pluginKey] = [
|
|
3149
|
-
{
|
|
3150
|
-
scope: 'user',
|
|
3151
|
-
installPath: installPath,
|
|
3152
|
-
version: version,
|
|
3153
|
-
installedAt: originalInstalledAt || new Date().toISOString(),
|
|
3154
|
-
lastUpdated: new Date().toISOString(),
|
|
3155
|
-
},
|
|
3156
|
-
];
|
|
3157
|
-
}
|
|
3158
|
-
else {
|
|
3159
|
-
// First install
|
|
3160
|
-
entries.push({
|
|
3161
|
-
scope: 'user',
|
|
3162
|
-
installPath: installPath,
|
|
3163
|
-
version: version,
|
|
3164
|
-
installedAt: new Date().toISOString(),
|
|
3165
|
-
lastUpdated: new Date().toISOString(),
|
|
3166
|
-
});
|
|
3167
|
-
}
|
|
3168
|
-
// Save installed_plugins.json
|
|
3169
|
-
await fs.writeFile(installedPluginsPath, JSON.stringify(installedPlugins, null, 2));
|
|
3170
|
-
spinner.succeed('Claude Code plugin installed');
|
|
3171
|
-
console.log(`\n ${chalk.green('✓')} Plugin: ${pluginKey} v${version}`);
|
|
3172
|
-
console.log(` ${chalk.gray('Installed to:')} ${installPath}`);
|
|
3173
|
-
console.log(`\n ${chalk.blue('Note:')} Restart Claude Code to load the plugin.\n`);
|
|
3174
|
-
}
|
|
3175
|
-
catch (error) {
|
|
3176
|
-
spinner.fail('Failed to install plugin');
|
|
3177
|
-
console.error(chalk.red(String(error)));
|
|
3178
|
-
process.exit(1);
|
|
3179
|
-
}
|
|
3180
|
-
}
|
|
3181
|
-
// ─── Plans Commands ────────────────────────────────────────────────────────
|
|
3182
|
-
/**
|
|
3183
|
-
* Add sequential-thinking MCP server entry to mcp.json (or .cursor/mcp.json).
|
|
3184
|
-
* Non-destructive: skips if already present.
|
|
3185
|
-
*/
|
|
3186
|
-
async function addSequentialThinkingMcp(cwd) {
|
|
3187
|
-
const { readFileSync, writeFileSync, existsSync } = await import('fs');
|
|
3188
|
-
const { mkdirSync } = await import('fs');
|
|
3189
|
-
const seqEntry = {
|
|
3190
|
-
command: 'npx',
|
|
3191
|
-
args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
|
|
3192
|
-
};
|
|
3193
|
-
const candidates = [
|
|
3194
|
-
path.join(cwd, 'mcp.json'),
|
|
3195
|
-
path.join(cwd, '.cursor', 'mcp.json'),
|
|
3196
|
-
path.join(cwd, '.mcp.json'),
|
|
3197
|
-
];
|
|
3198
|
-
// Find existing mcp.json or default to mcp.json
|
|
3199
|
-
let mcpPath = candidates.find((p) => existsSync(p)) ?? path.join(cwd, 'mcp.json');
|
|
3200
|
-
let mcpConfig = {};
|
|
3201
|
-
if (existsSync(mcpPath)) {
|
|
3202
|
-
try {
|
|
3203
|
-
mcpConfig = JSON.parse(readFileSync(mcpPath, 'utf8'));
|
|
3204
|
-
}
|
|
3205
|
-
catch {
|
|
3206
|
-
mcpConfig = {};
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
mcpConfig.mcpServers = mcpConfig.mcpServers ?? {};
|
|
3210
|
-
// Already configured under any key variant
|
|
3211
|
-
const keys = Object.keys(mcpConfig.mcpServers);
|
|
3212
|
-
const alreadyPresent = keys.some((k) => ['sequential-thinking', 'sequential_thinking', 'sequentialThinking'].includes(k));
|
|
3213
|
-
if (alreadyPresent)
|
|
3214
|
-
return;
|
|
3215
|
-
mcpConfig.mcpServers['sequential-thinking'] = seqEntry;
|
|
3216
|
-
// Ensure directory exists
|
|
3217
|
-
const dir = path.dirname(mcpPath);
|
|
3218
|
-
if (!existsSync(dir))
|
|
3219
|
-
mkdirSync(dir, { recursive: true });
|
|
3220
|
-
writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
3221
|
-
}
|
|
3222
|
-
/**
|
|
3223
|
-
* Show contents of AGENTS.override.md.
|
|
3224
|
-
*/
|
|
3225
|
-
export async function overrideShowCommand() {
|
|
3226
|
-
const cwd = process.cwd();
|
|
3227
|
-
const { overrideExists, getOverridePath, readOverrideContent } = await import('../core/override-manager.js');
|
|
3228
|
-
if (!overrideExists(cwd)) {
|
|
3229
|
-
console.log(chalk.yellow('AGENTS.override.md does not exist. Run `rulebook override edit` or `rulebook init` to create it.'));
|
|
3230
|
-
return;
|
|
3231
|
-
}
|
|
3232
|
-
const content = await readOverrideContent(cwd);
|
|
3233
|
-
if (!content) {
|
|
3234
|
-
console.log(chalk.gray('AGENTS.override.md exists but has no custom content yet.'));
|
|
3235
|
-
console.log(chalk.gray(` Path: ${getOverridePath(cwd)}`));
|
|
3236
|
-
return;
|
|
3237
|
-
}
|
|
3238
|
-
console.log(chalk.bold('\n📄 AGENTS.override.md\n'));
|
|
3239
|
-
console.log(content);
|
|
3240
|
-
console.log();
|
|
3241
|
-
}
|
|
3242
|
-
/**
|
|
3243
|
-
* Open AGENTS.override.md in $EDITOR, or print path if no EDITOR.
|
|
3244
|
-
*/
|
|
3245
|
-
export async function overrideEditCommand() {
|
|
3246
|
-
const cwd = process.cwd();
|
|
3247
|
-
const { initOverride, getOverridePath } = await import('../core/override-manager.js');
|
|
3248
|
-
await initOverride(cwd); // create if missing
|
|
3249
|
-
const overridePath = getOverridePath(cwd);
|
|
3250
|
-
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
3251
|
-
if (editor) {
|
|
3252
|
-
const { spawn } = await import('child_process');
|
|
3253
|
-
const proc = spawn(editor, [overridePath], { stdio: 'inherit', shell: true });
|
|
3254
|
-
await new Promise((resolve) => proc.on('close', () => resolve()));
|
|
3255
|
-
}
|
|
3256
|
-
else {
|
|
3257
|
-
console.log(chalk.gray(`No $EDITOR set. Edit the file directly:`));
|
|
3258
|
-
console.log(chalk.cyan(` ${overridePath}`));
|
|
3259
|
-
}
|
|
3260
|
-
}
|
|
3261
|
-
/**
|
|
3262
|
-
* Reset AGENTS.override.md to empty template.
|
|
3263
|
-
*/
|
|
3264
|
-
export async function overrideClearCommand() {
|
|
3265
|
-
const cwd = process.cwd();
|
|
3266
|
-
const { clearOverride } = await import('../core/override-manager.js');
|
|
3267
|
-
await clearOverride(cwd);
|
|
3268
|
-
console.log(chalk.green('✓ AGENTS.override.md reset to empty template'));
|
|
3269
|
-
}
|
|
3270
|
-
/**
|
|
3271
|
-
* Set the AGENTS.md generation mode (lean or full).
|
|
3272
|
-
*/
|
|
3273
|
-
export async function modeSetCommand(mode) {
|
|
3274
|
-
const cwd = process.cwd();
|
|
3275
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
3276
|
-
const configManager = createConfigManager(cwd);
|
|
3277
|
-
const config = await configManager.loadConfig();
|
|
3278
|
-
config.agentsMode = mode;
|
|
3279
|
-
await configManager.saveConfig(config);
|
|
3280
|
-
console.log(chalk.green(`✓ AGENTS.md mode set to: ${chalk.bold(mode)}`));
|
|
3281
|
-
if (mode === 'lean') {
|
|
3282
|
-
console.log(chalk.gray(' Lean mode: AGENTS.md will be a lightweight index (<3KB).\n' +
|
|
3283
|
-
' Run `rulebook update` to regenerate AGENTS.md.'));
|
|
3284
|
-
}
|
|
3285
|
-
else {
|
|
3286
|
-
console.log(chalk.gray(' Full mode: AGENTS.md will include all rules inline.\n' +
|
|
3287
|
-
' Run `rulebook update` to regenerate AGENTS.md.'));
|
|
3288
|
-
}
|
|
3289
|
-
}
|
|
3290
|
-
/**
|
|
3291
|
-
* Show current PLANS.md content.
|
|
3292
|
-
*/
|
|
3293
|
-
export async function plansShowCommand() {
|
|
3294
|
-
const { readPlans, getPlansPath } = await import('../core/plans-manager.js');
|
|
3295
|
-
const cwd = process.cwd();
|
|
3296
|
-
const plans = await readPlans(cwd);
|
|
3297
|
-
if (!plans) {
|
|
3298
|
-
console.log(chalk.yellow(`No PLANS.md found at ${getPlansPath(cwd)}`));
|
|
3299
|
-
console.log(chalk.gray('Run `rulebook plans init` to create one.'));
|
|
3300
|
-
return;
|
|
3301
|
-
}
|
|
3302
|
-
console.log(chalk.bold.blue('\n📋 PLANS.md — Session Scratchpad\n'));
|
|
3303
|
-
if (plans.context &&
|
|
3304
|
-
plans.context !== '_No active context. Start a session to populate this section._') {
|
|
3305
|
-
console.log(chalk.bold('Active Context:'));
|
|
3306
|
-
console.log(chalk.white(plans.context));
|
|
3307
|
-
}
|
|
3308
|
-
if (plans.currentTask && plans.currentTask !== '_No task in progress._') {
|
|
3309
|
-
console.log(chalk.bold('\nCurrent Task:'));
|
|
3310
|
-
console.log(chalk.cyan(plans.currentTask));
|
|
3311
|
-
}
|
|
3312
|
-
if (plans.history) {
|
|
3313
|
-
console.log(chalk.bold('\nSession History:'));
|
|
3314
|
-
console.log(chalk.gray(plans.history));
|
|
3315
|
-
}
|
|
3316
|
-
console.log('');
|
|
3317
|
-
}
|
|
3318
|
-
/**
|
|
3319
|
-
* Initialize PLANS.md in project root.
|
|
3320
|
-
*/
|
|
3321
|
-
export async function plansInitCommand() {
|
|
3322
|
-
const { initPlans, getPlansPath } = await import('../core/plans-manager.js');
|
|
3323
|
-
const cwd = process.cwd();
|
|
3324
|
-
const created = await initPlans(cwd);
|
|
3325
|
-
if (created) {
|
|
3326
|
-
console.log(chalk.green(`✓ Created ${getPlansPath(cwd)}`));
|
|
3327
|
-
console.log(chalk.gray(' AI agents will use this file for session continuity.'));
|
|
3328
|
-
}
|
|
3329
|
-
else {
|
|
3330
|
-
console.log(chalk.yellow(`PLANS.md already exists at ${getPlansPath(cwd)}`));
|
|
3331
|
-
}
|
|
3332
|
-
}
|
|
3333
|
-
/**
|
|
3334
|
-
* Reset PLANS.md to the empty template.
|
|
3335
|
-
*/
|
|
3336
|
-
export async function plansClearCommand() {
|
|
3337
|
-
const { clearPlans, getPlansPath } = await import('../core/plans-manager.js');
|
|
3338
|
-
const cwd = process.cwd();
|
|
3339
|
-
await clearPlans(cwd);
|
|
3340
|
-
console.log(chalk.green(`✓ Cleared ${getPlansPath(cwd)}`));
|
|
3341
|
-
console.log(chalk.gray(' Session history and context have been reset.'));
|
|
3342
|
-
}
|
|
3343
|
-
// ─── Continue Command ───────────────────────────────────────────────────────
|
|
3344
|
-
/**
|
|
3345
|
-
* `rulebook continue` — Print orientations to resume work in a new AI session.
|
|
3346
|
-
*
|
|
3347
|
-
* Aggregates context from:
|
|
3348
|
-
* 1. PLANS.md (session scratchpad)
|
|
3349
|
-
* 2. Active rulebook tasks (pending items in tasks.md)
|
|
3350
|
-
* 3. Recent git commits (last 5)
|
|
3351
|
-
* 4. Ralph status (if running)
|
|
3352
|
-
*
|
|
3353
|
-
* Outputs a structured prompt that the AI agent can paste at the start of a session.
|
|
3354
|
-
*/
|
|
3355
|
-
export async function continueCommand() {
|
|
3356
|
-
const cwd = process.cwd();
|
|
3357
|
-
const { readPlans } = await import('../core/plans-manager.js');
|
|
3358
|
-
const { exec } = await import('child_process');
|
|
3359
|
-
const { promisify } = await import('util');
|
|
3360
|
-
const execAsync = promisify(exec);
|
|
3361
|
-
const fs = await import('fs/promises');
|
|
3362
|
-
console.log(chalk.bold.blue('\n🔄 Generating session continuity context...\n'));
|
|
3363
|
-
const sections = [];
|
|
3364
|
-
// ── 1. PLANS.md context ──
|
|
3365
|
-
const plans = await readPlans(cwd);
|
|
3366
|
-
if (plans && (plans.context || plans.currentTask)) {
|
|
3367
|
-
const plansParts = ['## Active Plans'];
|
|
3368
|
-
if (plans.context && !plans.context.includes('_No active context')) {
|
|
3369
|
-
plansParts.push(plans.context);
|
|
3370
|
-
}
|
|
3371
|
-
if (plans.currentTask && !plans.currentTask.includes('_No task')) {
|
|
3372
|
-
plansParts.push(`**Current Task:** ${plans.currentTask}`);
|
|
3373
|
-
}
|
|
3374
|
-
sections.push(plansParts.join('\n'));
|
|
3375
|
-
}
|
|
3376
|
-
// ── 2. Active tasks (pending checklist items) ──
|
|
3377
|
-
const tasksDir = path.join(cwd, '.rulebook', 'tasks');
|
|
3378
|
-
if (existsSync(tasksDir)) {
|
|
3379
|
-
const taskSummaries = [];
|
|
3380
|
-
try {
|
|
3381
|
-
const entries = await fs.readdir(tasksDir, { withFileTypes: true });
|
|
3382
|
-
for (const entry of entries) {
|
|
3383
|
-
if (!entry.isDirectory() || entry.name === 'archive')
|
|
3384
|
-
continue;
|
|
3385
|
-
const tasksPath = path.join(tasksDir, entry.name, 'tasks.md');
|
|
3386
|
-
if (!existsSync(tasksPath))
|
|
3387
|
-
continue;
|
|
3388
|
-
const content = await fs.readFile(tasksPath, 'utf-8');
|
|
3389
|
-
const pending = content
|
|
3390
|
-
.split('\n')
|
|
3391
|
-
.filter((l) => l.match(/^- \[ \]/))
|
|
3392
|
-
.map((l) => l.replace(/^- \[ \]\s*/, '').trim())
|
|
3393
|
-
.slice(0, 3);
|
|
3394
|
-
if (pending.length > 0) {
|
|
3395
|
-
taskSummaries.push(`**${entry.name}** (${pending.length}+ pending):`);
|
|
3396
|
-
pending.forEach((p) => taskSummaries.push(` - ${p}`));
|
|
3397
|
-
}
|
|
3398
|
-
}
|
|
3399
|
-
}
|
|
3400
|
-
catch {
|
|
3401
|
-
// ignore
|
|
3402
|
-
}
|
|
3403
|
-
if (taskSummaries.length > 0) {
|
|
3404
|
-
sections.push('## Pending Tasks\n' + taskSummaries.join('\n'));
|
|
3405
|
-
}
|
|
3406
|
-
}
|
|
3407
|
-
// ── 3. Recent git commits ──
|
|
3408
|
-
try {
|
|
3409
|
-
const { stdout } = await execAsync('git log --oneline -8', { cwd });
|
|
3410
|
-
if (stdout.trim()) {
|
|
3411
|
-
sections.push('## Recent Commits\n```\n' + stdout.trim() + '\n```');
|
|
3412
|
-
}
|
|
3413
|
-
}
|
|
3414
|
-
catch {
|
|
3415
|
-
// not a git repo or git not available
|
|
3416
|
-
}
|
|
3417
|
-
// ── 4. Ralph status ──
|
|
3418
|
-
const ralphStatePath = path.join(cwd, '.rulebook', 'ralph', 'state.json');
|
|
3419
|
-
if (existsSync(ralphStatePath)) {
|
|
3420
|
-
try {
|
|
3421
|
-
const state = JSON.parse(await fs.readFile(ralphStatePath, 'utf-8'));
|
|
3422
|
-
if (state.enabled) {
|
|
3423
|
-
const prdPath = path.join(cwd, '.rulebook', 'ralph', 'prd.json');
|
|
3424
|
-
let prdInfo = '';
|
|
3425
|
-
if (existsSync(prdPath)) {
|
|
3426
|
-
const prd = JSON.parse(await fs.readFile(prdPath, 'utf-8'));
|
|
3427
|
-
const pending = (prd.userStories ?? []).filter((s) => !s.passes).length;
|
|
3428
|
-
const total = (prd.userStories ?? []).length;
|
|
3429
|
-
prdInfo = ` | ${total - pending}/${total} stories complete`;
|
|
3430
|
-
}
|
|
3431
|
-
sections.push(`## Ralph Status\n` +
|
|
3432
|
-
`Iteration ${state.current_iteration}/${state.max_iterations}${prdInfo} | Tool: ${state.tool} | Paused: ${state.paused}`);
|
|
3433
|
-
}
|
|
3434
|
-
}
|
|
3435
|
-
catch {
|
|
3436
|
-
// ignore
|
|
3437
|
-
}
|
|
3438
|
-
}
|
|
3439
|
-
// ── 5. Current branch ──
|
|
3440
|
-
try {
|
|
3441
|
-
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd });
|
|
3442
|
-
const branch = stdout.trim();
|
|
3443
|
-
if (branch) {
|
|
3444
|
-
sections.unshift(`## Branch\n\`${branch}\``);
|
|
3445
|
-
}
|
|
3446
|
-
}
|
|
3447
|
-
catch {
|
|
3448
|
-
// ignore
|
|
3449
|
-
}
|
|
3450
|
-
// ── Render ──
|
|
3451
|
-
if (sections.length === 0) {
|
|
3452
|
-
console.log(chalk.yellow('No session context found. Create tasks, a PLANS.md, or make some commits.'));
|
|
3453
|
-
return;
|
|
3454
|
-
}
|
|
3455
|
-
const output = [
|
|
3456
|
-
'─'.repeat(60),
|
|
3457
|
-
chalk.bold('📋 SESSION CONTINUITY CONTEXT'),
|
|
3458
|
-
chalk.gray('Paste this at the start of a new AI session:'),
|
|
3459
|
-
'─'.repeat(60),
|
|
3460
|
-
'',
|
|
3461
|
-
sections.join('\n\n'),
|
|
3462
|
-
'',
|
|
3463
|
-
'─'.repeat(60),
|
|
3464
|
-
].join('\n');
|
|
3465
|
-
console.log(output);
|
|
3466
|
-
// Also write to PLANS.md if it exists
|
|
3467
|
-
if (plans !== null) {
|
|
3468
|
-
const { appendPlansHistory } = await import('../core/plans-manager.js');
|
|
3469
|
-
try {
|
|
3470
|
-
await appendPlansHistory(cwd, `Session context generated. Branch: current. Pending tasks summarized.`);
|
|
3471
|
-
}
|
|
3472
|
-
catch {
|
|
3473
|
-
// non-critical
|
|
3474
|
-
}
|
|
3475
|
-
}
|
|
3476
|
-
}
|
|
3477
|
-
export async function migrateMemoryDirectory() {
|
|
3478
|
-
const oraModule = await import('ora');
|
|
3479
|
-
const ora = oraModule.default;
|
|
3480
|
-
const spinner = ora('Migrating memory directory structure...').start();
|
|
3481
|
-
try {
|
|
3482
|
-
const { createConfigManager } = await import('../core/config-manager.js');
|
|
3483
|
-
const fs = await import('fs');
|
|
3484
|
-
const fsPromises = fs.promises;
|
|
3485
|
-
const cwd = process.cwd();
|
|
3486
|
-
const oldDir = path.join(cwd, '.rulebook-memory');
|
|
3487
|
-
const rulebookDir = path.join(cwd, '.rulebook');
|
|
3488
|
-
const newDir = path.join(rulebookDir, 'memory');
|
|
3489
|
-
// Check if old directory exists
|
|
3490
|
-
if (!existsSync(oldDir)) {
|
|
3491
|
-
spinner.info('No old memory directory found (.rulebook-memory)');
|
|
3492
|
-
return;
|
|
3493
|
-
}
|
|
3494
|
-
// Create .rulebook directory if it doesn't exist
|
|
3495
|
-
if (!existsSync(rulebookDir)) {
|
|
3496
|
-
await fsPromises.mkdir(rulebookDir, { recursive: true });
|
|
3497
|
-
}
|
|
3498
|
-
// Create memory subdirectory
|
|
3499
|
-
await fsPromises.mkdir(newDir, { recursive: true });
|
|
3500
|
-
// Copy files from old to new
|
|
3501
|
-
const files = await fsPromises.readdir(oldDir);
|
|
3502
|
-
for (const file of files) {
|
|
3503
|
-
const oldPath = path.join(oldDir, file);
|
|
3504
|
-
const newPath = path.join(newDir, file);
|
|
3505
|
-
await fsPromises.copyFile(oldPath, newPath);
|
|
3506
|
-
}
|
|
3507
|
-
// Remove old directory
|
|
3508
|
-
await fsPromises.rm(oldDir, { recursive: true, force: true });
|
|
3509
|
-
// Update config to use new path
|
|
3510
|
-
const configManager = createConfigManager(cwd);
|
|
3511
|
-
const existingConfig = await configManager.loadConfig();
|
|
3512
|
-
if (existingConfig.memory) {
|
|
3513
|
-
existingConfig.memory.dbPath = '.rulebook/memory/memory.db';
|
|
3514
|
-
}
|
|
3515
|
-
await configManager.updateConfig(existingConfig);
|
|
3516
|
-
spinner.succeed('Memory directory migrated');
|
|
3517
|
-
console.log(`\n ${chalk.green('✓')} Migrated to: ${newDir}`);
|
|
3518
|
-
console.log(` ${chalk.gray('Old directory removed: .rulebook-memory')}\n`);
|
|
3519
|
-
}
|
|
3520
|
-
catch (error) {
|
|
3521
|
-
const oraModule = await import('ora');
|
|
3522
|
-
const ora = oraModule.default;
|
|
3523
|
-
const spinner2 = ora();
|
|
3524
|
-
spinner2.fail('Failed to migrate memory directory');
|
|
3525
|
-
console.error(chalk.red(String(error)));
|
|
3526
|
-
process.exit(1);
|
|
3527
|
-
}
|
|
3528
|
-
}
|
|
3529
|
-
// ─── Review Command ─────────────────────────────────────────────────────────
|
|
3530
|
-
/**
|
|
3531
|
-
* `rulebook review` — Run AI-powered code review on changes vs a base branch.
|
|
3532
|
-
*
|
|
3533
|
-
* Retrieves the git diff, builds a structured prompt, sends it to an AI tool,
|
|
3534
|
-
* parses the result, and outputs in the requested format.
|
|
3535
|
-
*/
|
|
3536
|
-
export async function reviewCommand(options) {
|
|
3537
|
-
const cwd = process.cwd();
|
|
3538
|
-
const baseBranch = options.baseBranch ?? 'main';
|
|
3539
|
-
const outputFormat = options.output ?? 'terminal';
|
|
3540
|
-
const tool = (options.tool ?? 'claude');
|
|
3541
|
-
const { getDiffContext, buildReviewPrompt, runAIReview, parseReviewOutput, formatReviewTerminal, postGitHubComment, readAgentsMd, hasFailingIssues, } = await import('../core/review-manager.js');
|
|
3542
|
-
// 1. Get diff
|
|
3543
|
-
const diff = await getDiffContext(cwd, baseBranch);
|
|
3544
|
-
if (!diff) {
|
|
3545
|
-
console.log(chalk.yellow(`No changes detected vs ${baseBranch}`));
|
|
3546
|
-
return;
|
|
3547
|
-
}
|
|
3548
|
-
// 2. Read AGENTS.md (optional context)
|
|
3549
|
-
const agentsMdContent = await readAgentsMd(cwd);
|
|
3550
|
-
// 3. Build prompt
|
|
3551
|
-
const projectName = path.basename(cwd);
|
|
3552
|
-
const prompt = buildReviewPrompt(diff, { agentsMdContent, projectName });
|
|
3553
|
-
// 4. Run AI review
|
|
3554
|
-
const spinner = ora('Running AI review...').start();
|
|
3555
|
-
const rawOutput = await runAIReview(prompt, tool);
|
|
3556
|
-
if (!rawOutput) {
|
|
3557
|
-
spinner.fail('AI review returned no output. Is the AI tool installed and configured?');
|
|
3558
|
-
process.exit(1);
|
|
3559
|
-
}
|
|
3560
|
-
spinner.succeed('AI review complete');
|
|
3561
|
-
// 5. Parse result
|
|
3562
|
-
const result = parseReviewOutput(rawOutput);
|
|
3563
|
-
// 6. Output
|
|
3564
|
-
switch (outputFormat) {
|
|
3565
|
-
case 'terminal':
|
|
3566
|
-
console.log(formatReviewTerminal(result));
|
|
3567
|
-
break;
|
|
3568
|
-
case 'json':
|
|
3569
|
-
console.log(JSON.stringify(result, null, 2));
|
|
3570
|
-
break;
|
|
3571
|
-
case 'github-comment':
|
|
3572
|
-
try {
|
|
3573
|
-
await postGitHubComment(result);
|
|
3574
|
-
console.log(chalk.green('Review posted as PR comment'));
|
|
3575
|
-
}
|
|
3576
|
-
catch (error) {
|
|
3577
|
-
console.error(chalk.red(`Failed to post comment: ${error}`));
|
|
3578
|
-
process.exit(1);
|
|
3579
|
-
}
|
|
3580
|
-
break;
|
|
3581
|
-
}
|
|
3582
|
-
// 7. Exit code
|
|
3583
|
-
if (options.failOn && hasFailingIssues(result.issues, options.failOn)) {
|
|
3584
|
-
console.log(chalk.red(`\nFailing: found issues at or above "${options.failOn}" severity`));
|
|
3585
|
-
process.exit(1);
|
|
3586
|
-
}
|
|
3587
|
-
}
|
|
3588
|
-
// ─── Ralph Import Issues Command ───
|
|
3589
|
-
export async function ralphImportIssuesCommand(options) {
|
|
3590
|
-
const oraModule = await import('ora');
|
|
3591
|
-
const ora = oraModule.default;
|
|
3592
|
-
try {
|
|
3593
|
-
const { checkGhCliAvailable, fetchGithubIssues, convertIssueToStory, mergeStoriesIntoExistingPrd, } = await import('../core/github-issues-importer.js');
|
|
3594
|
-
// 1. Check gh CLI availability
|
|
3595
|
-
const ghAvailable = await checkGhCliAvailable();
|
|
3596
|
-
if (!ghAvailable) {
|
|
3597
|
-
console.error(chalk.red('GitHub CLI (gh) is not installed. Install it from: https://cli.github.com'));
|
|
3598
|
-
return;
|
|
3599
|
-
}
|
|
3600
|
-
// 2. Fetch issues
|
|
3601
|
-
const spinner = ora('Fetching GitHub issues...').start();
|
|
3602
|
-
const issues = await fetchGithubIssues({
|
|
3603
|
-
label: options.label,
|
|
3604
|
-
milestone: options.milestone,
|
|
3605
|
-
limit: options.limit ?? 20,
|
|
3606
|
-
});
|
|
3607
|
-
if (issues.length === 0) {
|
|
3608
|
-
spinner.info('No open issues found matching the given filters.');
|
|
3609
|
-
return;
|
|
3610
|
-
}
|
|
3611
|
-
spinner.text = `Converting ${issues.length} issues to Ralph stories...`;
|
|
3612
|
-
// 3. Load existing PRD (may not exist yet)
|
|
3613
|
-
const cwd = process.cwd();
|
|
3614
|
-
let existingPrd = null;
|
|
3615
|
-
try {
|
|
3616
|
-
const { RalphManager } = await import('../core/ralph-manager.js');
|
|
3617
|
-
const { Logger } = await import('../core/logger.js');
|
|
3618
|
-
const logger = new Logger(cwd);
|
|
3619
|
-
const manager = new RalphManager(cwd, logger);
|
|
3620
|
-
existingPrd = await manager.loadPRD();
|
|
3621
|
-
}
|
|
3622
|
-
catch {
|
|
3623
|
-
// PRD not initialized — will create a new one
|
|
3624
|
-
}
|
|
3625
|
-
// 4. Convert issues to stories
|
|
3626
|
-
const newStories = issues.map((issue) => convertIssueToStory(issue));
|
|
3627
|
-
// 5. Merge into existing PRD
|
|
3628
|
-
const { prd: mergedPrd, result } = mergeStoriesIntoExistingPrd(existingPrd, newStories);
|
|
3629
|
-
// 6. Dry run — preview only
|
|
3630
|
-
if (options.dryRun) {
|
|
3631
|
-
spinner.stop();
|
|
3632
|
-
console.log(chalk.yellow(`Dry run — would import ${result.imported} stories, update ${result.updated}, skip ${result.skipped}`));
|
|
3633
|
-
console.log('');
|
|
3634
|
-
for (const story of mergedPrd.userStories) {
|
|
3635
|
-
const marker = story.passes ? chalk.green('[PASS]') : chalk.gray('[ ]');
|
|
3636
|
-
console.log(` ${marker} ${story.id}: ${story.title}`);
|
|
3637
|
-
}
|
|
3638
|
-
return;
|
|
3639
|
-
}
|
|
3640
|
-
// 7. Save PRD
|
|
3641
|
-
const prdPath = path.join(cwd, '.rulebook', 'ralph', 'prd.json');
|
|
3642
|
-
await ensureDir(path.join(cwd, '.rulebook', 'ralph'));
|
|
3643
|
-
await writeFile(prdPath, JSON.stringify(mergedPrd, null, 2));
|
|
3644
|
-
spinner.succeed(`Imported ${result.imported} new stories, updated ${result.updated} existing, ${result.skipped} skipped`);
|
|
3645
|
-
console.log(`\n PRD saved to: ${prdPath}`);
|
|
3646
|
-
console.log(` Total stories: ${mergedPrd.userStories.length}\n`);
|
|
3647
|
-
}
|
|
3648
|
-
catch (error) {
|
|
3649
|
-
console.error(chalk.red(`Failed to import GitHub issues: ${String(error)}`));
|
|
3650
|
-
process.exit(1);
|
|
3651
|
-
}
|
|
3652
|
-
}
|
|
3653
|
-
// ============================================
|
|
3654
|
-
// Workspace Commands (v4.2)
|
|
3655
|
-
// ============================================
|
|
3656
|
-
/** Resolve the workspace config path inside .rulebook/ */
|
|
3657
|
-
function getWorkspaceConfigPath(cwd) {
|
|
3658
|
-
return path.join(cwd, '.rulebook', 'workspace.json');
|
|
3659
|
-
}
|
|
3660
|
-
export async function workspaceInitCommand() {
|
|
3661
|
-
const cwd = process.cwd();
|
|
3662
|
-
const configPath = getWorkspaceConfigPath(cwd);
|
|
3663
|
-
if (existsSync(configPath)) {
|
|
3664
|
-
console.log(chalk.yellow('Workspace already initialized at .rulebook/workspace.json'));
|
|
3665
|
-
return;
|
|
3666
|
-
}
|
|
3667
|
-
const spinner = ora('Detecting workspace structure...').start();
|
|
3668
|
-
// Try auto-discovery first
|
|
3669
|
-
let config = WorkspaceManager.findWorkspaceConfig(cwd);
|
|
3670
|
-
if (config) {
|
|
3671
|
-
spinner.succeed(`Detected workspace: ${config.name} (${config.projects.length} projects)`);
|
|
3672
|
-
console.log('\n Projects found:');
|
|
3673
|
-
for (const p of config.projects) {
|
|
3674
|
-
console.log(` - ${chalk.cyan(p.name)} → ${p.path}`);
|
|
3675
|
-
}
|
|
3676
|
-
}
|
|
3677
|
-
else {
|
|
3678
|
-
spinner.info('No workspace structure detected. Creating empty workspace config.');
|
|
3679
|
-
config = {
|
|
3680
|
-
name: path.basename(cwd),
|
|
3681
|
-
version: '1.0.0',
|
|
3682
|
-
projects: [],
|
|
3683
|
-
};
|
|
3684
|
-
}
|
|
3685
|
-
// Ensure .rulebook/ directory exists
|
|
3686
|
-
const rulebookDir = path.join(cwd, '.rulebook');
|
|
3687
|
-
if (!existsSync(rulebookDir)) {
|
|
3688
|
-
const { mkdirSync } = await import('fs');
|
|
3689
|
-
mkdirSync(rulebookDir, { recursive: true });
|
|
3690
|
-
}
|
|
3691
|
-
// Write config
|
|
3692
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
3693
|
-
console.log(chalk.green(`\n Created: .rulebook/workspace.json`));
|
|
3694
|
-
// Check for legacy .mcp.json files
|
|
3695
|
-
const migration = await migrateLegacyMcpConfigs(cwd);
|
|
3696
|
-
if (migration.migratedFiles.length > 0) {
|
|
3697
|
-
console.log(chalk.yellow(`\n Migrated ${migration.migratedFiles.length} legacy .mcp.json files (backups at *.mcp.json.bak)`));
|
|
3698
|
-
}
|
|
3699
|
-
console.log(chalk.dim('\n Use `rulebook workspace add <path>` to add more projects'));
|
|
3700
|
-
console.log(chalk.dim(' Use `rulebook mcp init --workspace` to configure MCP for workspace'));
|
|
3701
|
-
}
|
|
3702
|
-
export async function workspaceAddCommand(projectPath) {
|
|
3703
|
-
const cwd = process.cwd();
|
|
3704
|
-
const configPath = getWorkspaceConfigPath(cwd);
|
|
3705
|
-
if (!existsSync(configPath)) {
|
|
3706
|
-
console.error(chalk.red('No workspace found. Run `rulebook workspace init` first.'));
|
|
3707
|
-
process.exit(1);
|
|
3708
|
-
}
|
|
3709
|
-
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
3710
|
-
const resolvedPath = path.resolve(cwd, projectPath);
|
|
3711
|
-
const name = path.basename(resolvedPath);
|
|
3712
|
-
// Check for duplicates
|
|
3713
|
-
if (config.projects.some((p) => p.name === name)) {
|
|
3714
|
-
console.error(chalk.red(`Project "${name}" already exists in workspace.`));
|
|
3715
|
-
process.exit(1);
|
|
3716
|
-
}
|
|
3717
|
-
// Use relative path if within workspace, absolute otherwise
|
|
3718
|
-
const isSubpath = resolvedPath.startsWith(cwd);
|
|
3719
|
-
const storedPath = isSubpath ? path.relative(cwd, resolvedPath) : resolvedPath;
|
|
3720
|
-
const project = {
|
|
3721
|
-
name,
|
|
3722
|
-
path: storedPath.startsWith('.') ? storedPath : `./${storedPath}`,
|
|
3723
|
-
};
|
|
3724
|
-
config.projects.push(project);
|
|
3725
|
-
if (!config.defaultProject) {
|
|
3726
|
-
config.defaultProject = name;
|
|
3727
|
-
}
|
|
3728
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
3729
|
-
console.log(chalk.green(`Added project "${name}" → ${project.path}`));
|
|
3730
|
-
}
|
|
3731
|
-
export async function workspaceRemoveCommand(projectName) {
|
|
3732
|
-
const cwd = process.cwd();
|
|
3733
|
-
const configPath = getWorkspaceConfigPath(cwd);
|
|
3734
|
-
if (!existsSync(configPath)) {
|
|
3735
|
-
console.error(chalk.red('No workspace found. Run `rulebook workspace init` first.'));
|
|
3736
|
-
process.exit(1);
|
|
3737
|
-
}
|
|
3738
|
-
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
3739
|
-
const idx = config.projects.findIndex((p) => p.name === projectName);
|
|
3740
|
-
if (idx === -1) {
|
|
3741
|
-
console.error(chalk.red(`Project "${projectName}" not found in workspace.`));
|
|
3742
|
-
process.exit(1);
|
|
3743
|
-
}
|
|
3744
|
-
config.projects.splice(idx, 1);
|
|
3745
|
-
if (config.defaultProject === projectName) {
|
|
3746
|
-
config.defaultProject = config.projects[0]?.name;
|
|
3747
|
-
}
|
|
3748
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
3749
|
-
console.log(chalk.green(`Removed project "${projectName}" from workspace.`));
|
|
3750
|
-
}
|
|
3751
|
-
export async function workspaceListCommand() {
|
|
3752
|
-
const cwd = process.cwd();
|
|
3753
|
-
const config = WorkspaceManager.findWorkspaceConfig(cwd);
|
|
3754
|
-
if (!config) {
|
|
3755
|
-
console.log(chalk.yellow('No workspace found. Run `rulebook workspace init` to create one.'));
|
|
3756
|
-
return;
|
|
3757
|
-
}
|
|
3758
|
-
console.log(chalk.bold(`\nWorkspace: ${config.name}`));
|
|
3759
|
-
console.log(chalk.dim(` Version: ${config.version}`));
|
|
3760
|
-
if (config.defaultProject) {
|
|
3761
|
-
console.log(chalk.dim(` Default: ${config.defaultProject}`));
|
|
3762
|
-
}
|
|
3763
|
-
console.log();
|
|
3764
|
-
for (const p of config.projects) {
|
|
3765
|
-
const isDefault = p.name === config.defaultProject;
|
|
3766
|
-
const marker = isDefault ? chalk.green(' (default)') : '';
|
|
3767
|
-
const disabled = p.enabled === false ? chalk.red(' [disabled]') : '';
|
|
3768
|
-
console.log(` ${chalk.cyan(p.name)}${marker}${disabled}`);
|
|
3769
|
-
console.log(` ${chalk.dim(p.path)}`);
|
|
3770
|
-
}
|
|
3771
|
-
console.log(chalk.dim(`\n ${config.projects.length} project(s) total`));
|
|
3772
|
-
}
|
|
3773
|
-
export async function workspaceStatusCommand() {
|
|
3774
|
-
const cwd = process.cwd();
|
|
3775
|
-
const config = WorkspaceManager.findWorkspaceConfig(cwd);
|
|
3776
|
-
if (!config) {
|
|
3777
|
-
console.log(chalk.yellow('No workspace found. Run `rulebook workspace init` to create one.'));
|
|
3778
|
-
return;
|
|
3779
|
-
}
|
|
3780
|
-
const manager = new WorkspaceManager(config, cwd);
|
|
3781
|
-
const spinner = ora('Checking workspace status...').start();
|
|
3782
|
-
try {
|
|
3783
|
-
const status = await manager.getStatus();
|
|
3784
|
-
spinner.stop();
|
|
3785
|
-
console.log(chalk.bold(`\nWorkspace: ${status.name}`));
|
|
3786
|
-
console.log(` Projects: ${status.totalProjects} | Active workers: ${status.activeWorkers}`);
|
|
3787
|
-
console.log();
|
|
3788
|
-
for (const p of status.projects) {
|
|
3789
|
-
const configBadge = p.hasRulebookConfig ? chalk.green('.rulebook') : chalk.dim('no config');
|
|
3790
|
-
const memBadge = p.memoryEnabled ? chalk.blue('memory') : '';
|
|
3791
|
-
const taskBadge = p.taskCount > 0 ? chalk.yellow(`${p.taskCount} tasks`) : '';
|
|
3792
|
-
const badges = [configBadge, memBadge, taskBadge].filter(Boolean).join(' ');
|
|
3793
|
-
console.log(` ${chalk.cyan(p.name)} ${badges}`);
|
|
3794
|
-
console.log(` ${chalk.dim(p.path)}`);
|
|
3795
|
-
}
|
|
3796
|
-
console.log();
|
|
3797
|
-
}
|
|
3798
|
-
catch (error) {
|
|
3799
|
-
spinner.fail(`Failed: ${String(error)}`);
|
|
3800
|
-
}
|
|
3801
|
-
finally {
|
|
3802
|
-
await manager.shutdownAll();
|
|
3803
|
-
}
|
|
3804
|
-
}
|
|
3805
|
-
// Context Intelligence Layer commands (v4.4)
|
|
3806
|
-
export async function decisionCreateCommand(title, options) {
|
|
3807
|
-
const { DecisionManager } = await import('../core/decision-manager.js');
|
|
3808
|
-
const mgr = new DecisionManager(process.cwd());
|
|
3809
|
-
const spinner = ora('Creating decision...').start();
|
|
3810
|
-
try {
|
|
3811
|
-
const d = await mgr.create(title, {
|
|
3812
|
-
context: options.context,
|
|
3813
|
-
relatedTasks: options.relatedTask ? [options.relatedTask] : undefined,
|
|
3814
|
-
});
|
|
3815
|
-
spinner.succeed(`Decision ADR-${String(d.id).padStart(3, '0')}: ${d.title} (${d.status})`);
|
|
3816
|
-
}
|
|
3817
|
-
catch (error) {
|
|
3818
|
-
spinner.fail(`Failed: ${String(error)}`);
|
|
3819
|
-
}
|
|
3820
|
-
}
|
|
3821
|
-
export async function decisionListCommand(options) {
|
|
3822
|
-
const { DecisionManager } = await import('../core/decision-manager.js');
|
|
3823
|
-
const mgr = new DecisionManager(process.cwd());
|
|
3824
|
-
const decisions = await mgr.list(options.status);
|
|
3825
|
-
if (decisions.length === 0) {
|
|
3826
|
-
console.log(chalk.dim('No decisions found.'));
|
|
3827
|
-
return;
|
|
3828
|
-
}
|
|
3829
|
-
for (const d of decisions) {
|
|
3830
|
-
const badge = d.status === 'accepted'
|
|
3831
|
-
? chalk.green(d.status)
|
|
3832
|
-
: d.status === 'superseded'
|
|
3833
|
-
? chalk.dim(d.status)
|
|
3834
|
-
: chalk.yellow(d.status);
|
|
3835
|
-
console.log(` ADR-${String(d.id).padStart(3, '0')} ${d.title} ${badge}`);
|
|
3836
|
-
}
|
|
3837
|
-
}
|
|
3838
|
-
export async function decisionShowCommand(id) {
|
|
3839
|
-
const { DecisionManager } = await import('../core/decision-manager.js');
|
|
3840
|
-
const mgr = new DecisionManager(process.cwd());
|
|
3841
|
-
const result = await mgr.show(parseInt(id));
|
|
3842
|
-
if (!result) {
|
|
3843
|
-
console.log(chalk.red(`Decision ${id} not found.`));
|
|
3844
|
-
return;
|
|
3845
|
-
}
|
|
3846
|
-
console.log(result.content);
|
|
3847
|
-
}
|
|
3848
|
-
export async function decisionSupersedeCommand(oldId, newId) {
|
|
3849
|
-
const { DecisionManager } = await import('../core/decision-manager.js');
|
|
3850
|
-
const mgr = new DecisionManager(process.cwd());
|
|
3851
|
-
const ok = await mgr.supersede(parseInt(oldId), parseInt(newId));
|
|
3852
|
-
if (ok) {
|
|
3853
|
-
console.log(chalk.green(`Decision ${oldId} superseded by ${newId}.`));
|
|
3854
|
-
}
|
|
3855
|
-
else {
|
|
3856
|
-
console.log(chalk.red(`Decision ${oldId} not found.`));
|
|
3857
|
-
}
|
|
3858
|
-
}
|
|
3859
|
-
export async function knowledgeAddCommand(type, title, options) {
|
|
3860
|
-
const { KnowledgeManager } = await import('../core/knowledge-manager.js');
|
|
3861
|
-
const mgr = new KnowledgeManager(process.cwd());
|
|
3862
|
-
const spinner = ora('Adding knowledge entry...').start();
|
|
3863
|
-
try {
|
|
3864
|
-
const entry = await mgr.add(type, title, {
|
|
3865
|
-
category: (options.category ?? 'code'),
|
|
3866
|
-
description: options.description ?? '',
|
|
3867
|
-
});
|
|
3868
|
-
spinner.succeed(`${entry.type}: ${entry.title} (${entry.category})`);
|
|
3869
|
-
}
|
|
3870
|
-
catch (error) {
|
|
3871
|
-
spinner.fail(`Failed: ${String(error)}`);
|
|
3872
|
-
}
|
|
3873
|
-
}
|
|
3874
|
-
export async function knowledgeListCommand(options) {
|
|
3875
|
-
const { KnowledgeManager } = await import('../core/knowledge-manager.js');
|
|
3876
|
-
const mgr = new KnowledgeManager(process.cwd());
|
|
3877
|
-
const entries = await mgr.list(options.type, options.category);
|
|
3878
|
-
if (entries.length === 0) {
|
|
3879
|
-
console.log(chalk.dim('No knowledge entries found.'));
|
|
3880
|
-
return;
|
|
3881
|
-
}
|
|
3882
|
-
for (const e of entries) {
|
|
3883
|
-
const badge = e.type === 'pattern' ? chalk.green('pattern') : chalk.red('anti-pattern');
|
|
3884
|
-
console.log(` ${badge} ${e.title} ${chalk.dim(e.category)}`);
|
|
3885
|
-
}
|
|
3886
|
-
}
|
|
3887
|
-
export async function knowledgeShowCommand(id) {
|
|
3888
|
-
const { KnowledgeManager } = await import('../core/knowledge-manager.js');
|
|
3889
|
-
const mgr = new KnowledgeManager(process.cwd());
|
|
3890
|
-
const result = await mgr.show(id);
|
|
3891
|
-
if (!result) {
|
|
3892
|
-
console.log(chalk.red(`Knowledge entry "${id}" not found.`));
|
|
3893
|
-
return;
|
|
3894
|
-
}
|
|
3895
|
-
console.log(result.content);
|
|
3896
|
-
}
|
|
3897
|
-
export async function knowledgeRemoveCommand(id) {
|
|
3898
|
-
const { KnowledgeManager } = await import('../core/knowledge-manager.js');
|
|
3899
|
-
const mgr = new KnowledgeManager(process.cwd());
|
|
3900
|
-
const ok = await mgr.remove(id);
|
|
3901
|
-
if (ok) {
|
|
3902
|
-
console.log(chalk.green(`Knowledge entry "${id}" removed.`));
|
|
3903
|
-
}
|
|
3904
|
-
else {
|
|
3905
|
-
console.log(chalk.red(`Knowledge entry "${id}" not found.`));
|
|
3906
|
-
}
|
|
3907
|
-
}
|
|
3908
|
-
export async function learnCaptureCommand(options) {
|
|
3909
|
-
const { LearnManager } = await import('../core/learn-manager.js');
|
|
3910
|
-
const mgr = new LearnManager(process.cwd());
|
|
3911
|
-
// If no title/content provided, this would be interactive (placeholder for prompts)
|
|
3912
|
-
const title = options.title ?? 'Untitled learning';
|
|
3913
|
-
const content = options.content ?? '';
|
|
3914
|
-
const tags = options.tags ? options.tags.split(',').map((t) => t.trim()) : [];
|
|
3915
|
-
const spinner = ora('Capturing learning...').start();
|
|
3916
|
-
try {
|
|
3917
|
-
const learning = await mgr.capture(title, content, {
|
|
3918
|
-
source: 'manual',
|
|
3919
|
-
relatedTask: options.relatedTask,
|
|
3920
|
-
tags,
|
|
3921
|
-
});
|
|
3922
|
-
spinner.succeed(`Learning captured: ${learning.title}`);
|
|
3923
|
-
}
|
|
3924
|
-
catch (error) {
|
|
3925
|
-
spinner.fail(`Failed: ${String(error)}`);
|
|
3926
|
-
}
|
|
3927
|
-
}
|
|
3928
|
-
export async function learnFromRalphCommand() {
|
|
3929
|
-
const { LearnManager } = await import('../core/learn-manager.js');
|
|
3930
|
-
const mgr = new LearnManager(process.cwd());
|
|
3931
|
-
const spinner = ora('Extracting learnings from Ralph history...').start();
|
|
3932
|
-
try {
|
|
3933
|
-
const learnings = await mgr.fromRalph();
|
|
3934
|
-
if (learnings.length === 0) {
|
|
3935
|
-
spinner.info('No new learnings found in Ralph history.');
|
|
3936
|
-
}
|
|
3937
|
-
else {
|
|
3938
|
-
spinner.succeed(`Extracted ${learnings.length} learning(s) from Ralph history.`);
|
|
3939
|
-
}
|
|
3940
|
-
}
|
|
3941
|
-
catch (error) {
|
|
3942
|
-
spinner.fail(`Failed: ${String(error)}`);
|
|
3943
|
-
}
|
|
3944
|
-
}
|
|
3945
|
-
export async function learnListCommand(options) {
|
|
3946
|
-
const { LearnManager } = await import('../core/learn-manager.js');
|
|
3947
|
-
const mgr = new LearnManager(process.cwd());
|
|
3948
|
-
const limit = options.limit ? parseInt(options.limit) : undefined;
|
|
3949
|
-
const learnings = await mgr.list(limit);
|
|
3950
|
-
if (learnings.length === 0) {
|
|
3951
|
-
console.log(chalk.dim('No learnings found.'));
|
|
3952
|
-
return;
|
|
3953
|
-
}
|
|
3954
|
-
for (const l of learnings) {
|
|
3955
|
-
const badge = l.source === 'ralph'
|
|
3956
|
-
? chalk.blue('ralph')
|
|
3957
|
-
: l.source === 'task-archive'
|
|
3958
|
-
? chalk.yellow('archive')
|
|
3959
|
-
: chalk.dim('manual');
|
|
3960
|
-
const promoted = l.promotedTo ? chalk.green(` → ${l.promotedTo.type}`) : '';
|
|
3961
|
-
console.log(` ${badge} ${l.title}${promoted}`);
|
|
3962
|
-
}
|
|
3963
|
-
}
|
|
3964
|
-
export async function learnPromoteCommand(id, target, options) {
|
|
3965
|
-
const { LearnManager } = await import('../core/learn-manager.js');
|
|
3966
|
-
const mgr = new LearnManager(process.cwd());
|
|
3967
|
-
if (target !== 'knowledge' && target !== 'decision') {
|
|
3968
|
-
console.log(chalk.red('Target must be "knowledge" or "decision".'));
|
|
3969
|
-
return;
|
|
3970
|
-
}
|
|
3971
|
-
const spinner = ora(`Promoting learning to ${target}...`).start();
|
|
3972
|
-
try {
|
|
3973
|
-
const result = await mgr.promote(id, target, { title: options.title });
|
|
3974
|
-
if (!result) {
|
|
3975
|
-
spinner.fail(`Learning "${id}" not found.`);
|
|
3976
|
-
return;
|
|
3977
|
-
}
|
|
3978
|
-
spinner.succeed(`Learning promoted to ${result.type} (id: ${result.id}).`);
|
|
3979
|
-
}
|
|
3980
|
-
catch (error) {
|
|
3981
|
-
spinner.fail(`Failed: ${String(error)}`);
|
|
3982
|
-
}
|
|
3983
|
-
}
|
|
3984
|
-
//# sourceMappingURL=commands.js.map
|