@hivehub/rulebook 3.3.1 → 4.0.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/.claude/commands/continue.md +33 -0
- package/.claude-plugin/marketplace.json +28 -29
- package/.claude-plugin/plugin.json +8 -8
- package/README.md +32 -144
- package/dist/agents/ralph-parser.d.ts +44 -5
- package/dist/agents/ralph-parser.d.ts.map +1 -1
- package/dist/agents/ralph-parser.js +218 -26
- package/dist/agents/ralph-parser.js.map +1 -1
- package/dist/cli/commands.d.ts +65 -0
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +841 -175
- package/dist/cli/commands.js.map +1 -1
- package/dist/core/agent-manager.d.ts +12 -32
- package/dist/core/agent-manager.d.ts.map +1 -1
- package/dist/core/agent-manager.js +150 -220
- package/dist/core/agent-manager.js.map +1 -1
- package/dist/core/claude-mcp.d.ts +17 -0
- package/dist/core/claude-mcp.d.ts.map +1 -1
- package/dist/core/claude-mcp.js +90 -6
- package/dist/core/claude-mcp.js.map +1 -1
- package/dist/core/cli-bridge.js +1 -1
- package/dist/core/cli-bridge.js.map +1 -1
- package/dist/core/config-manager.d.ts.map +1 -1
- package/dist/core/config-manager.js +40 -0
- package/dist/core/config-manager.js.map +1 -1
- package/dist/core/cursor-mdc-generator.d.ts +30 -0
- package/dist/core/cursor-mdc-generator.d.ts.map +1 -0
- package/dist/core/cursor-mdc-generator.js +98 -0
- package/dist/core/cursor-mdc-generator.js.map +1 -0
- package/dist/core/detector.d.ts +25 -1
- package/dist/core/detector.d.ts.map +1 -1
- package/dist/core/detector.js +321 -1
- package/dist/core/detector.js.map +1 -1
- package/dist/core/generator.d.ts +10 -0
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +182 -9
- package/dist/core/generator.js.map +1 -1
- package/dist/core/github-issues-importer.d.ts +82 -0
- package/dist/core/github-issues-importer.d.ts.map +1 -0
- package/dist/core/github-issues-importer.js +161 -0
- package/dist/core/github-issues-importer.js.map +1 -0
- package/dist/core/health-scorer.d.ts +39 -0
- package/dist/core/health-scorer.d.ts.map +1 -1
- package/dist/core/health-scorer.js +256 -13
- package/dist/core/health-scorer.js.map +1 -1
- package/dist/core/iteration-tracker.d.ts +28 -0
- package/dist/core/iteration-tracker.d.ts.map +1 -1
- package/dist/core/iteration-tracker.js +86 -0
- package/dist/core/iteration-tracker.js.map +1 -1
- package/dist/core/logger.js +1 -1
- package/dist/core/logger.js.map +1 -1
- package/dist/core/migrator.js +1 -1
- package/dist/core/migrator.js.map +1 -1
- package/dist/core/modern-console.d.ts +1 -2
- package/dist/core/modern-console.d.ts.map +1 -1
- package/dist/core/modern-console.js +6 -18
- package/dist/core/modern-console.js.map +1 -1
- package/dist/core/multi-tool-generator.d.ts +59 -0
- package/dist/core/multi-tool-generator.d.ts.map +1 -0
- package/dist/core/multi-tool-generator.js +157 -0
- package/dist/core/multi-tool-generator.js.map +1 -0
- package/dist/core/override-manager.d.ts +23 -0
- package/dist/core/override-manager.d.ts.map +1 -0
- package/dist/core/override-manager.js +82 -0
- package/dist/core/override-manager.js.map +1 -0
- package/dist/core/plans-manager.d.ts +46 -0
- package/dist/core/plans-manager.d.ts.map +1 -0
- package/dist/core/plans-manager.js +158 -0
- package/dist/core/plans-manager.js.map +1 -0
- package/dist/core/prd-generator.d.ts +12 -0
- package/dist/core/prd-generator.d.ts.map +1 -1
- package/dist/core/prd-generator.js +91 -2
- package/dist/core/prd-generator.js.map +1 -1
- package/dist/core/ralph-manager.d.ts +81 -1
- package/dist/core/ralph-manager.d.ts.map +1 -1
- package/dist/core/ralph-manager.js +214 -4
- package/dist/core/ralph-manager.js.map +1 -1
- package/dist/core/ralph-parallel.d.ts +55 -0
- package/dist/core/ralph-parallel.d.ts.map +1 -0
- package/dist/core/ralph-parallel.js +201 -0
- package/dist/core/ralph-parallel.js.map +1 -0
- package/dist/core/ralph-plan-checkpoint.d.ts +58 -0
- package/dist/core/ralph-plan-checkpoint.d.ts.map +1 -0
- package/dist/core/ralph-plan-checkpoint.js +154 -0
- package/dist/core/ralph-plan-checkpoint.js.map +1 -0
- package/dist/core/ralph-scripts.d.ts +12 -0
- package/dist/core/ralph-scripts.d.ts.map +1 -0
- package/dist/core/ralph-scripts.js +49 -0
- package/dist/core/ralph-scripts.js.map +1 -0
- package/dist/core/review-manager.d.ts +74 -0
- package/dist/core/review-manager.d.ts.map +1 -0
- package/dist/core/review-manager.js +371 -0
- package/dist/core/review-manager.js.map +1 -0
- package/dist/core/task-manager.d.ts +1 -1
- package/dist/core/task-manager.js +1 -1
- package/dist/core/workflow-generator.js +1 -1
- package/dist/core/workflow-generator.js.map +1 -1
- package/dist/index.js +96 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp/rulebook-server.d.ts.map +1 -1
- package/dist/mcp/rulebook-server.js +300 -164
- package/dist/mcp/rulebook-server.js.map +1 -1
- package/dist/memory/memory-store.d.ts.map +1 -1
- package/dist/memory/memory-store.js +4 -0
- package/dist/memory/memory-store.js.map +1 -1
- package/dist/types.d.ts +55 -34
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/templates/agents/implementer.md +35 -0
- package/templates/agents/researcher.md +34 -0
- package/templates/agents/team-lead.md +34 -0
- package/templates/agents/tester.md +42 -0
- package/templates/ci/rulebook-review.yml +26 -0
- package/templates/core/AGENTS_LEAN.md +25 -0
- package/templates/core/AGENTS_OVERRIDE.md +16 -0
- package/templates/core/MULTI_AGENT.md +74 -0
- package/templates/core/PLANS.md +28 -0
- package/templates/core/RALPH.md +45 -4
- package/templates/ides/CONTINUE_RULES.md +16 -0
- package/templates/ides/COPILOT_INSTRUCTIONS.md +23 -0
- package/templates/ides/GEMINI_RULES.md +17 -0
- package/templates/ides/WINDSURF_RULES.md +14 -0
- package/templates/ides/cursor-mdc/go.mdc +24 -0
- package/templates/ides/cursor-mdc/python.mdc +24 -0
- package/templates/ides/cursor-mdc/quality.mdc +25 -0
- package/templates/ides/cursor-mdc/ralph.mdc +39 -0
- package/templates/ides/cursor-mdc/rulebook.mdc +38 -0
- package/templates/ides/cursor-mdc/rust.mdc +24 -0
- package/templates/ides/cursor-mdc/typescript.mdc +25 -0
- package/templates/modules/sequential-thinking.md +42 -0
- package/templates/ralph/ralph-history.bat +4 -0
- package/templates/ralph/ralph-history.sh +5 -0
- package/templates/ralph/ralph-init.bat +5 -0
- package/templates/ralph/ralph-init.sh +5 -0
- package/templates/ralph/ralph-pause.bat +5 -0
- package/templates/ralph/ralph-pause.sh +5 -0
- package/templates/ralph/ralph-run.bat +5 -0
- package/templates/ralph/ralph-run.sh +5 -0
- package/templates/ralph/ralph-status.bat +4 -0
- package/templates/ralph/ralph-status.sh +5 -0
- package/templates/services/DATADOG.md +26 -0
- package/templates/services/DOCKER.md +124 -0
- package/templates/services/DOCKER_COMPOSE.md +168 -0
- package/templates/services/HELM.md +194 -0
- package/templates/services/KUBERNETES.md +208 -0
- package/templates/services/OPENTELEMETRY.md +25 -0
- package/templates/services/PINO.md +24 -0
- package/templates/services/PROMETHEUS.md +33 -0
- package/templates/services/SENTRY.md +23 -0
- package/templates/services/WINSTON.md +30 -0
- package/dist/core/openspec-manager.d.ts +0 -133
- package/dist/core/openspec-manager.d.ts.map +0 -1
- package/dist/core/openspec-manager.js +0 -596
- package/dist/core/openspec-manager.js.map +0 -1
- package/dist/core/openspec-migrator.d.ts +0 -27
- package/dist/core/openspec-migrator.d.ts.map +0 -1
- package/dist/core/openspec-migrator.js +0 -262
- package/dist/core/openspec-migrator.js.map +0 -1
- package/dist/core/test-task-manager.d.ts +0 -49
- package/dist/core/test-task-manager.d.ts.map +0 -1
- package/dist/core/test-task-manager.js +0 -121
- package/dist/core/test-task-manager.js.map +0 -1
package/dist/cli/commands.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { detectProject } from '../core/detector.js';
|
|
4
|
-
import { promptProjectConfig, promptSimplifiedConfig, promptMergeStrategy } from './prompts.js';
|
|
5
4
|
import { generateFullAgents } from '../core/generator.js';
|
|
6
5
|
import { mergeFullAgents } from '../core/merger.js';
|
|
7
6
|
import { generateWorkflows, generateIDEFiles, generateAICLIFiles, } from '../core/workflow-generator.js';
|
|
8
|
-
import { writeFile, createBackup } from '../utils/file-system.js';
|
|
7
|
+
import { writeFile, createBackup, ensureDir } from '../utils/file-system.js';
|
|
9
8
|
import { existsSync } from 'fs';
|
|
10
9
|
import { parseRulesIgnore } from '../utils/rulesignore.js';
|
|
11
10
|
import { installGitHooks } from '../utils/git-hooks.js';
|
|
@@ -67,6 +66,24 @@ export async function initCommand(options) {
|
|
|
67
66
|
console.log(` - ${module.module} (${module.source})`);
|
|
68
67
|
}
|
|
69
68
|
}
|
|
69
|
+
// Show monorepo detection
|
|
70
|
+
if (detection.monorepo?.detected) {
|
|
71
|
+
const mono = detection.monorepo;
|
|
72
|
+
console.log(chalk.green(`\n✓ Monorepo detected: ${chalk.bold(mono.tool ?? 'manual')}`));
|
|
73
|
+
if (mono.packages.length > 0) {
|
|
74
|
+
console.log(` Packages (${mono.packages.length}): ${mono.packages.slice(0, 5).join(', ')}${mono.packages.length > 5 ? ` +${mono.packages.length - 5} more` : ''}`);
|
|
75
|
+
}
|
|
76
|
+
if (options.package) {
|
|
77
|
+
console.log(chalk.cyan(` → Initializing package: ${options.package}`));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Recommend sequential-thinking MCP if not detected
|
|
81
|
+
const seqThinking = detection.modules.find((m) => m.module === 'sequential_thinking');
|
|
82
|
+
if (seqThinking && !seqThinking.detected) {
|
|
83
|
+
console.log(chalk.yellow('\n💡 Tip: Install sequential-thinking MCP for structured problem solving:\n' +
|
|
84
|
+
' npx @modelcontextprotocol/create-server sequential-thinking\n' +
|
|
85
|
+
' or add to .mcp.json: { "mcpServers": { "sequential-thinking": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-sequential-thinking"] } } }'));
|
|
86
|
+
}
|
|
70
87
|
const detectedFrameworks = detection.frameworks.filter((f) => f.detected);
|
|
71
88
|
if (detectedFrameworks.length > 0) {
|
|
72
89
|
console.log(chalk.green('\n✓ Detected frameworks:'));
|
|
@@ -83,45 +100,27 @@ export async function initCommand(options) {
|
|
|
83
100
|
console.log(` - ${block.name}`);
|
|
84
101
|
}
|
|
85
102
|
}
|
|
86
|
-
// Get project configuration
|
|
87
|
-
let config;
|
|
103
|
+
// Get project configuration — auto-setup from detection, no prompts
|
|
88
104
|
const cliMinimal = Boolean(options.minimal);
|
|
89
105
|
const cliLight = Boolean(options.light);
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
};
|
|
109
|
-
console.log(chalk.blue('\nUsing detected defaults...'));
|
|
110
|
-
}
|
|
111
|
-
else if (cliQuick) {
|
|
112
|
-
// Quick mode - minimal prompts (language, MCP, hooks only)
|
|
113
|
-
config = await promptSimplifiedConfig(detection);
|
|
114
|
-
config.lightMode = cliLight;
|
|
115
|
-
config.minimal = cliMinimal;
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
// Full interactive mode
|
|
119
|
-
console.log('');
|
|
120
|
-
config = await promptProjectConfig(detection, {
|
|
121
|
-
defaultMode: cliMinimal ? 'minimal' : 'full',
|
|
122
|
-
});
|
|
123
|
-
config.lightMode = cliLight;
|
|
124
|
-
}
|
|
106
|
+
const cliLean = Boolean(options.lean);
|
|
107
|
+
const config = {
|
|
108
|
+
languages: detection.languages.map((l) => l.language),
|
|
109
|
+
modules: cliMinimal ? [] : detection.modules.filter((m) => m.detected).map((m) => m.module),
|
|
110
|
+
frameworks: detection.frameworks.filter((f) => f.detected).map((f) => f.framework),
|
|
111
|
+
ides: cliMinimal ? [] : ['cursor'],
|
|
112
|
+
projectType: 'application',
|
|
113
|
+
coverageThreshold: 95,
|
|
114
|
+
strictDocs: true,
|
|
115
|
+
generateWorkflows: true,
|
|
116
|
+
includeGitWorkflow: true,
|
|
117
|
+
gitPushMode: 'manual',
|
|
118
|
+
installGitHooks: false,
|
|
119
|
+
minimal: cliMinimal,
|
|
120
|
+
lightMode: cliLight,
|
|
121
|
+
modular: true,
|
|
122
|
+
};
|
|
123
|
+
console.log(chalk.blue('\nAuto-configuring from detection results...'));
|
|
125
124
|
const minimalMode = config.minimal ?? cliMinimal;
|
|
126
125
|
config.minimal = minimalMode;
|
|
127
126
|
config.modules = minimalMode ? [] : config.modules || [];
|
|
@@ -130,6 +129,9 @@ export async function initCommand(options) {
|
|
|
130
129
|
config.includeGitWorkflow = config.includeGitWorkflow ?? true;
|
|
131
130
|
config.generateWorkflows = config.generateWorkflows ?? true;
|
|
132
131
|
config.modular = config.modular ?? true; // Enable modular by default
|
|
132
|
+
if (cliLean) {
|
|
133
|
+
config.agentsMode = 'lean';
|
|
134
|
+
}
|
|
133
135
|
let minimalArtifacts = [];
|
|
134
136
|
if (minimalMode) {
|
|
135
137
|
minimalArtifacts = await scaffoldMinimalProject(cwd, {
|
|
@@ -183,6 +185,8 @@ export async function initCommand(options) {
|
|
|
183
185
|
const dirMigrationSpinner = ora('Migrating directory structure...').start();
|
|
184
186
|
await configManager.migrateDirectoryStructure(cwd);
|
|
185
187
|
dirMigrationSpinner.succeed('Directory structure migrated');
|
|
188
|
+
// Ensure .rulebook/memory/ directory exists for per-project memory persistence
|
|
189
|
+
await ensureDir(path.join(cwd, '.rulebook', 'memory'));
|
|
186
190
|
// Ensure .gitignore has .rulebook entries (keep specs/ and tasks/ tracked)
|
|
187
191
|
await configManager.ensureGitignore();
|
|
188
192
|
// Auto-detect and enable skills based on project detection (v2.0)
|
|
@@ -217,10 +221,19 @@ export async function initCommand(options) {
|
|
|
217
221
|
services: config.services,
|
|
218
222
|
modular: config.modular ?? true,
|
|
219
223
|
rulebookDir: config.rulebookDir || '.rulebook',
|
|
224
|
+
...(config.agentsMode ? { agentsMode: config.agentsMode } : {}),
|
|
220
225
|
skills: enabledSkills.length > 0 ? { enabled: enabledSkills } : undefined,
|
|
221
226
|
ralph: existingConfig.ralph,
|
|
222
227
|
memory: existingConfig.memory,
|
|
223
228
|
});
|
|
229
|
+
// --package: generate only the specified package's AGENTS.md and exit
|
|
230
|
+
if (options.package) {
|
|
231
|
+
const packageRoot = path.join(cwd, options.package);
|
|
232
|
+
const { generatePackageAgentsMd } = await import('../core/generator.js');
|
|
233
|
+
await generatePackageAgentsMd(packageRoot, config, cwd);
|
|
234
|
+
console.log(chalk.green(`\n✅ AGENTS.md generated for package: ${options.package}`));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
224
237
|
// Generate or merge AGENTS.md
|
|
225
238
|
const agentsPath = path.join(cwd, 'AGENTS.md');
|
|
226
239
|
let finalContent;
|
|
@@ -236,22 +249,11 @@ export async function initCommand(options) {
|
|
|
236
249
|
}
|
|
237
250
|
}
|
|
238
251
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const backupPath = await createBackup(agentsPath);
|
|
245
|
-
mergeSpinner.succeed(`Backup created: ${path.basename(backupPath)}`);
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
const backupSpinner = ora('Creating backup...').start();
|
|
249
|
-
const backupPath = await createBackup(agentsPath);
|
|
250
|
-
backupSpinner.succeed(`Backup created: ${path.basename(backupPath)}`);
|
|
251
|
-
const genSpinner = ora('Generating new AGENTS.md...').start();
|
|
252
|
-
finalContent = await generateFullAgents(config, cwd);
|
|
253
|
-
genSpinner.succeed('AGENTS.md generated');
|
|
254
|
-
}
|
|
252
|
+
// Always merge — preserve existing customizations
|
|
253
|
+
const mergeSpinner = ora('Merging with existing AGENTS.md...').start();
|
|
254
|
+
finalContent = await mergeFullAgents(detection.existingAgents, config, cwd);
|
|
255
|
+
const backupPath = await createBackup(agentsPath);
|
|
256
|
+
mergeSpinner.succeed(`Backup created: ${path.basename(backupPath)}`);
|
|
255
257
|
}
|
|
256
258
|
else {
|
|
257
259
|
const genSpinner = ora('Generating AGENTS.md...').start();
|
|
@@ -261,6 +263,28 @@ export async function initCommand(options) {
|
|
|
261
263
|
// Write AGENTS.md
|
|
262
264
|
await writeFile(agentsPath, finalContent);
|
|
263
265
|
console.log(chalk.green(`\n✅ AGENTS.md written to ${agentsPath}`));
|
|
266
|
+
// Show Cursor MDC feedback
|
|
267
|
+
if (detection.cursor?.detected) {
|
|
268
|
+
if (detection.cursor.hasMdcRules) {
|
|
269
|
+
console.log(chalk.gray(' • Cursor .mdc rules updated in .cursor/rules/'));
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
console.log(chalk.gray(' • Cursor .mdc rules generated in .cursor/rules/'));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Show multi-tool config feedback
|
|
276
|
+
if (detection.geminiCli?.detected) {
|
|
277
|
+
console.log(chalk.gray(' • Gemini CLI config generated: GEMINI.md'));
|
|
278
|
+
}
|
|
279
|
+
if (detection.continueDev?.detected) {
|
|
280
|
+
console.log(chalk.gray(' • Continue.dev rules generated in .continue/rules/'));
|
|
281
|
+
}
|
|
282
|
+
if (detection.windsurf?.detected) {
|
|
283
|
+
console.log(chalk.gray(' • Windsurf rules generated: .windsurfrules'));
|
|
284
|
+
}
|
|
285
|
+
if (detection.githubCopilot?.detected) {
|
|
286
|
+
console.log(chalk.gray(' • GitHub Copilot instructions generated in .github/'));
|
|
287
|
+
}
|
|
264
288
|
// Generate workflows if requested
|
|
265
289
|
if (config.generateWorkflows) {
|
|
266
290
|
const workflowSpinner = ora('Generating GitHub Actions workflows...').start();
|
|
@@ -327,6 +351,12 @@ export async function initCommand(options) {
|
|
|
327
351
|
if (result.skillsInstalled.length > 0) {
|
|
328
352
|
console.log(chalk.gray(` • ${result.skillsInstalled.length} skills installed to .claude/commands/`));
|
|
329
353
|
}
|
|
354
|
+
if (result.agentTeamsEnabled) {
|
|
355
|
+
console.log(chalk.gray(' • Multi-agent teams enabled in .claude/settings.json'));
|
|
356
|
+
}
|
|
357
|
+
if (result.agentDefinitionsInstalled.length > 0) {
|
|
358
|
+
console.log(chalk.gray(` • ${result.agentDefinitionsInstalled.length} agent definitions installed to .claude/agents/`));
|
|
359
|
+
}
|
|
330
360
|
}
|
|
331
361
|
else {
|
|
332
362
|
claudeSpinner.info('Claude Code not detected (skipped)');
|
|
@@ -336,6 +366,49 @@ export async function initCommand(options) {
|
|
|
336
366
|
claudeSpinner.info('Claude Code integration skipped');
|
|
337
367
|
}
|
|
338
368
|
}
|
|
369
|
+
// Install Ralph shell scripts
|
|
370
|
+
try {
|
|
371
|
+
const { installRalphScripts } = await import('../core/ralph-scripts.js');
|
|
372
|
+
const scripts = await installRalphScripts(cwd);
|
|
373
|
+
if (scripts.length > 0) {
|
|
374
|
+
console.log(chalk.gray(` • ${scripts.length} Ralph scripts installed to .rulebook/scripts/`));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// Skip if Ralph scripts installation fails
|
|
379
|
+
}
|
|
380
|
+
// Create PLANS.md for session continuity
|
|
381
|
+
try {
|
|
382
|
+
const { initPlans } = await import('../core/plans-manager.js');
|
|
383
|
+
const created = await initPlans(cwd);
|
|
384
|
+
if (created) {
|
|
385
|
+
console.log(chalk.gray(' • PLANS.md created for session continuity'));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// Non-blocking
|
|
390
|
+
}
|
|
391
|
+
// Create AGENTS.override.md (never overwrites existing)
|
|
392
|
+
try {
|
|
393
|
+
const { initOverride } = await import('../core/override-manager.js');
|
|
394
|
+
const created = await initOverride(cwd);
|
|
395
|
+
if (created) {
|
|
396
|
+
console.log(chalk.gray(' • AGENTS.override.md created (add project-specific rules here)'));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Non-blocking
|
|
401
|
+
}
|
|
402
|
+
// --add-sequential-thinking: inject into mcp.json if not already present
|
|
403
|
+
if (options.addSequentialThinking) {
|
|
404
|
+
try {
|
|
405
|
+
await addSequentialThinkingMcp(cwd);
|
|
406
|
+
console.log(chalk.gray(' • sequential-thinking MCP added to mcp.json'));
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
console.log(chalk.yellow(' ⚠ Could not add sequential-thinking MCP'));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
339
412
|
if (minimalMode && minimalArtifacts.length > 0) {
|
|
340
413
|
console.log(chalk.green('\n✅ Essentials created:'));
|
|
341
414
|
for (const artifact of minimalArtifacts) {
|
|
@@ -652,13 +725,12 @@ export async function healthCommand() {
|
|
|
652
725
|
try {
|
|
653
726
|
const cwd = process.cwd();
|
|
654
727
|
console.log(chalk.bold.blue('\n🏥 Project Health Check\n'));
|
|
655
|
-
const { calculateHealthScore
|
|
728
|
+
const { calculateHealthScore } = await import('../core/health-scorer.js');
|
|
656
729
|
const spinner = ora('Analyzing project health...').start();
|
|
657
730
|
const health = await calculateHealthScore(cwd);
|
|
658
731
|
spinner.succeed('Health analysis complete');
|
|
659
732
|
console.log('');
|
|
660
|
-
|
|
661
|
-
console.log(chalk.bold(`Overall Health Score: ${health.overall}/100 (${grade})`));
|
|
733
|
+
console.log(chalk.bold(`Overall Health Score: ${health.overall}/100 (${health.grade})`));
|
|
662
734
|
console.log('');
|
|
663
735
|
console.log(chalk.bold('Category Scores:\n'));
|
|
664
736
|
console.log(` 📝 Documentation: ${health.categories.documentation}/100`);
|
|
@@ -667,6 +739,9 @@ export async function healthCommand() {
|
|
|
667
739
|
console.log(` 🔒 Security: ${health.categories.security}/100`);
|
|
668
740
|
console.log(` 🔄 CI/CD: ${health.categories.cicd}/100`);
|
|
669
741
|
console.log(` 📦 Dependencies: ${health.categories.dependencies}/100`);
|
|
742
|
+
console.log(` 🤖 AGENTS.md: ${health.categories.agentsMd}/100`);
|
|
743
|
+
console.log(` 🔁 Ralph: ${health.categories.ralph}/100`);
|
|
744
|
+
console.log(` 🧠 Memory: ${health.categories.memory}/100`);
|
|
670
745
|
console.log('');
|
|
671
746
|
if (health.recommendations.length > 0) {
|
|
672
747
|
console.log(chalk.bold.yellow('Recommendations:\n'));
|
|
@@ -839,8 +914,7 @@ export async function taskCreateCommand(taskId) {
|
|
|
839
914
|
console.log(chalk.green(`✅ Task ${taskId} created successfully`));
|
|
840
915
|
console.log(chalk.gray(`Location: ${rulebookDir}/tasks/${taskId}/`));
|
|
841
916
|
console.log(chalk.yellow('\n⚠️ Remember to:'));
|
|
842
|
-
console.log(chalk.gray(' 1.
|
|
843
|
-
console.log(chalk.gray(' 2. Fill in proposal.md (minimum 20 characters in "Why" section)'));
|
|
917
|
+
console.log(chalk.gray(' 1. Fill in proposal.md (minimum 20 characters in "Why" section)'));
|
|
844
918
|
console.log(chalk.gray(' 3. Add tasks to tasks.md'));
|
|
845
919
|
console.log(chalk.gray(' 4. Create spec deltas in specs/*/spec.md'));
|
|
846
920
|
console.log(chalk.gray(' 5. Validate with: rulebook task validate ' + taskId));
|
|
@@ -1097,8 +1171,8 @@ export async function tasksCommand(options) {
|
|
|
1097
1171
|
console.log(chalk.gray(' - rulebook task validate <task-id>'));
|
|
1098
1172
|
console.log(chalk.gray(' - rulebook task archive <task-id>'));
|
|
1099
1173
|
if (options.tree || options.current || options.status) {
|
|
1100
|
-
console.log(chalk.red('\n❌ Legacy
|
|
1101
|
-
console.log(chalk.yellow('Please
|
|
1174
|
+
console.log(chalk.red('\n❌ Legacy commands are no longer supported.'));
|
|
1175
|
+
console.log(chalk.yellow('Please use the Rulebook task system.'));
|
|
1102
1176
|
process.exit(1);
|
|
1103
1177
|
}
|
|
1104
1178
|
// Fallback to list tasks
|
|
@@ -1189,6 +1263,7 @@ export async function updateCommand(options) {
|
|
|
1189
1263
|
}
|
|
1190
1264
|
const minimalMode = options.minimal ?? existingMode === 'minimal';
|
|
1191
1265
|
const lightMode = options.light !== undefined ? options.light : (existingLightMode ?? false);
|
|
1266
|
+
const leanMode = options.lean ?? existingConfig?.agentsMode === 'lean';
|
|
1192
1267
|
// Build config from detected project
|
|
1193
1268
|
const config = {
|
|
1194
1269
|
languages: detection.languages.map((l) => l.language),
|
|
@@ -1204,6 +1279,7 @@ export async function updateCommand(options) {
|
|
|
1204
1279
|
installGitHooks: installHooksOnUpdate,
|
|
1205
1280
|
minimal: minimalMode,
|
|
1206
1281
|
lightMode: lightMode,
|
|
1282
|
+
...(leanMode ? { agentsMode: 'lean' } : {}),
|
|
1207
1283
|
};
|
|
1208
1284
|
if (minimalMode) {
|
|
1209
1285
|
config.ides = [];
|
|
@@ -1217,120 +1293,15 @@ export async function updateCommand(options) {
|
|
|
1217
1293
|
license: 'MIT',
|
|
1218
1294
|
});
|
|
1219
1295
|
}
|
|
1220
|
-
//
|
|
1221
|
-
const openspecChangesPath = path.join(cwd, 'openspec', 'changes');
|
|
1222
|
-
if (existsSync(openspecChangesPath)) {
|
|
1223
|
-
const migrationSpinner = ora('Migrating OpenSpec tasks to Rulebook format...').start();
|
|
1224
|
-
const { migrateOpenSpecToRulebook, migrateOpenSpecArchives, removeOpenSpecRulebookFile } = await import('../core/openspec-migrator.js');
|
|
1225
|
-
const rulebookDir = config.rulebookDir || '.rulebook';
|
|
1226
|
-
const migrationResult = await migrateOpenSpecToRulebook(cwd, rulebookDir);
|
|
1227
|
-
const archiveMigrationResult = await migrateOpenSpecArchives(cwd, rulebookDir);
|
|
1228
|
-
if (migrationResult.migrated > 0 || archiveMigrationResult.migrated > 0) {
|
|
1229
|
-
const totalMigrated = migrationResult.migrated + archiveMigrationResult.migrated;
|
|
1230
|
-
migrationSpinner.succeed(`Migrated ${totalMigrated} OpenSpec task(s) to Rulebook format`);
|
|
1231
|
-
if (migrationResult.migratedTasks.length > 0) {
|
|
1232
|
-
console.log(chalk.gray(` Active tasks: ${migrationResult.migratedTasks.join(', ')}`));
|
|
1233
|
-
}
|
|
1234
|
-
if (archiveMigrationResult.migratedTasks.length > 0) {
|
|
1235
|
-
console.log(chalk.gray(` Archived tasks: ${archiveMigrationResult.migratedTasks.join(', ')}`));
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
else if (migrationResult.skipped > 0 || archiveMigrationResult.skipped > 0) {
|
|
1239
|
-
migrationSpinner.info('No OpenSpec tasks to migrate (already migrated or none found)');
|
|
1240
|
-
}
|
|
1241
|
-
else {
|
|
1242
|
-
migrationSpinner.info('No OpenSpec tasks found');
|
|
1243
|
-
}
|
|
1244
|
-
const allErrors = [...migrationResult.errors, ...archiveMigrationResult.errors];
|
|
1245
|
-
if (allErrors.length > 0) {
|
|
1246
|
-
console.log(chalk.yellow('\n⚠️ Migration warnings:'));
|
|
1247
|
-
for (const error of allErrors) {
|
|
1248
|
-
console.log(chalk.yellow(` - ${error}`));
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
// Remove /.rulebook/specs/OPENSPEC.md if exists
|
|
1252
|
-
const removed = await removeOpenSpecRulebookFile(cwd, rulebookDir);
|
|
1253
|
-
if (removed) {
|
|
1254
|
-
console.log(chalk.gray(` Removed /${rulebookDir}/specs/OPENSPEC.md`));
|
|
1255
|
-
}
|
|
1256
|
-
// Remove OpenSpec commands from .cursor/commands/
|
|
1257
|
-
const { removeOpenSpecCommands } = await import('../core/openspec-migrator.js');
|
|
1258
|
-
const removedCommands = await removeOpenSpecCommands(cwd);
|
|
1259
|
-
if (removedCommands > 0) {
|
|
1260
|
-
console.log(chalk.gray(` Removed ${removedCommands} OpenSpec command(s) from .cursor/commands/`));
|
|
1261
|
-
}
|
|
1262
|
-
// Generate Rulebook commands if Cursor is detected or if OpenSpec was used (likely Cursor project)
|
|
1263
|
-
const cursorRulesPath = path.join(cwd, '.cursorrules');
|
|
1264
|
-
const cursorCommandsDir = path.join(cwd, '.cursor', 'commands');
|
|
1265
|
-
const usesCursor = existsSync(cursorRulesPath) || existsSync(cursorCommandsDir);
|
|
1266
|
-
// Always generate commands if OpenSpec exists (OpenSpec was primarily used with Cursor)
|
|
1267
|
-
// or if Cursor is explicitly detected
|
|
1268
|
-
if (usesCursor || removedCommands > 0) {
|
|
1269
|
-
const { generateCursorCommands } = await import('../core/workflow-generator.js');
|
|
1270
|
-
const generatedCommands = await generateCursorCommands(cwd);
|
|
1271
|
-
if (generatedCommands.length > 0) {
|
|
1272
|
-
console.log(chalk.green(` Generated ${generatedCommands.length} Rulebook command(s) in .cursor/commands/`));
|
|
1273
|
-
}
|
|
1274
|
-
else if (usesCursor || removedCommands > 0) {
|
|
1275
|
-
// Commands already exist, just inform user
|
|
1276
|
-
console.log(chalk.gray(' Rulebook commands already exist in .cursor/commands/'));
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
// Remove OpenSpec directory after successful migration
|
|
1280
|
-
const openspecPath = path.join(cwd, 'openspec');
|
|
1281
|
-
if (existsSync(openspecPath)) {
|
|
1282
|
-
const hasErrors = migrationResult.errors.length > 0 || archiveMigrationResult.errors.length > 0;
|
|
1283
|
-
// Remove directory if no errors occurred (migration was successful)
|
|
1284
|
-
// Even if no tasks were migrated (already migrated or empty), remove the directory
|
|
1285
|
-
if (!hasErrors) {
|
|
1286
|
-
try {
|
|
1287
|
-
const { rmSync } = await import('fs');
|
|
1288
|
-
rmSync(openspecPath, { recursive: true, force: true });
|
|
1289
|
-
console.log(chalk.gray(' Removed /openspec directory'));
|
|
1290
|
-
}
|
|
1291
|
-
catch (error) {
|
|
1292
|
-
console.log(chalk.yellow(` ⚠️ Could not remove /openspec directory: ${error.message}`));
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
else {
|
|
1296
|
-
console.log(chalk.yellow(' ⚠️ /openspec directory kept due to migration errors (review and remove manually)'));
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
else {
|
|
1301
|
-
// Check if /openspec directory exists (even without /openspec/changes)
|
|
1302
|
-
const openspecPath = path.join(cwd, 'openspec');
|
|
1303
|
-
if (existsSync(openspecPath)) {
|
|
1304
|
-
// Remove OpenSpec commands and generate Rulebook commands
|
|
1305
|
-
const { removeOpenSpecCommands } = await import('../core/openspec-migrator.js');
|
|
1306
|
-
const removedCommands = await removeOpenSpecCommands(cwd);
|
|
1307
|
-
if (removedCommands > 0) {
|
|
1308
|
-
console.log(chalk.gray(` Removed ${removedCommands} OpenSpec command(s) from .cursor/commands/`));
|
|
1309
|
-
}
|
|
1310
|
-
// Generate Rulebook commands if Cursor is detected or if OpenSpec was used
|
|
1311
|
-
const cursorRulesPath = path.join(cwd, '.cursorrules');
|
|
1312
|
-
const cursorCommandsDir = path.join(cwd, '.cursor', 'commands');
|
|
1313
|
-
const usesCursor = existsSync(cursorRulesPath) || existsSync(cursorCommandsDir);
|
|
1314
|
-
// Always generate commands if OpenSpec exists (OpenSpec was primarily used with Cursor)
|
|
1315
|
-
// or if Cursor is explicitly detected
|
|
1316
|
-
if (usesCursor || removedCommands > 0) {
|
|
1317
|
-
const { generateCursorCommands } = await import('../core/workflow-generator.js');
|
|
1318
|
-
const generatedCommands = await generateCursorCommands(cwd);
|
|
1319
|
-
if (generatedCommands.length > 0) {
|
|
1320
|
-
console.log(chalk.green(` Generated ${generatedCommands.length} Rulebook command(s) in .cursor/commands/`));
|
|
1321
|
-
}
|
|
1322
|
-
else if (usesCursor || removedCommands > 0) {
|
|
1323
|
-
// Commands already exist, just inform user
|
|
1324
|
-
console.log(chalk.gray(' Rulebook commands already exist in .cursor/commands/'));
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
// Always generate Rulebook commands if Cursor is detected (even without OpenSpec)
|
|
1296
|
+
// Generate Rulebook commands if Cursor is detected
|
|
1330
1297
|
// This ensures commands are available for all Cursor projects
|
|
1331
1298
|
const cursorRulesPath = path.join(cwd, '.cursorrules');
|
|
1332
1299
|
const cursorCommandsDir = path.join(cwd, '.cursor', 'commands');
|
|
1333
1300
|
const usesCursor = existsSync(cursorRulesPath) || existsSync(cursorCommandsDir);
|
|
1301
|
+
// Deprecated notice: .cursorrules is superseded by .cursor/rules/*.mdc in Cursor v0.45+
|
|
1302
|
+
if (existsSync(cursorRulesPath)) {
|
|
1303
|
+
console.log(chalk.yellow(' ⚠ .cursorrules is deprecated as of Cursor v0.45. Use .cursor/rules/*.mdc instead.'));
|
|
1304
|
+
}
|
|
1334
1305
|
if (usesCursor) {
|
|
1335
1306
|
// Check if commands already exist to avoid duplicate generation
|
|
1336
1307
|
const existingCommandsDir = path.join(cwd, '.cursor', 'commands');
|
|
@@ -1424,6 +1395,19 @@ export async function updateCommand(options) {
|
|
|
1424
1395
|
const mergedContent = await mergeFullAgents(detection.existingAgents, config, cwd);
|
|
1425
1396
|
await writeFile(agentsPath, mergedContent);
|
|
1426
1397
|
mergeSpinner.succeed('AGENTS.md updated');
|
|
1398
|
+
// Show multi-tool config feedback (update command)
|
|
1399
|
+
if (detection.geminiCli?.detected) {
|
|
1400
|
+
console.log(chalk.gray(' • Gemini CLI config updated: GEMINI.md'));
|
|
1401
|
+
}
|
|
1402
|
+
if (detection.continueDev?.detected) {
|
|
1403
|
+
console.log(chalk.gray(' • Continue.dev rules updated in .continue/rules/'));
|
|
1404
|
+
}
|
|
1405
|
+
if (detection.windsurf?.detected) {
|
|
1406
|
+
console.log(chalk.gray(' • Windsurf rules updated: .windsurfrules'));
|
|
1407
|
+
}
|
|
1408
|
+
if (detection.githubCopilot?.detected) {
|
|
1409
|
+
console.log(chalk.gray(' • GitHub Copilot instructions updated in .github/'));
|
|
1410
|
+
}
|
|
1427
1411
|
if (installHooksOnUpdate) {
|
|
1428
1412
|
const hookLanguages = detection.languages.length > 0
|
|
1429
1413
|
? detection.languages
|
|
@@ -1485,6 +1469,11 @@ export async function updateCommand(options) {
|
|
|
1485
1469
|
...(existingConfig.memory ? { memory: existingConfig.memory } : {}),
|
|
1486
1470
|
...(existingConfig.ralph ? { ralph: existingConfig.ralph } : {}),
|
|
1487
1471
|
...(existingConfig.skills ? { skills: existingConfig.skills } : {}),
|
|
1472
|
+
...(leanMode
|
|
1473
|
+
? { agentsMode: 'lean' }
|
|
1474
|
+
: existingConfig.agentsMode
|
|
1475
|
+
? { agentsMode: existingConfig.agentsMode }
|
|
1476
|
+
: {}),
|
|
1488
1477
|
};
|
|
1489
1478
|
await configManager.saveConfig(rulebookConfig);
|
|
1490
1479
|
configSpinner.succeed('.rulebook configuration updated');
|
|
@@ -1501,6 +1490,12 @@ export async function updateCommand(options) {
|
|
|
1501
1490
|
if (result.skillsInstalled.length > 0) {
|
|
1502
1491
|
console.log(chalk.gray(` • ${result.skillsInstalled.length} skills updated in .claude/commands/`));
|
|
1503
1492
|
}
|
|
1493
|
+
if (result.agentTeamsEnabled) {
|
|
1494
|
+
console.log(chalk.gray(' • Multi-agent teams enabled in .claude/settings.json'));
|
|
1495
|
+
}
|
|
1496
|
+
if (result.agentDefinitionsInstalled.length > 0) {
|
|
1497
|
+
console.log(chalk.gray(` • ${result.agentDefinitionsInstalled.length} agent definitions updated in .claude/agents/`));
|
|
1498
|
+
}
|
|
1504
1499
|
}
|
|
1505
1500
|
else {
|
|
1506
1501
|
claudeSpinner.info('Claude Code not detected (skipped)');
|
|
@@ -1509,6 +1504,25 @@ export async function updateCommand(options) {
|
|
|
1509
1504
|
catch {
|
|
1510
1505
|
claudeSpinner.info('Claude Code integration skipped');
|
|
1511
1506
|
}
|
|
1507
|
+
// Install/update Ralph shell scripts
|
|
1508
|
+
try {
|
|
1509
|
+
const { installRalphScripts } = await import('../core/ralph-scripts.js');
|
|
1510
|
+
const scripts = await installRalphScripts(cwd);
|
|
1511
|
+
if (scripts.length > 0) {
|
|
1512
|
+
console.log(chalk.gray(` • ${scripts.length} Ralph scripts updated in .rulebook/scripts/`));
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
catch {
|
|
1516
|
+
// Skip if Ralph scripts installation fails
|
|
1517
|
+
}
|
|
1518
|
+
// Ensure PLANS.md exists (create if missing, never overwrite)
|
|
1519
|
+
try {
|
|
1520
|
+
const { initPlans } = await import('../core/plans-manager.js');
|
|
1521
|
+
await initPlans(cwd);
|
|
1522
|
+
}
|
|
1523
|
+
catch {
|
|
1524
|
+
// Non-blocking
|
|
1525
|
+
}
|
|
1512
1526
|
// Migrate memory directory if old structure exists
|
|
1513
1527
|
try {
|
|
1514
1528
|
await migrateMemoryDirectory();
|
|
@@ -1989,6 +2003,58 @@ export async function memoryStatsCommand() {
|
|
|
1989
2003
|
process.exit(1);
|
|
1990
2004
|
}
|
|
1991
2005
|
}
|
|
2006
|
+
export async function memoryVerifyCommand() {
|
|
2007
|
+
const ora = (await import('ora')).default;
|
|
2008
|
+
const chalk = (await import('chalk')).default;
|
|
2009
|
+
const spinner = ora('Verifying memory system...').start();
|
|
2010
|
+
try {
|
|
2011
|
+
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2012
|
+
const cwd = process.cwd();
|
|
2013
|
+
const configManager = createConfigManager(cwd);
|
|
2014
|
+
const config = await configManager.loadConfig();
|
|
2015
|
+
const memoryEnabled = config.memory?.enabled ?? false;
|
|
2016
|
+
const dbPathRelative = config.memory?.dbPath ?? '.rulebook/memory/memory.db';
|
|
2017
|
+
const dbPathAbsolute = path.join(cwd, dbPathRelative);
|
|
2018
|
+
spinner.succeed('Memory verification');
|
|
2019
|
+
// Check enabled status
|
|
2020
|
+
console.log(`\n ${memoryEnabled ? chalk.green('✓') : chalk.red('✗')} Memory enabled: ${memoryEnabled}`);
|
|
2021
|
+
// Check DB path
|
|
2022
|
+
console.log(` ${chalk.green('✓')} DB path: ${dbPathRelative}`);
|
|
2023
|
+
// Check if file exists on disk
|
|
2024
|
+
const fileExists = existsSync(dbPathAbsolute);
|
|
2025
|
+
if (fileExists) {
|
|
2026
|
+
const { statSync } = await import('fs');
|
|
2027
|
+
const fileStat = statSync(dbPathAbsolute);
|
|
2028
|
+
const sizeKB = (fileStat.size / 1024).toFixed(1);
|
|
2029
|
+
console.log(` ${chalk.green('✓')} File exists: YES (${sizeKB} KB)`);
|
|
2030
|
+
}
|
|
2031
|
+
else {
|
|
2032
|
+
console.log(` ${chalk.red('✗')} File exists: NO`);
|
|
2033
|
+
}
|
|
2034
|
+
// If memory is enabled and file exists, show record count
|
|
2035
|
+
if (memoryEnabled && fileExists) {
|
|
2036
|
+
try {
|
|
2037
|
+
const { createMemoryManager } = await import('../memory/memory-manager.js');
|
|
2038
|
+
const manager = createMemoryManager(cwd, config.memory);
|
|
2039
|
+
const stats = await manager.getStats();
|
|
2040
|
+
console.log(` ${chalk.green('✓')} Record count: ${stats.memoryCount} memories`);
|
|
2041
|
+
await manager.close();
|
|
2042
|
+
}
|
|
2043
|
+
catch (error) {
|
|
2044
|
+
console.log(` ${chalk.yellow('!')} Record count: unable to read (${String(error)})`);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
else if (!memoryEnabled) {
|
|
2048
|
+
console.log(` ${chalk.yellow('!')} Enable memory with: ${chalk.bold('rulebook config --feature memory --enable')}`);
|
|
2049
|
+
}
|
|
2050
|
+
console.log('');
|
|
2051
|
+
}
|
|
2052
|
+
catch (error) {
|
|
2053
|
+
spinner.fail('Memory verification failed');
|
|
2054
|
+
console.error(chalk.red(String(error)));
|
|
2055
|
+
process.exit(1);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
1992
2058
|
export async function memoryCleanupCommand(options) {
|
|
1993
2059
|
const ora = (await import('ora')).default;
|
|
1994
2060
|
const chalk = (await import('chalk')).default;
|
|
@@ -2103,6 +2169,7 @@ export async function ralphRunCommand(options) {
|
|
|
2103
2169
|
const { RalphManager } = await import('../core/ralph-manager.js');
|
|
2104
2170
|
const { RalphParser } = await import('../agents/ralph-parser.js');
|
|
2105
2171
|
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2172
|
+
const { IterationTracker } = await import('../core/iteration-tracker.js');
|
|
2106
2173
|
const childProcess = await import('child_process');
|
|
2107
2174
|
const logger = new Logger(cwd);
|
|
2108
2175
|
const configManager = createConfigManager(cwd);
|
|
@@ -2110,6 +2177,22 @@ export async function ralphRunCommand(options) {
|
|
|
2110
2177
|
const ralphManager = new RalphManager(cwd, logger);
|
|
2111
2178
|
const maxIterations = options.maxIterations || config.ralph?.maxIterations || 10;
|
|
2112
2179
|
const tool = options.tool || config.ralph?.tool || 'claude';
|
|
2180
|
+
// Resolve parallel mode — CLI flag takes precedence over config
|
|
2181
|
+
const parallelWorkers = options.parallel ??
|
|
2182
|
+
(config.ralph?.parallel?.enabled ? config.ralph.parallel.maxWorkers : undefined);
|
|
2183
|
+
// Resolve plan checkpoint config — --plan-first CLI flag takes precedence
|
|
2184
|
+
const planCheckpointConfig = {
|
|
2185
|
+
enabled: options.planFirst ?? config.ralph?.planCheckpoint?.enabled ?? false,
|
|
2186
|
+
autoApproveAfterSeconds: config.ralph?.planCheckpoint?.autoApproveAfterSeconds ?? 0,
|
|
2187
|
+
requireApprovalForStories: config.ralph?.planCheckpoint?.requireApprovalForStories ?? 'all',
|
|
2188
|
+
};
|
|
2189
|
+
// Context compression config
|
|
2190
|
+
const compressionConfig = config.ralph?.contextCompression;
|
|
2191
|
+
const compressionEnabled = compressionConfig?.enabled !== false;
|
|
2192
|
+
const compressionRecentCount = compressionConfig?.recentCount ?? 3;
|
|
2193
|
+
const compressionThreshold = compressionConfig?.threshold ?? 5;
|
|
2194
|
+
const iterationTracker = new IterationTracker(cwd, logger);
|
|
2195
|
+
await iterationTracker.initialize();
|
|
2113
2196
|
await ralphManager.initialize(maxIterations, tool);
|
|
2114
2197
|
// Create git branch from PRD
|
|
2115
2198
|
const prd = await ralphManager.loadPRD();
|
|
@@ -2126,6 +2209,115 @@ export async function ralphRunCommand(options) {
|
|
|
2126
2209
|
process.on('SIGINT', handleInterrupt);
|
|
2127
2210
|
// Sync task count from PRD (may have been saved after initialize)
|
|
2128
2211
|
await ralphManager.refreshTaskCount();
|
|
2212
|
+
// ─── Parallel execution mode ───
|
|
2213
|
+
if (parallelWorkers && parallelWorkers > 1) {
|
|
2214
|
+
spinner.text = `Ralph parallel mode (${parallelWorkers} workers)...`;
|
|
2215
|
+
const batches = await ralphManager.getParallelBatches(parallelWorkers);
|
|
2216
|
+
spinner.stop();
|
|
2217
|
+
console.log(chalk.bold.cyan(`\n Parallel mode: ${batches.length} batch(es), max ${parallelWorkers} workers\n`));
|
|
2218
|
+
let iterationCount = 0;
|
|
2219
|
+
for (const batch of batches) {
|
|
2220
|
+
if (interrupted)
|
|
2221
|
+
break;
|
|
2222
|
+
console.log(chalk.bold(` ── Batch: ${batch.map((s) => s.id).join(', ')} (${batch.length} stories) ──`));
|
|
2223
|
+
// Run all stories in the batch concurrently
|
|
2224
|
+
const batchResults = await Promise.allSettled(batch.map(async (task) => {
|
|
2225
|
+
iterationCount++;
|
|
2226
|
+
const localIteration = iterationCount;
|
|
2227
|
+
const startTime = Date.now();
|
|
2228
|
+
// Build context (shared — read-only)
|
|
2229
|
+
let contextHistory = '';
|
|
2230
|
+
if (compressionEnabled) {
|
|
2231
|
+
contextHistory = await iterationTracker.buildCompressedContext(compressionRecentCount, compressionThreshold);
|
|
2232
|
+
}
|
|
2233
|
+
let plansContext = '';
|
|
2234
|
+
try {
|
|
2235
|
+
const { readPlans, plansExists } = await import('../core/plans-manager.js');
|
|
2236
|
+
if (plansExists(cwd)) {
|
|
2237
|
+
const plans = await readPlans(cwd);
|
|
2238
|
+
if (plans?.context && plans.context.trim()) {
|
|
2239
|
+
plansContext = plans.context.trim();
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
catch {
|
|
2244
|
+
// PLANS.md injection is optional
|
|
2245
|
+
}
|
|
2246
|
+
const prompt = ralphBuildPrompt(task, prd, contextHistory, plansContext);
|
|
2247
|
+
let agentOutput = '';
|
|
2248
|
+
try {
|
|
2249
|
+
agentOutput = await ralphExecuteAgent(tool, prompt, cwd, childProcess.spawn);
|
|
2250
|
+
}
|
|
2251
|
+
catch (agentError) {
|
|
2252
|
+
agentOutput = `Error executing agent: ${agentError.message || agentError}`;
|
|
2253
|
+
}
|
|
2254
|
+
const qualityResults = await ralphRunQualityGates(cwd, childProcess.spawn);
|
|
2255
|
+
const executionTime = Date.now() - startTime;
|
|
2256
|
+
const parsed = RalphParser.parseAgentOutput(agentOutput, localIteration, task.id, task.title, tool);
|
|
2257
|
+
const allGatesPass = qualityResults.type_check &&
|
|
2258
|
+
qualityResults.lint &&
|
|
2259
|
+
qualityResults.tests &&
|
|
2260
|
+
qualityResults.coverage_met;
|
|
2261
|
+
const passCount = Object.values(qualityResults).filter(Boolean).length;
|
|
2262
|
+
const status = allGatesPass
|
|
2263
|
+
? 'success'
|
|
2264
|
+
: passCount >= 2
|
|
2265
|
+
? 'partial'
|
|
2266
|
+
: 'failed';
|
|
2267
|
+
let gitCommit;
|
|
2268
|
+
if (allGatesPass) {
|
|
2269
|
+
gitCommit = await ralphGitCommit(cwd, task, localIteration, childProcess.spawn);
|
|
2270
|
+
await ralphManager.markStoryComplete(task.id);
|
|
2271
|
+
console.log(chalk.green(` [parallel] Story ${task.id} completed`));
|
|
2272
|
+
}
|
|
2273
|
+
else {
|
|
2274
|
+
console.log(chalk.yellow(` [parallel] Story ${task.id} not completed (quality gates failed)`));
|
|
2275
|
+
}
|
|
2276
|
+
const result = {
|
|
2277
|
+
iteration: localIteration,
|
|
2278
|
+
timestamp: new Date().toISOString(),
|
|
2279
|
+
task_id: task.id,
|
|
2280
|
+
task_title: task.title,
|
|
2281
|
+
status,
|
|
2282
|
+
ai_tool: tool,
|
|
2283
|
+
execution_time_ms: executionTime,
|
|
2284
|
+
quality_checks: qualityResults,
|
|
2285
|
+
output_summary: parsed.output_summary || `Iteration ${localIteration}: ${task.title}`,
|
|
2286
|
+
git_commit: gitCommit,
|
|
2287
|
+
learnings: parsed.learnings,
|
|
2288
|
+
errors: parsed.errors,
|
|
2289
|
+
metadata: {
|
|
2290
|
+
context_loss_count: parsed.metadata.context_loss_count,
|
|
2291
|
+
parsed_completion: parsed.metadata.parsed_completion,
|
|
2292
|
+
},
|
|
2293
|
+
};
|
|
2294
|
+
await ralphManager.recordIteration(result);
|
|
2295
|
+
return result;
|
|
2296
|
+
}));
|
|
2297
|
+
// Log rejected promises
|
|
2298
|
+
for (const [i, result] of batchResults.entries()) {
|
|
2299
|
+
if (result.status === 'rejected') {
|
|
2300
|
+
const story = batch[i];
|
|
2301
|
+
console.log(chalk.red(` [parallel] Story ${story.id} threw: ${result.reason}`));
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
// Check for pause
|
|
2305
|
+
const currentStatus = await ralphManager.getStatus();
|
|
2306
|
+
if (currentStatus?.paused)
|
|
2307
|
+
break;
|
|
2308
|
+
}
|
|
2309
|
+
// Cleanup and summary for parallel mode
|
|
2310
|
+
process.removeListener('SIGINT', handleInterrupt);
|
|
2311
|
+
const stats = await ralphManager.getTaskStats();
|
|
2312
|
+
console.log(`\n Parallel run complete: ${stats.completed}/${stats.total} tasks completed`);
|
|
2313
|
+
console.log(` Iterations: ${iterationCount}`);
|
|
2314
|
+
if (interrupted) {
|
|
2315
|
+
console.log(chalk.yellow(` Paused by user. Resume: ${chalk.bold('rulebook ralph resume')}`));
|
|
2316
|
+
}
|
|
2317
|
+
console.log(`\n View history: ${chalk.bold('rulebook ralph history')}\n`);
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
// ─── Sequential execution (default) ───
|
|
2129
2321
|
spinner.text = 'Ralph loop running (Ctrl+C to pause)...';
|
|
2130
2322
|
let iterationCount = 0;
|
|
2131
2323
|
while (ralphManager.canContinue() && !interrupted) {
|
|
@@ -2137,8 +2329,38 @@ export async function ralphRunCommand(options) {
|
|
|
2137
2329
|
spinner.stop();
|
|
2138
2330
|
console.log(chalk.bold.cyan(`\n ── Iteration ${iterationCount}: ${task.title} ──\n`));
|
|
2139
2331
|
const startTime = Date.now();
|
|
2332
|
+
// 0. Plan checkpoint — require approval before implementation
|
|
2333
|
+
if (planCheckpointConfig.enabled) {
|
|
2334
|
+
const checkpoint = await ralphManager.runCheckpoint(task, tool, planCheckpointConfig);
|
|
2335
|
+
if (!checkpoint.proceed) {
|
|
2336
|
+
console.log(chalk.yellow(` Plan rejected for ${task.id}. Skipping implementation.`));
|
|
2337
|
+
if (checkpoint.feedback) {
|
|
2338
|
+
console.log(chalk.gray(` Feedback: ${checkpoint.feedback}`));
|
|
2339
|
+
}
|
|
2340
|
+
spinner.start('Preparing next iteration...');
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2140
2344
|
// 1. Execute AI agent with task context
|
|
2141
|
-
|
|
2345
|
+
let contextHistory = '';
|
|
2346
|
+
if (compressionEnabled) {
|
|
2347
|
+
contextHistory = await iterationTracker.buildCompressedContext(compressionRecentCount, compressionThreshold);
|
|
2348
|
+
}
|
|
2349
|
+
// Read PLANS.md context for session scratchpad injection
|
|
2350
|
+
let plansContext = '';
|
|
2351
|
+
try {
|
|
2352
|
+
const { readPlans, plansExists } = await import('../core/plans-manager.js');
|
|
2353
|
+
if (plansExists(cwd)) {
|
|
2354
|
+
const plans = await readPlans(cwd);
|
|
2355
|
+
if (plans?.context && plans.context.trim()) {
|
|
2356
|
+
plansContext = plans.context.trim();
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
catch {
|
|
2361
|
+
// PLANS.md injection is optional — skip on error
|
|
2362
|
+
}
|
|
2363
|
+
const prompt = ralphBuildPrompt(task, prd, contextHistory, plansContext);
|
|
2142
2364
|
let agentOutput = '';
|
|
2143
2365
|
try {
|
|
2144
2366
|
agentOutput = await ralphExecuteAgent(tool, prompt, cwd, childProcess.spawn);
|
|
@@ -2223,11 +2445,15 @@ export async function ralphRunCommand(options) {
|
|
|
2223
2445
|
/**
|
|
2224
2446
|
* Build prompt for AI agent from user story context
|
|
2225
2447
|
*/
|
|
2226
|
-
function ralphBuildPrompt(task, prd) {
|
|
2448
|
+
function ralphBuildPrompt(task, prd, contextHistory, plansContext) {
|
|
2227
2449
|
const criteria = (task.acceptanceCriteria || []).map((c) => `- ${c}`).join('\n');
|
|
2228
2450
|
return [
|
|
2229
2451
|
`You are working on project: ${prd?.project || 'unknown'}`,
|
|
2230
2452
|
``,
|
|
2453
|
+
plansContext ? `## Session Context (PLANS.md)\n${plansContext}\n` : '',
|
|
2454
|
+
contextHistory && contextHistory !== 'No iteration history available.'
|
|
2455
|
+
? `## Iteration History\n${contextHistory}\n`
|
|
2456
|
+
: '',
|
|
2231
2457
|
`## Current Task: ${task.title}`,
|
|
2232
2458
|
`ID: ${task.id}`,
|
|
2233
2459
|
``,
|
|
@@ -2328,11 +2554,25 @@ async function ralphRunQualityGates(cwd, spawn) {
|
|
|
2328
2554
|
}, 120000);
|
|
2329
2555
|
});
|
|
2330
2556
|
};
|
|
2557
|
+
// Detect monorepo to choose the right test command
|
|
2558
|
+
const { detectMonorepo } = await import('../core/detector.js');
|
|
2559
|
+
const monorepo = await detectMonorepo(cwd).catch(() => ({
|
|
2560
|
+
detected: false,
|
|
2561
|
+
tool: null,
|
|
2562
|
+
packages: [],
|
|
2563
|
+
}));
|
|
2564
|
+
let testCmd = ['npm', ['test']];
|
|
2565
|
+
if (monorepo.detected) {
|
|
2566
|
+
if (monorepo.tool === 'turborepo')
|
|
2567
|
+
testCmd = ['turbo', ['run', 'test']];
|
|
2568
|
+
else if (monorepo.tool === 'nx')
|
|
2569
|
+
testCmd = ['nx', ['run-many', '--target=test']];
|
|
2570
|
+
}
|
|
2331
2571
|
// Run gates in parallel
|
|
2332
2572
|
const [typeCheck, lint, tests] = await Promise.all([
|
|
2333
2573
|
runGate('npm', ['run', 'type-check']),
|
|
2334
2574
|
runGate('npm', ['run', 'lint']),
|
|
2335
|
-
runGate(
|
|
2575
|
+
runGate(testCmd[0], testCmd[1]),
|
|
2336
2576
|
]);
|
|
2337
2577
|
return {
|
|
2338
2578
|
type_check: typeCheck,
|
|
@@ -2439,12 +2679,18 @@ export async function ralphStatusCommand() {
|
|
|
2439
2679
|
return;
|
|
2440
2680
|
}
|
|
2441
2681
|
spinner.stop();
|
|
2682
|
+
// Show agentsMode from config
|
|
2683
|
+
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2684
|
+
const configManager = createConfigManager(cwd);
|
|
2685
|
+
const cfg = await configManager.loadConfig();
|
|
2686
|
+
const agentsMode = cfg.agentsMode ?? 'full';
|
|
2442
2687
|
console.log(`\n ${chalk.bold('Ralph Loop Status')}`);
|
|
2443
2688
|
console.log(` Iteration: ${status.current_iteration}/${status.max_iterations}`);
|
|
2444
2689
|
console.log(` Tasks: ${status.completed_tasks}/${status.total_tasks}`);
|
|
2445
2690
|
console.log(` Status: ${status.paused ? chalk.yellow('PAUSED') : chalk.green('RUNNING')}`);
|
|
2446
2691
|
console.log(` AI Tool: ${status.tool}`);
|
|
2447
2692
|
console.log(` Started: ${new Date(status.started_at).toLocaleString()}`);
|
|
2693
|
+
console.log(` Agents Mode: ${agentsMode === 'lean' ? chalk.cyan('lean') : chalk.gray('full')} (rulebook mode set lean|full)`);
|
|
2448
2694
|
console.log();
|
|
2449
2695
|
}
|
|
2450
2696
|
catch (error) {
|
|
@@ -2618,6 +2864,302 @@ export async function setupClaudeCodePlugin() {
|
|
|
2618
2864
|
process.exit(1);
|
|
2619
2865
|
}
|
|
2620
2866
|
}
|
|
2867
|
+
// ─── Plans Commands ────────────────────────────────────────────────────────
|
|
2868
|
+
/**
|
|
2869
|
+
* Add sequential-thinking MCP server entry to mcp.json (or .cursor/mcp.json).
|
|
2870
|
+
* Non-destructive: skips if already present.
|
|
2871
|
+
*/
|
|
2872
|
+
async function addSequentialThinkingMcp(cwd) {
|
|
2873
|
+
const { readFileSync, writeFileSync, existsSync } = await import('fs');
|
|
2874
|
+
const { mkdirSync } = await import('fs');
|
|
2875
|
+
const seqEntry = {
|
|
2876
|
+
command: 'npx',
|
|
2877
|
+
args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
|
|
2878
|
+
};
|
|
2879
|
+
const candidates = [
|
|
2880
|
+
path.join(cwd, 'mcp.json'),
|
|
2881
|
+
path.join(cwd, '.cursor', 'mcp.json'),
|
|
2882
|
+
path.join(cwd, '.mcp.json'),
|
|
2883
|
+
];
|
|
2884
|
+
// Find existing mcp.json or default to mcp.json
|
|
2885
|
+
let mcpPath = candidates.find((p) => existsSync(p)) ?? path.join(cwd, 'mcp.json');
|
|
2886
|
+
let mcpConfig = {};
|
|
2887
|
+
if (existsSync(mcpPath)) {
|
|
2888
|
+
try {
|
|
2889
|
+
mcpConfig = JSON.parse(readFileSync(mcpPath, 'utf8'));
|
|
2890
|
+
}
|
|
2891
|
+
catch {
|
|
2892
|
+
mcpConfig = {};
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
mcpConfig.mcpServers = mcpConfig.mcpServers ?? {};
|
|
2896
|
+
// Already configured under any key variant
|
|
2897
|
+
const keys = Object.keys(mcpConfig.mcpServers);
|
|
2898
|
+
const alreadyPresent = keys.some((k) => ['sequential-thinking', 'sequential_thinking', 'sequentialThinking'].includes(k));
|
|
2899
|
+
if (alreadyPresent)
|
|
2900
|
+
return;
|
|
2901
|
+
mcpConfig.mcpServers['sequential-thinking'] = seqEntry;
|
|
2902
|
+
// Ensure directory exists
|
|
2903
|
+
const dir = path.dirname(mcpPath);
|
|
2904
|
+
if (!existsSync(dir))
|
|
2905
|
+
mkdirSync(dir, { recursive: true });
|
|
2906
|
+
writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Show contents of AGENTS.override.md.
|
|
2910
|
+
*/
|
|
2911
|
+
export async function overrideShowCommand() {
|
|
2912
|
+
const cwd = process.cwd();
|
|
2913
|
+
const { overrideExists, getOverridePath, readOverrideContent } = await import('../core/override-manager.js');
|
|
2914
|
+
if (!overrideExists(cwd)) {
|
|
2915
|
+
console.log(chalk.yellow('AGENTS.override.md does not exist. Run `rulebook override edit` or `rulebook init` to create it.'));
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
const content = await readOverrideContent(cwd);
|
|
2919
|
+
if (!content) {
|
|
2920
|
+
console.log(chalk.gray('AGENTS.override.md exists but has no custom content yet.'));
|
|
2921
|
+
console.log(chalk.gray(` Path: ${getOverridePath(cwd)}`));
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
console.log(chalk.bold('\n📄 AGENTS.override.md\n'));
|
|
2925
|
+
console.log(content);
|
|
2926
|
+
console.log();
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* Open AGENTS.override.md in $EDITOR, or print path if no EDITOR.
|
|
2930
|
+
*/
|
|
2931
|
+
export async function overrideEditCommand() {
|
|
2932
|
+
const cwd = process.cwd();
|
|
2933
|
+
const { initOverride, getOverridePath } = await import('../core/override-manager.js');
|
|
2934
|
+
await initOverride(cwd); // create if missing
|
|
2935
|
+
const overridePath = getOverridePath(cwd);
|
|
2936
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
2937
|
+
if (editor) {
|
|
2938
|
+
const { spawn } = await import('child_process');
|
|
2939
|
+
const proc = spawn(editor, [overridePath], { stdio: 'inherit', shell: true });
|
|
2940
|
+
await new Promise((resolve) => proc.on('close', () => resolve()));
|
|
2941
|
+
}
|
|
2942
|
+
else {
|
|
2943
|
+
console.log(chalk.gray(`No $EDITOR set. Edit the file directly:`));
|
|
2944
|
+
console.log(chalk.cyan(` ${overridePath}`));
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
/**
|
|
2948
|
+
* Reset AGENTS.override.md to empty template.
|
|
2949
|
+
*/
|
|
2950
|
+
export async function overrideClearCommand() {
|
|
2951
|
+
const cwd = process.cwd();
|
|
2952
|
+
const { clearOverride } = await import('../core/override-manager.js');
|
|
2953
|
+
await clearOverride(cwd);
|
|
2954
|
+
console.log(chalk.green('✓ AGENTS.override.md reset to empty template'));
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Set the AGENTS.md generation mode (lean or full).
|
|
2958
|
+
*/
|
|
2959
|
+
export async function modeSetCommand(mode) {
|
|
2960
|
+
const cwd = process.cwd();
|
|
2961
|
+
const { createConfigManager } = await import('../core/config-manager.js');
|
|
2962
|
+
const configManager = createConfigManager(cwd);
|
|
2963
|
+
const config = await configManager.loadConfig();
|
|
2964
|
+
config.agentsMode = mode;
|
|
2965
|
+
await configManager.saveConfig(config);
|
|
2966
|
+
console.log(chalk.green(`✓ AGENTS.md mode set to: ${chalk.bold(mode)}`));
|
|
2967
|
+
if (mode === 'lean') {
|
|
2968
|
+
console.log(chalk.gray(' Lean mode: AGENTS.md will be a lightweight index (<3KB).\n' +
|
|
2969
|
+
' Run `rulebook update` to regenerate AGENTS.md.'));
|
|
2970
|
+
}
|
|
2971
|
+
else {
|
|
2972
|
+
console.log(chalk.gray(' Full mode: AGENTS.md will include all rules inline.\n' +
|
|
2973
|
+
' Run `rulebook update` to regenerate AGENTS.md.'));
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
/**
|
|
2977
|
+
* Show current PLANS.md content.
|
|
2978
|
+
*/
|
|
2979
|
+
export async function plansShowCommand() {
|
|
2980
|
+
const { readPlans, getPlansPath } = await import('../core/plans-manager.js');
|
|
2981
|
+
const cwd = process.cwd();
|
|
2982
|
+
const plans = await readPlans(cwd);
|
|
2983
|
+
if (!plans) {
|
|
2984
|
+
console.log(chalk.yellow(`No PLANS.md found at ${getPlansPath(cwd)}`));
|
|
2985
|
+
console.log(chalk.gray('Run `rulebook plans init` to create one.'));
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
console.log(chalk.bold.blue('\n📋 PLANS.md — Session Scratchpad\n'));
|
|
2989
|
+
if (plans.context &&
|
|
2990
|
+
plans.context !== '_No active context. Start a session to populate this section._') {
|
|
2991
|
+
console.log(chalk.bold('Active Context:'));
|
|
2992
|
+
console.log(chalk.white(plans.context));
|
|
2993
|
+
}
|
|
2994
|
+
if (plans.currentTask && plans.currentTask !== '_No task in progress._') {
|
|
2995
|
+
console.log(chalk.bold('\nCurrent Task:'));
|
|
2996
|
+
console.log(chalk.cyan(plans.currentTask));
|
|
2997
|
+
}
|
|
2998
|
+
if (plans.history) {
|
|
2999
|
+
console.log(chalk.bold('\nSession History:'));
|
|
3000
|
+
console.log(chalk.gray(plans.history));
|
|
3001
|
+
}
|
|
3002
|
+
console.log('');
|
|
3003
|
+
}
|
|
3004
|
+
/**
|
|
3005
|
+
* Initialize PLANS.md in project root.
|
|
3006
|
+
*/
|
|
3007
|
+
export async function plansInitCommand() {
|
|
3008
|
+
const { initPlans, getPlansPath } = await import('../core/plans-manager.js');
|
|
3009
|
+
const cwd = process.cwd();
|
|
3010
|
+
const created = await initPlans(cwd);
|
|
3011
|
+
if (created) {
|
|
3012
|
+
console.log(chalk.green(`✓ Created ${getPlansPath(cwd)}`));
|
|
3013
|
+
console.log(chalk.gray(' AI agents will use this file for session continuity.'));
|
|
3014
|
+
}
|
|
3015
|
+
else {
|
|
3016
|
+
console.log(chalk.yellow(`PLANS.md already exists at ${getPlansPath(cwd)}`));
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* Reset PLANS.md to the empty template.
|
|
3021
|
+
*/
|
|
3022
|
+
export async function plansClearCommand() {
|
|
3023
|
+
const { clearPlans, getPlansPath } = await import('../core/plans-manager.js');
|
|
3024
|
+
const cwd = process.cwd();
|
|
3025
|
+
await clearPlans(cwd);
|
|
3026
|
+
console.log(chalk.green(`✓ Cleared ${getPlansPath(cwd)}`));
|
|
3027
|
+
console.log(chalk.gray(' Session history and context have been reset.'));
|
|
3028
|
+
}
|
|
3029
|
+
// ─── Continue Command ───────────────────────────────────────────────────────
|
|
3030
|
+
/**
|
|
3031
|
+
* `rulebook continue` — Print orientations to resume work in a new AI session.
|
|
3032
|
+
*
|
|
3033
|
+
* Aggregates context from:
|
|
3034
|
+
* 1. PLANS.md (session scratchpad)
|
|
3035
|
+
* 2. Active rulebook tasks (pending items in tasks.md)
|
|
3036
|
+
* 3. Recent git commits (last 5)
|
|
3037
|
+
* 4. Ralph status (if running)
|
|
3038
|
+
*
|
|
3039
|
+
* Outputs a structured prompt that the AI agent can paste at the start of a session.
|
|
3040
|
+
*/
|
|
3041
|
+
export async function continueCommand() {
|
|
3042
|
+
const cwd = process.cwd();
|
|
3043
|
+
const { readPlans } = await import('../core/plans-manager.js');
|
|
3044
|
+
const { exec } = await import('child_process');
|
|
3045
|
+
const { promisify } = await import('util');
|
|
3046
|
+
const execAsync = promisify(exec);
|
|
3047
|
+
const fs = await import('fs/promises');
|
|
3048
|
+
console.log(chalk.bold.blue('\n🔄 Generating session continuity context...\n'));
|
|
3049
|
+
const sections = [];
|
|
3050
|
+
// ── 1. PLANS.md context ──
|
|
3051
|
+
const plans = await readPlans(cwd);
|
|
3052
|
+
if (plans && (plans.context || plans.currentTask)) {
|
|
3053
|
+
const plansParts = ['## Active Plans'];
|
|
3054
|
+
if (plans.context && !plans.context.includes('_No active context')) {
|
|
3055
|
+
plansParts.push(plans.context);
|
|
3056
|
+
}
|
|
3057
|
+
if (plans.currentTask && !plans.currentTask.includes('_No task')) {
|
|
3058
|
+
plansParts.push(`**Current Task:** ${plans.currentTask}`);
|
|
3059
|
+
}
|
|
3060
|
+
sections.push(plansParts.join('\n'));
|
|
3061
|
+
}
|
|
3062
|
+
// ── 2. Active tasks (pending checklist items) ──
|
|
3063
|
+
const tasksDir = path.join(cwd, '.rulebook', 'tasks');
|
|
3064
|
+
if (existsSync(tasksDir)) {
|
|
3065
|
+
const taskSummaries = [];
|
|
3066
|
+
try {
|
|
3067
|
+
const entries = await fs.readdir(tasksDir, { withFileTypes: true });
|
|
3068
|
+
for (const entry of entries) {
|
|
3069
|
+
if (!entry.isDirectory() || entry.name === 'archive')
|
|
3070
|
+
continue;
|
|
3071
|
+
const tasksPath = path.join(tasksDir, entry.name, 'tasks.md');
|
|
3072
|
+
if (!existsSync(tasksPath))
|
|
3073
|
+
continue;
|
|
3074
|
+
const content = await fs.readFile(tasksPath, 'utf-8');
|
|
3075
|
+
const pending = content
|
|
3076
|
+
.split('\n')
|
|
3077
|
+
.filter((l) => l.match(/^- \[ \]/))
|
|
3078
|
+
.map((l) => l.replace(/^- \[ \]\s*/, '').trim())
|
|
3079
|
+
.slice(0, 3);
|
|
3080
|
+
if (pending.length > 0) {
|
|
3081
|
+
taskSummaries.push(`**${entry.name}** (${pending.length}+ pending):`);
|
|
3082
|
+
pending.forEach((p) => taskSummaries.push(` - ${p}`));
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
catch {
|
|
3087
|
+
// ignore
|
|
3088
|
+
}
|
|
3089
|
+
if (taskSummaries.length > 0) {
|
|
3090
|
+
sections.push('## Pending Tasks\n' + taskSummaries.join('\n'));
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
// ── 3. Recent git commits ──
|
|
3094
|
+
try {
|
|
3095
|
+
const { stdout } = await execAsync('git log --oneline -8', { cwd });
|
|
3096
|
+
if (stdout.trim()) {
|
|
3097
|
+
sections.push('## Recent Commits\n```\n' + stdout.trim() + '\n```');
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
catch {
|
|
3101
|
+
// not a git repo or git not available
|
|
3102
|
+
}
|
|
3103
|
+
// ── 4. Ralph status ──
|
|
3104
|
+
const ralphStatePath = path.join(cwd, '.rulebook', 'ralph', 'state.json');
|
|
3105
|
+
if (existsSync(ralphStatePath)) {
|
|
3106
|
+
try {
|
|
3107
|
+
const state = JSON.parse(await fs.readFile(ralphStatePath, 'utf-8'));
|
|
3108
|
+
if (state.enabled) {
|
|
3109
|
+
const prdPath = path.join(cwd, '.rulebook', 'ralph', 'prd.json');
|
|
3110
|
+
let prdInfo = '';
|
|
3111
|
+
if (existsSync(prdPath)) {
|
|
3112
|
+
const prd = JSON.parse(await fs.readFile(prdPath, 'utf-8'));
|
|
3113
|
+
const pending = (prd.userStories ?? []).filter((s) => !s.passes).length;
|
|
3114
|
+
const total = (prd.userStories ?? []).length;
|
|
3115
|
+
prdInfo = ` | ${total - pending}/${total} stories complete`;
|
|
3116
|
+
}
|
|
3117
|
+
sections.push(`## Ralph Status\n` +
|
|
3118
|
+
`Iteration ${state.current_iteration}/${state.max_iterations}${prdInfo} | Tool: ${state.tool} | Paused: ${state.paused}`);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
catch {
|
|
3122
|
+
// ignore
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
// ── 5. Current branch ──
|
|
3126
|
+
try {
|
|
3127
|
+
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd });
|
|
3128
|
+
const branch = stdout.trim();
|
|
3129
|
+
if (branch) {
|
|
3130
|
+
sections.unshift(`## Branch\n\`${branch}\``);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
catch {
|
|
3134
|
+
// ignore
|
|
3135
|
+
}
|
|
3136
|
+
// ── Render ──
|
|
3137
|
+
if (sections.length === 0) {
|
|
3138
|
+
console.log(chalk.yellow('No session context found. Create tasks, a PLANS.md, or make some commits.'));
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
const output = [
|
|
3142
|
+
'─'.repeat(60),
|
|
3143
|
+
chalk.bold('📋 SESSION CONTINUITY CONTEXT'),
|
|
3144
|
+
chalk.gray('Paste this at the start of a new AI session:'),
|
|
3145
|
+
'─'.repeat(60),
|
|
3146
|
+
'',
|
|
3147
|
+
sections.join('\n\n'),
|
|
3148
|
+
'',
|
|
3149
|
+
'─'.repeat(60),
|
|
3150
|
+
].join('\n');
|
|
3151
|
+
console.log(output);
|
|
3152
|
+
// Also write to PLANS.md if it exists
|
|
3153
|
+
if (plans !== null) {
|
|
3154
|
+
const { appendPlansHistory } = await import('../core/plans-manager.js');
|
|
3155
|
+
try {
|
|
3156
|
+
await appendPlansHistory(cwd, `Session context generated. Branch: current. Pending tasks summarized.`);
|
|
3157
|
+
}
|
|
3158
|
+
catch {
|
|
3159
|
+
// non-critical
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
2621
3163
|
export async function migrateMemoryDirectory() {
|
|
2622
3164
|
const oraModule = await import('ora');
|
|
2623
3165
|
const ora = oraModule.default;
|
|
@@ -2670,4 +3212,128 @@ export async function migrateMemoryDirectory() {
|
|
|
2670
3212
|
process.exit(1);
|
|
2671
3213
|
}
|
|
2672
3214
|
}
|
|
3215
|
+
// ─── Review Command ─────────────────────────────────────────────────────────
|
|
3216
|
+
/**
|
|
3217
|
+
* `rulebook review` — Run AI-powered code review on changes vs a base branch.
|
|
3218
|
+
*
|
|
3219
|
+
* Retrieves the git diff, builds a structured prompt, sends it to an AI tool,
|
|
3220
|
+
* parses the result, and outputs in the requested format.
|
|
3221
|
+
*/
|
|
3222
|
+
export async function reviewCommand(options) {
|
|
3223
|
+
const cwd = process.cwd();
|
|
3224
|
+
const baseBranch = options.baseBranch ?? 'main';
|
|
3225
|
+
const outputFormat = options.output ?? 'terminal';
|
|
3226
|
+
const tool = (options.tool ?? 'claude');
|
|
3227
|
+
const { getDiffContext, buildReviewPrompt, runAIReview, parseReviewOutput, formatReviewTerminal, postGitHubComment, readAgentsMd, hasFailingIssues, } = await import('../core/review-manager.js');
|
|
3228
|
+
// 1. Get diff
|
|
3229
|
+
const diff = await getDiffContext(cwd, baseBranch);
|
|
3230
|
+
if (!diff) {
|
|
3231
|
+
console.log(chalk.yellow(`No changes detected vs ${baseBranch}`));
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
// 2. Read AGENTS.md (optional context)
|
|
3235
|
+
const agentsMdContent = await readAgentsMd(cwd);
|
|
3236
|
+
// 3. Build prompt
|
|
3237
|
+
const projectName = path.basename(cwd);
|
|
3238
|
+
const prompt = buildReviewPrompt(diff, { agentsMdContent, projectName });
|
|
3239
|
+
// 4. Run AI review
|
|
3240
|
+
const spinner = ora('Running AI review...').start();
|
|
3241
|
+
const rawOutput = await runAIReview(prompt, tool);
|
|
3242
|
+
if (!rawOutput) {
|
|
3243
|
+
spinner.fail('AI review returned no output. Is the AI tool installed and configured?');
|
|
3244
|
+
process.exit(1);
|
|
3245
|
+
}
|
|
3246
|
+
spinner.succeed('AI review complete');
|
|
3247
|
+
// 5. Parse result
|
|
3248
|
+
const result = parseReviewOutput(rawOutput);
|
|
3249
|
+
// 6. Output
|
|
3250
|
+
switch (outputFormat) {
|
|
3251
|
+
case 'terminal':
|
|
3252
|
+
console.log(formatReviewTerminal(result));
|
|
3253
|
+
break;
|
|
3254
|
+
case 'json':
|
|
3255
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3256
|
+
break;
|
|
3257
|
+
case 'github-comment':
|
|
3258
|
+
try {
|
|
3259
|
+
await postGitHubComment(result);
|
|
3260
|
+
console.log(chalk.green('Review posted as PR comment'));
|
|
3261
|
+
}
|
|
3262
|
+
catch (error) {
|
|
3263
|
+
console.error(chalk.red(`Failed to post comment: ${error}`));
|
|
3264
|
+
process.exit(1);
|
|
3265
|
+
}
|
|
3266
|
+
break;
|
|
3267
|
+
}
|
|
3268
|
+
// 7. Exit code
|
|
3269
|
+
if (options.failOn && hasFailingIssues(result.issues, options.failOn)) {
|
|
3270
|
+
console.log(chalk.red(`\nFailing: found issues at or above "${options.failOn}" severity`));
|
|
3271
|
+
process.exit(1);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
// ─── Ralph Import Issues Command ───
|
|
3275
|
+
export async function ralphImportIssuesCommand(options) {
|
|
3276
|
+
const oraModule = await import('ora');
|
|
3277
|
+
const ora = oraModule.default;
|
|
3278
|
+
try {
|
|
3279
|
+
const { checkGhCliAvailable, fetchGithubIssues, convertIssueToStory, mergeStoriesIntoExistingPrd, } = await import('../core/github-issues-importer.js');
|
|
3280
|
+
// 1. Check gh CLI availability
|
|
3281
|
+
const ghAvailable = await checkGhCliAvailable();
|
|
3282
|
+
if (!ghAvailable) {
|
|
3283
|
+
console.error(chalk.red('GitHub CLI (gh) is not installed. Install it from: https://cli.github.com'));
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
3286
|
+
// 2. Fetch issues
|
|
3287
|
+
const spinner = ora('Fetching GitHub issues...').start();
|
|
3288
|
+
const issues = await fetchGithubIssues({
|
|
3289
|
+
label: options.label,
|
|
3290
|
+
milestone: options.milestone,
|
|
3291
|
+
limit: options.limit ?? 20,
|
|
3292
|
+
});
|
|
3293
|
+
if (issues.length === 0) {
|
|
3294
|
+
spinner.info('No open issues found matching the given filters.');
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
spinner.text = `Converting ${issues.length} issues to Ralph stories...`;
|
|
3298
|
+
// 3. Load existing PRD (may not exist yet)
|
|
3299
|
+
const cwd = process.cwd();
|
|
3300
|
+
let existingPrd = null;
|
|
3301
|
+
try {
|
|
3302
|
+
const { RalphManager } = await import('../core/ralph-manager.js');
|
|
3303
|
+
const { Logger } = await import('../core/logger.js');
|
|
3304
|
+
const logger = new Logger(cwd);
|
|
3305
|
+
const manager = new RalphManager(cwd, logger);
|
|
3306
|
+
existingPrd = await manager.loadPRD();
|
|
3307
|
+
}
|
|
3308
|
+
catch {
|
|
3309
|
+
// PRD not initialized — will create a new one
|
|
3310
|
+
}
|
|
3311
|
+
// 4. Convert issues to stories
|
|
3312
|
+
const newStories = issues.map((issue) => convertIssueToStory(issue));
|
|
3313
|
+
// 5. Merge into existing PRD
|
|
3314
|
+
const { prd: mergedPrd, result } = mergeStoriesIntoExistingPrd(existingPrd, newStories);
|
|
3315
|
+
// 6. Dry run — preview only
|
|
3316
|
+
if (options.dryRun) {
|
|
3317
|
+
spinner.stop();
|
|
3318
|
+
console.log(chalk.yellow(`Dry run — would import ${result.imported} stories, update ${result.updated}, skip ${result.skipped}`));
|
|
3319
|
+
console.log('');
|
|
3320
|
+
for (const story of mergedPrd.userStories) {
|
|
3321
|
+
const marker = story.passes ? chalk.green('[PASS]') : chalk.gray('[ ]');
|
|
3322
|
+
console.log(` ${marker} ${story.id}: ${story.title}`);
|
|
3323
|
+
}
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
// 7. Save PRD
|
|
3327
|
+
const prdPath = path.join(cwd, '.rulebook', 'ralph', 'prd.json');
|
|
3328
|
+
await ensureDir(path.join(cwd, '.rulebook', 'ralph'));
|
|
3329
|
+
await writeFile(prdPath, JSON.stringify(mergedPrd, null, 2));
|
|
3330
|
+
spinner.succeed(`Imported ${result.imported} new stories, updated ${result.updated} existing, ${result.skipped} skipped`);
|
|
3331
|
+
console.log(`\n PRD saved to: ${prdPath}`);
|
|
3332
|
+
console.log(` Total stories: ${mergedPrd.userStories.length}\n`);
|
|
3333
|
+
}
|
|
3334
|
+
catch (error) {
|
|
3335
|
+
console.error(chalk.red(`Failed to import GitHub issues: ${String(error)}`));
|
|
3336
|
+
process.exit(1);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
2673
3339
|
//# sourceMappingURL=commands.js.map
|