@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.
Files changed (162) hide show
  1. package/.claude/commands/continue.md +33 -0
  2. package/.claude-plugin/marketplace.json +28 -29
  3. package/.claude-plugin/plugin.json +8 -8
  4. package/README.md +32 -144
  5. package/dist/agents/ralph-parser.d.ts +44 -5
  6. package/dist/agents/ralph-parser.d.ts.map +1 -1
  7. package/dist/agents/ralph-parser.js +218 -26
  8. package/dist/agents/ralph-parser.js.map +1 -1
  9. package/dist/cli/commands.d.ts +65 -0
  10. package/dist/cli/commands.d.ts.map +1 -1
  11. package/dist/cli/commands.js +841 -175
  12. package/dist/cli/commands.js.map +1 -1
  13. package/dist/core/agent-manager.d.ts +12 -32
  14. package/dist/core/agent-manager.d.ts.map +1 -1
  15. package/dist/core/agent-manager.js +150 -220
  16. package/dist/core/agent-manager.js.map +1 -1
  17. package/dist/core/claude-mcp.d.ts +17 -0
  18. package/dist/core/claude-mcp.d.ts.map +1 -1
  19. package/dist/core/claude-mcp.js +90 -6
  20. package/dist/core/claude-mcp.js.map +1 -1
  21. package/dist/core/cli-bridge.js +1 -1
  22. package/dist/core/cli-bridge.js.map +1 -1
  23. package/dist/core/config-manager.d.ts.map +1 -1
  24. package/dist/core/config-manager.js +40 -0
  25. package/dist/core/config-manager.js.map +1 -1
  26. package/dist/core/cursor-mdc-generator.d.ts +30 -0
  27. package/dist/core/cursor-mdc-generator.d.ts.map +1 -0
  28. package/dist/core/cursor-mdc-generator.js +98 -0
  29. package/dist/core/cursor-mdc-generator.js.map +1 -0
  30. package/dist/core/detector.d.ts +25 -1
  31. package/dist/core/detector.d.ts.map +1 -1
  32. package/dist/core/detector.js +321 -1
  33. package/dist/core/detector.js.map +1 -1
  34. package/dist/core/generator.d.ts +10 -0
  35. package/dist/core/generator.d.ts.map +1 -1
  36. package/dist/core/generator.js +182 -9
  37. package/dist/core/generator.js.map +1 -1
  38. package/dist/core/github-issues-importer.d.ts +82 -0
  39. package/dist/core/github-issues-importer.d.ts.map +1 -0
  40. package/dist/core/github-issues-importer.js +161 -0
  41. package/dist/core/github-issues-importer.js.map +1 -0
  42. package/dist/core/health-scorer.d.ts +39 -0
  43. package/dist/core/health-scorer.d.ts.map +1 -1
  44. package/dist/core/health-scorer.js +256 -13
  45. package/dist/core/health-scorer.js.map +1 -1
  46. package/dist/core/iteration-tracker.d.ts +28 -0
  47. package/dist/core/iteration-tracker.d.ts.map +1 -1
  48. package/dist/core/iteration-tracker.js +86 -0
  49. package/dist/core/iteration-tracker.js.map +1 -1
  50. package/dist/core/logger.js +1 -1
  51. package/dist/core/logger.js.map +1 -1
  52. package/dist/core/migrator.js +1 -1
  53. package/dist/core/migrator.js.map +1 -1
  54. package/dist/core/modern-console.d.ts +1 -2
  55. package/dist/core/modern-console.d.ts.map +1 -1
  56. package/dist/core/modern-console.js +6 -18
  57. package/dist/core/modern-console.js.map +1 -1
  58. package/dist/core/multi-tool-generator.d.ts +59 -0
  59. package/dist/core/multi-tool-generator.d.ts.map +1 -0
  60. package/dist/core/multi-tool-generator.js +157 -0
  61. package/dist/core/multi-tool-generator.js.map +1 -0
  62. package/dist/core/override-manager.d.ts +23 -0
  63. package/dist/core/override-manager.d.ts.map +1 -0
  64. package/dist/core/override-manager.js +82 -0
  65. package/dist/core/override-manager.js.map +1 -0
  66. package/dist/core/plans-manager.d.ts +46 -0
  67. package/dist/core/plans-manager.d.ts.map +1 -0
  68. package/dist/core/plans-manager.js +158 -0
  69. package/dist/core/plans-manager.js.map +1 -0
  70. package/dist/core/prd-generator.d.ts +12 -0
  71. package/dist/core/prd-generator.d.ts.map +1 -1
  72. package/dist/core/prd-generator.js +91 -2
  73. package/dist/core/prd-generator.js.map +1 -1
  74. package/dist/core/ralph-manager.d.ts +81 -1
  75. package/dist/core/ralph-manager.d.ts.map +1 -1
  76. package/dist/core/ralph-manager.js +214 -4
  77. package/dist/core/ralph-manager.js.map +1 -1
  78. package/dist/core/ralph-parallel.d.ts +55 -0
  79. package/dist/core/ralph-parallel.d.ts.map +1 -0
  80. package/dist/core/ralph-parallel.js +201 -0
  81. package/dist/core/ralph-parallel.js.map +1 -0
  82. package/dist/core/ralph-plan-checkpoint.d.ts +58 -0
  83. package/dist/core/ralph-plan-checkpoint.d.ts.map +1 -0
  84. package/dist/core/ralph-plan-checkpoint.js +154 -0
  85. package/dist/core/ralph-plan-checkpoint.js.map +1 -0
  86. package/dist/core/ralph-scripts.d.ts +12 -0
  87. package/dist/core/ralph-scripts.d.ts.map +1 -0
  88. package/dist/core/ralph-scripts.js +49 -0
  89. package/dist/core/ralph-scripts.js.map +1 -0
  90. package/dist/core/review-manager.d.ts +74 -0
  91. package/dist/core/review-manager.d.ts.map +1 -0
  92. package/dist/core/review-manager.js +371 -0
  93. package/dist/core/review-manager.js.map +1 -0
  94. package/dist/core/task-manager.d.ts +1 -1
  95. package/dist/core/task-manager.js +1 -1
  96. package/dist/core/workflow-generator.js +1 -1
  97. package/dist/core/workflow-generator.js.map +1 -1
  98. package/dist/index.js +96 -4
  99. package/dist/index.js.map +1 -1
  100. package/dist/mcp/rulebook-server.d.ts.map +1 -1
  101. package/dist/mcp/rulebook-server.js +300 -164
  102. package/dist/mcp/rulebook-server.js.map +1 -1
  103. package/dist/memory/memory-store.d.ts.map +1 -1
  104. package/dist/memory/memory-store.js +4 -0
  105. package/dist/memory/memory-store.js.map +1 -1
  106. package/dist/types.d.ts +55 -34
  107. package/dist/types.d.ts.map +1 -1
  108. package/package.json +1 -1
  109. package/templates/agents/implementer.md +35 -0
  110. package/templates/agents/researcher.md +34 -0
  111. package/templates/agents/team-lead.md +34 -0
  112. package/templates/agents/tester.md +42 -0
  113. package/templates/ci/rulebook-review.yml +26 -0
  114. package/templates/core/AGENTS_LEAN.md +25 -0
  115. package/templates/core/AGENTS_OVERRIDE.md +16 -0
  116. package/templates/core/MULTI_AGENT.md +74 -0
  117. package/templates/core/PLANS.md +28 -0
  118. package/templates/core/RALPH.md +45 -4
  119. package/templates/ides/CONTINUE_RULES.md +16 -0
  120. package/templates/ides/COPILOT_INSTRUCTIONS.md +23 -0
  121. package/templates/ides/GEMINI_RULES.md +17 -0
  122. package/templates/ides/WINDSURF_RULES.md +14 -0
  123. package/templates/ides/cursor-mdc/go.mdc +24 -0
  124. package/templates/ides/cursor-mdc/python.mdc +24 -0
  125. package/templates/ides/cursor-mdc/quality.mdc +25 -0
  126. package/templates/ides/cursor-mdc/ralph.mdc +39 -0
  127. package/templates/ides/cursor-mdc/rulebook.mdc +38 -0
  128. package/templates/ides/cursor-mdc/rust.mdc +24 -0
  129. package/templates/ides/cursor-mdc/typescript.mdc +25 -0
  130. package/templates/modules/sequential-thinking.md +42 -0
  131. package/templates/ralph/ralph-history.bat +4 -0
  132. package/templates/ralph/ralph-history.sh +5 -0
  133. package/templates/ralph/ralph-init.bat +5 -0
  134. package/templates/ralph/ralph-init.sh +5 -0
  135. package/templates/ralph/ralph-pause.bat +5 -0
  136. package/templates/ralph/ralph-pause.sh +5 -0
  137. package/templates/ralph/ralph-run.bat +5 -0
  138. package/templates/ralph/ralph-run.sh +5 -0
  139. package/templates/ralph/ralph-status.bat +4 -0
  140. package/templates/ralph/ralph-status.sh +5 -0
  141. package/templates/services/DATADOG.md +26 -0
  142. package/templates/services/DOCKER.md +124 -0
  143. package/templates/services/DOCKER_COMPOSE.md +168 -0
  144. package/templates/services/HELM.md +194 -0
  145. package/templates/services/KUBERNETES.md +208 -0
  146. package/templates/services/OPENTELEMETRY.md +25 -0
  147. package/templates/services/PINO.md +24 -0
  148. package/templates/services/PROMETHEUS.md +33 -0
  149. package/templates/services/SENTRY.md +23 -0
  150. package/templates/services/WINSTON.md +30 -0
  151. package/dist/core/openspec-manager.d.ts +0 -133
  152. package/dist/core/openspec-manager.d.ts.map +0 -1
  153. package/dist/core/openspec-manager.js +0 -596
  154. package/dist/core/openspec-manager.js.map +0 -1
  155. package/dist/core/openspec-migrator.d.ts +0 -27
  156. package/dist/core/openspec-migrator.d.ts.map +0 -1
  157. package/dist/core/openspec-migrator.js +0 -262
  158. package/dist/core/openspec-migrator.js.map +0 -1
  159. package/dist/core/test-task-manager.d.ts +0 -49
  160. package/dist/core/test-task-manager.d.ts.map +0 -1
  161. package/dist/core/test-task-manager.js +0 -121
  162. package/dist/core/test-task-manager.js.map +0 -1
