@hivehub/rulebook 3.3.1 → 3.4.2

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 (42) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/agents/ralph-parser.d.ts +8 -9
  4. package/dist/agents/ralph-parser.d.ts.map +1 -1
  5. package/dist/agents/ralph-parser.js +37 -33
  6. package/dist/agents/ralph-parser.js.map +1 -1
  7. package/dist/cli/commands.d.ts.map +1 -1
  8. package/dist/cli/commands.js +4 -114
  9. package/dist/cli/commands.js.map +1 -1
  10. package/dist/core/agent-manager.d.ts +7 -34
  11. package/dist/core/agent-manager.d.ts.map +1 -1
  12. package/dist/core/agent-manager.js +7 -224
  13. package/dist/core/agent-manager.js.map +1 -1
  14. package/dist/core/cli-bridge.js +1 -1
  15. package/dist/core/cli-bridge.js.map +1 -1
  16. package/dist/core/generator.d.ts.map +1 -1
  17. package/dist/core/generator.js +5 -6
  18. package/dist/core/generator.js.map +1 -1
  19. package/dist/core/logger.js +1 -1
  20. package/dist/core/logger.js.map +1 -1
  21. package/dist/core/migrator.js +1 -1
  22. package/dist/core/migrator.js.map +1 -1
  23. package/dist/core/modern-console.d.ts +1 -2
  24. package/dist/core/modern-console.d.ts.map +1 -1
  25. package/dist/core/modern-console.js +6 -18
  26. package/dist/core/modern-console.js.map +1 -1
  27. package/dist/core/ralph-manager.d.ts +34 -0
  28. package/dist/core/ralph-manager.d.ts.map +1 -1
  29. package/dist/core/ralph-manager.js +107 -4
  30. package/dist/core/ralph-manager.js.map +1 -1
  31. package/dist/core/task-manager.d.ts +1 -1
  32. package/dist/core/task-manager.js +1 -1
  33. package/dist/core/workflow-generator.js +1 -1
  34. package/dist/core/workflow-generator.js.map +1 -1
  35. package/dist/index.js +2 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/mcp/rulebook-server.d.ts.map +1 -1
  38. package/dist/mcp/rulebook-server.js +291 -162
  39. package/dist/mcp/rulebook-server.js.map +1 -1
  40. package/dist/types.d.ts +0 -32
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +1 -1
@@ -781,11 +781,13 @@ export async function startRulebookMcpServer() {
781
781
  const configData = await configManager.loadConfig();
782
782
  const maxIterations = configData.ralph?.maxIterations || 10;
783
783
  const tool = (configData.ralph?.tool || 'claude');
784
- await ralphManager.initialize(maxIterations, tool);
784
+ // Generate PRD first, then initialize with correct task count
785
785
  const prd = await prdGenerator.generatePRD(basename(config.projectRoot) || 'project');
786
786
  const { writeFile } = await import('../utils/file-system.js');
787
787
  const prdPath = join(config.projectRoot, '.rulebook', 'ralph', 'prd.json');
788
788
  await writeFile(prdPath, JSON.stringify(prd, null, 2));
789
+ // Initialize after PRD is written so task count is correct
790
+ await ralphManager.initialize(maxIterations, tool);
789
791
  return {
790
792
  content: [
791
793
  {
@@ -826,182 +828,297 @@ export async function startRulebookMcpServer() {
826
828
  const { RalphManager } = await import('../core/ralph-manager.js');
827
829
  const { RalphParser } = await import('../agents/ralph-parser.js');
828
830
  const { spawn } = await import('child_process');
831
+ const { execSync } = await import('child_process');
829
832
  const logger = new Logger(config.projectRoot);
830
833
  const ralphManager = new RalphManager(config.projectRoot, logger);
831
834
  const configData = await configManager.loadConfig();
832
835
  const maxIterations = args.maxIterations || configData.ralph?.maxIterations || 10;
833
836
  const tool = (args.tool || configData.ralph?.tool || 'claude');
834
- // Resume existing state if available, otherwise initialize fresh
835
- const existingState = await ralphManager.getStatus();
836
- if (!existingState) {
837
- await ralphManager.initialize(maxIterations, tool);
837
+ // ── Concurrency guard: prevent multiple simultaneous Ralph runs ──
838
+ const lockAcquired = await ralphManager.acquireLock(tool);
839
+ if (!lockAcquired) {
840
+ const lockInfo = await ralphManager.getLockInfo();
841
+ return {
842
+ content: [
843
+ {
844
+ type: 'text',
845
+ text: JSON.stringify({
846
+ success: false,
847
+ error: `Ralph is already running (PID ${lockInfo?.pid}, started ${lockInfo?.startedAt}, task: ${lockInfo?.currentTask || 'starting'}, iteration: ${lockInfo?.iteration || 0}). Wait for it to finish or check ralph_status. Do NOT start another run.`,
848
+ }),
849
+ },
850
+ ],
851
+ };
838
852
  }
839
- // Helper: run a shell command and return stdout
840
- const runCmd = (cmd, cmdArgs) => new Promise((resolve) => {
841
- let stdout = '';
842
- let stderr = '';
843
- const proc = spawn(cmd, cmdArgs, {
844
- cwd: config.projectRoot,
845
- shell: true,
846
- stdio: ['pipe', 'pipe', 'pipe'],
847
- });
848
- proc.stdout?.on('data', (d) => {
849
- stdout += d.toString();
850
- });
851
- proc.stderr?.on('data', (d) => {
852
- stderr += d.toString();
853
- });
854
- proc.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
855
- proc.on('error', (err) => resolve({ code: 1, stdout, stderr: err.message }));
856
- });
857
- // Helper: build prompt for AI agent
858
- const buildPrompt = (task, prd) => {
859
- const criteria = (task.acceptanceCriteria || [])
860
- .map((c) => `- ${c}`)
861
- .join('\n');
862
- return [
863
- `You are working on project: ${prd?.project || 'unknown'}`,
864
- ``,
865
- `## Current Task: ${task.title}`,
866
- `ID: ${task.id}`,
867
- ``,
868
- `## Description`,
869
- task.description,
870
- ``,
871
- `## Acceptance Criteria`,
872
- criteria,
873
- ``,
874
- task.notes ? `## Notes\n${task.notes}\n` : '',
875
- `## Instructions`,
876
- `1. Implement the changes described above`,
877
- `2. Ensure all acceptance criteria are met`,
878
- `3. Run quality checks: type-check, lint, tests`,
879
- `4. Fix any issues found by quality checks`,
880
- `5. When done, summarize what was changed`,
881
- ]
882
- .filter(Boolean)
883
- .join('\n');
853
+ // Ensure lock is released on exit, even on crashes
854
+ const cleanupLock = async () => {
855
+ await ralphManager.releaseLock();
884
856
  };
885
- // Helper: execute AI agent
886
- const executeAgent = (agentTool, prompt) => new Promise((resolve, reject) => {
887
- let output = '';
888
- const toolCmds = {
889
- claude: {
890
- cmd: 'claude',
891
- args: ['-p', '--dangerously-skip-permissions', '--verbose'],
892
- stdinPrompt: true,
893
- },
894
- amp: { cmd: 'amp', args: ['-p', prompt], stdinPrompt: false },
895
- gemini: { cmd: 'gemini', args: ['-p', prompt], stdinPrompt: false },
857
+ process.on('SIGTERM', cleanupLock);
858
+ process.on('SIGINT', cleanupLock);
859
+ try {
860
+ // Validate tool is available before starting
861
+ const toolCmdNames = {
862
+ claude: 'claude',
863
+ amp: 'amp',
864
+ gemini: 'gemini',
896
865
  };
897
- const cfg = toolCmds[agentTool] || toolCmds.claude;
898
- const proc = spawn(cfg.cmd, cfg.args, {
899
- cwd: config.projectRoot,
900
- shell: true,
901
- stdio: ['pipe', 'pipe', 'pipe'],
902
- });
903
- if (cfg.stdinPrompt && proc.stdin) {
904
- proc.stdin.write(prompt);
905
- proc.stdin.end();
866
+ const toolCmd = toolCmdNames[tool] || 'claude';
867
+ try {
868
+ execSync(`${toolCmd} --version`, { stdio: 'pipe', timeout: 10000 });
869
+ }
870
+ catch {
871
+ return {
872
+ content: [
873
+ {
874
+ type: 'text',
875
+ text: JSON.stringify({
876
+ success: false,
877
+ error: `CLI tool "${toolCmd}" not found or not responding. Install it first: https://docs.anthropic.com/claude-code`,
878
+ }),
879
+ },
880
+ ],
881
+ };
882
+ }
883
+ // Resume existing state if available, otherwise initialize fresh
884
+ const existingState = await ralphManager.getStatus();
885
+ if (!existingState) {
886
+ await ralphManager.initialize(maxIterations, tool);
906
887
  }
907
- proc.stdout?.on('data', (d) => {
908
- output += d.toString();
888
+ // Helper: run a shell command and return stdout
889
+ const runCmd = (cmd, cmdArgs) => new Promise((resolve) => {
890
+ let stdout = '';
891
+ let stderr = '';
892
+ const proc = spawn(cmd, cmdArgs, {
893
+ cwd: config.projectRoot,
894
+ shell: true,
895
+ stdio: ['pipe', 'pipe', 'pipe'],
896
+ });
897
+ proc.stdout?.on('data', (d) => {
898
+ stdout += d.toString();
899
+ });
900
+ proc.stderr?.on('data', (d) => {
901
+ stderr += d.toString();
902
+ });
903
+ proc.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
904
+ proc.on('error', (err) => resolve({ code: 1, stdout, stderr: err.message }));
909
905
  });
910
- proc.stderr?.on('data', () => { });
911
- proc.on('close', (code) => {
912
- if (code === 0 || output.length > 0)
913
- resolve(output);
914
- else
915
- reject(new Error(`Agent ${agentTool} exited with code ${code}`));
906
+ // Helper: build prompt for AI agent
907
+ const buildPrompt = (task, projectName) => {
908
+ const criteria = (task.acceptanceCriteria || [])
909
+ .map((c) => `- ${c}`)
910
+ .join('\n');
911
+ return [
912
+ `You are working on project: ${projectName}`,
913
+ ``,
914
+ `## Current Task: ${task.title}`,
915
+ `ID: ${task.id}`,
916
+ ``,
917
+ `## Description`,
918
+ task.description,
919
+ ``,
920
+ `## Acceptance Criteria`,
921
+ criteria,
922
+ ``,
923
+ task.notes ? `## Notes\n${task.notes}\n` : '',
924
+ `## Instructions`,
925
+ `1. Implement the changes described above`,
926
+ `2. Ensure all acceptance criteria are met`,
927
+ `3. Run quality checks: type-check, lint, tests`,
928
+ `4. Fix any issues found by quality checks`,
929
+ `5. When done, summarize what was changed`,
930
+ ]
931
+ .filter(Boolean)
932
+ .join('\n');
933
+ };
934
+ // Helper: execute AI agent with proper error handling
935
+ const executeAgent = (agentTool, prompt) => new Promise((resolve, reject) => {
936
+ let output = '';
937
+ let stderrOutput = '';
938
+ const toolCmds = {
939
+ claude: {
940
+ cmd: 'claude',
941
+ args: ['-p', '--dangerously-skip-permissions', '--verbose'],
942
+ stdinPrompt: true,
943
+ },
944
+ amp: { cmd: 'amp', args: ['-p', prompt], stdinPrompt: false },
945
+ gemini: { cmd: 'gemini', args: ['-p', prompt], stdinPrompt: false },
946
+ };
947
+ const cfg = toolCmds[agentTool] || toolCmds.claude;
948
+ let settled = false;
949
+ const settle = (fn) => {
950
+ if (!settled) {
951
+ settled = true;
952
+ fn();
953
+ }
954
+ };
955
+ const proc = spawn(cfg.cmd, cfg.args, {
956
+ cwd: config.projectRoot,
957
+ shell: true,
958
+ stdio: ['pipe', 'pipe', 'pipe'],
959
+ });
960
+ if (cfg.stdinPrompt && proc.stdin) {
961
+ proc.stdin.write(prompt);
962
+ proc.stdin.end();
963
+ }
964
+ proc.stdout?.on('data', (d) => {
965
+ output += d.toString();
966
+ });
967
+ proc.stderr?.on('data', (d) => {
968
+ stderrOutput += d.toString();
969
+ });
970
+ proc.on('close', (code) => {
971
+ settle(() => {
972
+ if (code === 0 || output.length > 0) {
973
+ resolve(output);
974
+ }
975
+ else {
976
+ reject(new Error(`Agent ${agentTool} exited with code ${code}${stderrOutput ? ': ' + stderrOutput.slice(0, 500) : ''}`));
977
+ }
978
+ });
979
+ });
980
+ proc.on('error', (err) => {
981
+ settle(() => reject(new Error(`Failed to spawn ${agentTool}: ${err.message}`)));
982
+ });
983
+ const timeout = setTimeout(() => {
984
+ proc.kill('SIGTERM');
985
+ settle(() => resolve(output || `Agent ${agentTool} timed out after 10 minutes`));
986
+ }, 600000);
987
+ proc.on('close', () => clearTimeout(timeout));
916
988
  });
917
- proc.on('error', (err) => reject(err));
918
- setTimeout(() => {
919
- proc.kill('SIGTERM');
920
- resolve(output || 'Agent timed out');
921
- }, 600000);
922
- });
923
- // Sync task count from PRD
924
- await ralphManager.refreshTaskCount();
925
- const prd = await ralphManager.loadPRD();
926
- let iterationCount = 0;
927
- while (ralphManager.canContinue() && iterationCount < maxIterations) {
928
- iterationCount++;
929
- const task = await ralphManager.getNextTask();
930
- if (!task)
931
- break;
932
- const startTime = Date.now();
933
- // 1. Execute AI agent
934
- let agentOutput = '';
935
- try {
936
- const prompt = buildPrompt(task, prd);
937
- agentOutput = await executeAgent(tool, prompt);
938
- }
939
- catch (agentErr) {
940
- agentOutput = `Error: ${agentErr.message || agentErr}`;
989
+ // Sync task count from PRD
990
+ await ralphManager.refreshTaskCount();
991
+ // Load PRD for project name (used in prompts)
992
+ const prd = await ralphManager.loadPRD();
993
+ if (!prd || !prd.userStories || prd.userStories.length === 0) {
994
+ return {
995
+ content: [
996
+ {
997
+ type: 'text',
998
+ text: JSON.stringify({
999
+ success: false,
1000
+ error: 'No PRD found or no user stories. Run rulebook_ralph_init first.',
1001
+ }),
1002
+ },
1003
+ ],
1004
+ };
941
1005
  }
942
- // 2. Run quality gates
943
- const [typeCheck, lint, tests] = await Promise.all([
944
- runCmd('npm', ['run', 'type-check']).then((r) => r.code === 0),
945
- runCmd('npm', ['run', 'lint']).then((r) => r.code === 0),
946
- runCmd('npm', ['test']).then((r) => r.code === 0),
947
- ]);
948
- const qualityChecks = { type_check: typeCheck, lint, tests, coverage_met: tests };
949
- const allPass = typeCheck && lint && tests;
950
- const passCount = Object.values(qualityChecks).filter(Boolean).length;
951
- const status = allPass
952
- ? 'success'
953
- : passCount >= 2
954
- ? 'partial'
955
- : 'failed';
956
- // 3. Git commit if all gates pass
957
- let gitCommit;
958
- if (allPass) {
959
- await runCmd('git', ['add', '-A']);
960
- const commitResult = await runCmd('git', [
961
- 'commit',
962
- '-m',
963
- `ralph(${task.id}): ${task.title}\n\nIteration ${iterationCount} - Ralph autonomous loop`,
1006
+ const projectName = prd.project || 'unknown';
1007
+ const totalTasks = prd.userStories.filter((s) => !s.passes).length;
1008
+ let iterationCount = 0;
1009
+ const iterationResults = [];
1010
+ // Log to stderr so MCP callers can see progress
1011
+ const logProgress = (msg) => {
1012
+ process.stderr.write(`[Ralph] ${msg}\n`);
1013
+ };
1014
+ logProgress(`Starting Ralph loop: ${totalTasks} pending tasks, max ${maxIterations} iterations, tool=${tool}`);
1015
+ while (ralphManager.canContinue() && iterationCount < maxIterations) {
1016
+ iterationCount++;
1017
+ const task = await ralphManager.getNextTask();
1018
+ if (!task)
1019
+ break;
1020
+ // Update lock with current progress
1021
+ await ralphManager.updateLockProgress(iterationCount, `${task.id}: ${task.title}`);
1022
+ logProgress(`Iteration ${iterationCount}/${maxIterations} — Task: ${task.id} "${task.title}"`);
1023
+ const startTime = Date.now();
1024
+ // 1. Execute AI agent
1025
+ let agentOutput = '';
1026
+ try {
1027
+ logProgress(` Executing ${tool} agent...`);
1028
+ const prompt = buildPrompt(task, projectName);
1029
+ agentOutput = await executeAgent(tool, prompt);
1030
+ logProgress(` Agent finished (${((Date.now() - startTime) / 1000).toFixed(0)}s)`);
1031
+ }
1032
+ catch (agentErr) {
1033
+ agentOutput = `Error: ${agentErr.message || agentErr}`;
1034
+ logProgress(` Agent error: ${agentErr.message || agentErr}`);
1035
+ }
1036
+ // 2. Run quality gates
1037
+ logProgress(` Running quality gates...`);
1038
+ const [typeCheck, lint, tests] = await Promise.all([
1039
+ runCmd('npm', ['run', 'type-check']).then((r) => r.code === 0),
1040
+ runCmd('npm', ['run', 'lint']).then((r) => r.code === 0),
1041
+ runCmd('npm', ['test']).then((r) => r.code === 0),
964
1042
  ]);
965
- const hashMatch = commitResult.stdout.match(/\[[\w/.-]+ ([a-f0-9]+)\]/);
966
- gitCommit = hashMatch ? hashMatch[1] : undefined;
967
- await ralphManager.markStoryComplete(task.id);
1043
+ const qualityChecks = { type_check: typeCheck, lint, tests, coverage_met: tests };
1044
+ const allPass = typeCheck && lint && tests;
1045
+ const passCount = Object.values(qualityChecks).filter(Boolean).length;
1046
+ const status = allPass
1047
+ ? 'success'
1048
+ : passCount >= 2
1049
+ ? 'partial'
1050
+ : 'failed';
1051
+ logProgress(` Quality: type-check=${typeCheck ? 'PASS' : 'FAIL'} lint=${lint ? 'PASS' : 'FAIL'} tests=${tests ? 'PASS' : 'FAIL'} → ${status.toUpperCase()}`);
1052
+ // 3. Git commit if all gates pass
1053
+ let gitCommit;
1054
+ if (allPass) {
1055
+ await runCmd('git', ['add', '-A']);
1056
+ const commitResult = await runCmd('git', [
1057
+ 'commit',
1058
+ '-m',
1059
+ `ralph(${task.id}): ${task.title}\n\nIteration ${iterationCount} - Ralph autonomous loop`,
1060
+ ]);
1061
+ const hashMatch = commitResult.stdout.match(/\[[\w/.-]+ ([a-f0-9]+)\]/);
1062
+ gitCommit = hashMatch ? hashMatch[1] : undefined;
1063
+ await ralphManager.markStoryComplete(task.id);
1064
+ logProgress(` Committed: ${gitCommit || 'no hash'} — Story ${task.id} COMPLETE`);
1065
+ }
1066
+ const iterDuration = Date.now() - startTime;
1067
+ // 4. Parse output for learnings/errors
1068
+ const parsed = RalphParser.parseAgentOutput(agentOutput, iterationCount, task.id, task.title, tool);
1069
+ // 5. Record iteration and refresh task count for canContinue()
1070
+ await ralphManager.recordIteration({
1071
+ iteration: iterationCount,
1072
+ timestamp: new Date().toISOString(),
1073
+ task_id: task.id,
1074
+ task_title: task.title,
1075
+ status,
1076
+ ai_tool: tool,
1077
+ execution_time_ms: iterDuration,
1078
+ quality_checks: qualityChecks,
1079
+ output_summary: parsed.output_summary || `Iteration ${iterationCount}: ${task.title}`,
1080
+ git_commit: gitCommit,
1081
+ learnings: parsed.learnings,
1082
+ errors: parsed.errors,
1083
+ metadata: {
1084
+ context_loss_count: parsed.metadata.context_loss_count,
1085
+ parsed_completion: parsed.metadata.parsed_completion,
1086
+ },
1087
+ });
1088
+ iterationResults.push({
1089
+ iteration: iterationCount,
1090
+ taskId: task.id,
1091
+ taskTitle: task.title,
1092
+ status,
1093
+ durationMs: iterDuration,
1094
+ });
1095
+ // Refresh task count so canContinue() reflects updated PRD
1096
+ await ralphManager.refreshTaskCount();
1097
+ logProgress(` Iteration ${iterationCount} complete (${(iterDuration / 1000).toFixed(0)}s)\n`);
968
1098
  }
969
- // 4. Parse output for learnings/errors
970
- const parsed = RalphParser.parseAgentOutput(agentOutput, iterationCount, task.id, task.title, tool);
971
- // 5. Record iteration
972
- await ralphManager.recordIteration({
973
- iteration: iterationCount,
974
- timestamp: new Date().toISOString(),
975
- task_id: task.id,
976
- task_title: task.title,
977
- status,
978
- ai_tool: tool,
979
- execution_time_ms: Date.now() - startTime,
980
- quality_checks: qualityChecks,
981
- output_summary: parsed.output_summary || `Iteration ${iterationCount}: ${task.title}`,
982
- git_commit: gitCommit,
983
- learnings: parsed.learnings,
984
- errors: parsed.errors,
985
- metadata: {
986
- context_loss_count: parsed.metadata.context_loss_count,
987
- parsed_completion: parsed.metadata.parsed_completion,
988
- },
989
- });
1099
+ const stats = await ralphManager.getTaskStats();
1100
+ logProgress(`Ralph loop finished: ${iterationCount} iterations, ${stats.completed}/${stats.total} tasks completed`);
1101
+ return {
1102
+ content: [
1103
+ {
1104
+ type: 'text',
1105
+ text: JSON.stringify({
1106
+ success: true,
1107
+ iterations: iterationCount,
1108
+ completed: stats.completed,
1109
+ total: stats.total,
1110
+ results: iterationResults,
1111
+ }),
1112
+ },
1113
+ ],
1114
+ };
1115
+ }
1116
+ finally {
1117
+ // Always release lock, even on error
1118
+ await ralphManager.releaseLock();
1119
+ process.removeListener('SIGTERM', cleanupLock);
1120
+ process.removeListener('SIGINT', cleanupLock);
990
1121
  }
991
- const stats = await ralphManager.getTaskStats();
992
- return {
993
- content: [
994
- {
995
- type: 'text',
996
- text: JSON.stringify({
997
- success: true,
998
- iterations: iterationCount,
999
- completed: stats.completed,
1000
- total: stats.total,
1001
- }),
1002
- },
1003
- ],
1004
- };
1005
1122
  }
1006
1123
  catch (error) {
1007
1124
  return {
@@ -1037,12 +1154,24 @@ export async function startRulebookMcpServer() {
1037
1154
  };
1038
1155
  }
1039
1156
  const stats = await ralphManager.getTaskStats();
1157
+ // Check if Ralph is currently running (lock held by alive process)
1158
+ const running = await ralphManager.isRunning();
1159
+ const lockInfo = running ? await ralphManager.getLockInfo() : null;
1040
1160
  return {
1041
1161
  content: [
1042
1162
  {
1043
1163
  type: 'text',
1044
1164
  text: JSON.stringify({
1045
1165
  success: true,
1166
+ running,
1167
+ ...(running && lockInfo
1168
+ ? {
1169
+ runningPid: lockInfo.pid,
1170
+ runningTask: lockInfo.currentTask || null,
1171
+ runningIteration: lockInfo.iteration || 0,
1172
+ runningSince: lockInfo.startedAt,
1173
+ }
1174
+ : {}),
1046
1175
  iteration: status.current_iteration,
1047
1176
  maxIterations: status.max_iterations,
1048
1177
  completedTasks: stats.completed,