@@ -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 cliQuick = Boolean(options.quick);
91
- if (options.yes) {
92
- // Full auto mode - no prompts at all
93
- config = {
94
- languages: detection.languages.map((l) => l.language),
95
- modules: cliMinimal ? [] : detection.modules.filter((m) => m.detected).map((m) => m.module),
96
- frameworks: detection.frameworks.filter((f) => f.detected).map((f) => f.framework),
97
- ides: cliMinimal ? [] : ['cursor'],
98
- projectType: 'application',
99
- coverageThreshold: 95,
100
- strictDocs: true,
101
- generateWorkflows: true,
102
- includeGitWorkflow: true,
103
- gitPushMode: 'manual',
104
- installGitHooks: false,
105
- minimal: cliMinimal,
106
- lightMode: cliLight,
107
- modular: true, // Enable modular /rulebook directory by default
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
- const strategy = options.yes ? 'merge' : await promptMergeStrategy();
240
- if (strategy === 'merge') {
241
- const mergeSpinner = ora('Merging with existing AGENTS.md...').start();
242
- finalContent = await mergeFullAgents(detection.existingAgents, config, cwd);
243
- // Create backup
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, getHealthGrade } = await import('../core/health-scorer.js');
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
- const grade = getHealthGrade(health.overall);
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. Check Context7 MCP for OpenSpec format requirements'));
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 OpenSpec commands are no longer supported.'));
1101
- console.log(chalk.yellow('Please migrate to the new Rulebook task system.'));
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
- // Migrate OpenSpec tasks to Rulebook format (if OpenSpec exists)
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
- const prompt = ralphBuildPrompt(task, prd);
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('npm', ['test']),
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