bmalph 2.8.0 → 2.10.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 (113) hide show
  1. package/README.md +51 -28
  2. package/dist/cli.js +4 -2
  3. package/dist/commands/doctor-checks.js +1 -1
  4. package/dist/commands/doctor-health-checks.js +2 -1
  5. package/dist/commands/init.js +3 -1
  6. package/dist/commands/reset.js +1 -1
  7. package/dist/commands/run.js +49 -1
  8. package/dist/commands/status.js +0 -9
  9. package/dist/commands/upgrade.js +1 -1
  10. package/dist/installer/metadata.js +1 -1
  11. package/dist/installer/project-files.js +2 -3
  12. package/dist/installer/ralph-assets.js +8 -0
  13. package/dist/installer/template-files.js +54 -0
  14. package/dist/reset.js +2 -3
  15. package/dist/run/ralph-process.js +13 -3
  16. package/dist/run/run-dashboard.js +6 -4
  17. package/dist/transition/artifact-scan.js +3 -6
  18. package/dist/transition/context.js +1 -1
  19. package/dist/utils/constants.js +22 -0
  20. package/dist/utils/github.js +4 -3
  21. package/dist/utils/ralph-runtime-state.js +3 -13
  22. package/dist/utils/validate.js +4 -10
  23. package/dist/watch/dashboard.js +1 -0
  24. package/dist/watch/renderer.js +23 -0
  25. package/dist/watch/state-reader.js +20 -1
  26. package/package.json +8 -2
  27. package/ralph/drivers/DRIVER_INTERFACE.md +422 -0
  28. package/ralph/drivers/codex.sh +2 -2
  29. package/ralph/lib/response_analyzer.sh +87 -87
  30. package/ralph/ralph_loop.sh +220 -1
  31. package/ralph/templates/PROMPT.md +12 -0
  32. package/ralph/templates/REVIEW_PROMPT.md +60 -0
  33. package/ralph/templates/ralphrc.template +18 -0
  34. package/dist/cli.js.map +0 -1
  35. package/dist/commands/check-updates.js.map +0 -1
  36. package/dist/commands/doctor-checks.js.map +0 -1
  37. package/dist/commands/doctor-health-checks.js.map +0 -1
  38. package/dist/commands/doctor-runtime-checks.js.map +0 -1
  39. package/dist/commands/doctor.js.map +0 -1
  40. package/dist/commands/implement.js.map +0 -1
  41. package/dist/commands/init.js.map +0 -1
  42. package/dist/commands/reset.js.map +0 -1
  43. package/dist/commands/run.js.map +0 -1
  44. package/dist/commands/status.js.map +0 -1
  45. package/dist/commands/upgrade.js.map +0 -1
  46. package/dist/commands/watch.js.map +0 -1
  47. package/dist/installer/bmad-assets.js.map +0 -1
  48. package/dist/installer/commands.js.map +0 -1
  49. package/dist/installer/install.js.map +0 -1
  50. package/dist/installer/metadata.js.map +0 -1
  51. package/dist/installer/project-files.js.map +0 -1
  52. package/dist/installer/ralph-assets.js.map +0 -1
  53. package/dist/installer/template-files.js.map +0 -1
  54. package/dist/installer/types.js.map +0 -1
  55. package/dist/installer.js.map +0 -1
  56. package/dist/platform/aider.js.map +0 -1
  57. package/dist/platform/claude-code.js.map +0 -1
  58. package/dist/platform/codex.js.map +0 -1
  59. package/dist/platform/copilot.js.map +0 -1
  60. package/dist/platform/cursor-runtime-checks.js.map +0 -1
  61. package/dist/platform/cursor.js.map +0 -1
  62. package/dist/platform/detect.js.map +0 -1
  63. package/dist/platform/doctor-checks.js.map +0 -1
  64. package/dist/platform/guidance.js.map +0 -1
  65. package/dist/platform/instructions-snippet.js.map +0 -1
  66. package/dist/platform/opencode.js.map +0 -1
  67. package/dist/platform/registry.js.map +0 -1
  68. package/dist/platform/resolve.js.map +0 -1
  69. package/dist/platform/types.js.map +0 -1
  70. package/dist/platform/windsurf.js.map +0 -1
  71. package/dist/reset.js.map +0 -1
  72. package/dist/run/ralph-process.js.map +0 -1
  73. package/dist/run/run-dashboard.js.map +0 -1
  74. package/dist/run/types.js.map +0 -1
  75. package/dist/transition/artifact-collection.js.map +0 -1
  76. package/dist/transition/artifact-loading.js.map +0 -1
  77. package/dist/transition/artifact-scan.js.map +0 -1
  78. package/dist/transition/artifacts.js.map +0 -1
  79. package/dist/transition/context-output.js.map +0 -1
  80. package/dist/transition/context.js.map +0 -1
  81. package/dist/transition/fix-plan-sync.js.map +0 -1
  82. package/dist/transition/fix-plan.js.map +0 -1
  83. package/dist/transition/index.js.map +0 -1
  84. package/dist/transition/orchestration.js.map +0 -1
  85. package/dist/transition/preflight.js.map +0 -1
  86. package/dist/transition/section-patterns.js.map +0 -1
  87. package/dist/transition/specs-changelog.js.map +0 -1
  88. package/dist/transition/specs-index.js.map +0 -1
  89. package/dist/transition/specs-sync.js.map +0 -1
  90. package/dist/transition/sprint-status.js.map +0 -1
  91. package/dist/transition/story-id.js.map +0 -1
  92. package/dist/transition/story-parsing.js.map +0 -1
  93. package/dist/transition/tech-stack.js.map +0 -1
  94. package/dist/transition/types.js.map +0 -1
  95. package/dist/utils/artifact-definitions.js.map +0 -1
  96. package/dist/utils/config.js.map +0 -1
  97. package/dist/utils/constants.js.map +0 -1
  98. package/dist/utils/dryrun.js.map +0 -1
  99. package/dist/utils/errors.js.map +0 -1
  100. package/dist/utils/file-system.js.map +0 -1
  101. package/dist/utils/format-status.js.map +0 -1
  102. package/dist/utils/github.js.map +0 -1
  103. package/dist/utils/json.js.map +0 -1
  104. package/dist/utils/logger.js.map +0 -1
  105. package/dist/utils/ralph-runtime-state.js.map +0 -1
  106. package/dist/utils/state.js.map +0 -1
  107. package/dist/utils/validate.js.map +0 -1
  108. package/dist/watch/dashboard.js.map +0 -1
  109. package/dist/watch/file-watcher.js.map +0 -1
  110. package/dist/watch/frame-writer.js.map +0 -1
  111. package/dist/watch/renderer.js.map +0 -1
  112. package/dist/watch/state-reader.js.map +0 -1
  113. package/dist/watch/types.js.map +0 -1
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  [BMAD-METHOD](https://github.com/bmad-code-org/BMAD-METHOD) planning + [Ralph](https://github.com/snarktank/ralph) autonomous implementation, wired through platform-specific instructions, skills, and command indexes.
11
11
 
12
12
  <p align="center">
13
- <img src="docs/bmalph-diagram-light.svg" alt="bmalph workflow diagram" width="800" />
13
+ <img src="docs/bmalph-diagram.png" alt="bmalph workflow diagram" width="800" />
14
14
  </p>
15
15
 
16
16
  ## What is bmalph?
@@ -36,14 +36,15 @@ bmalph provides:
36
36
 
37
37
  bmalph works with multiple AI coding assistants. Each platform gets BMAD planning (Phases 1-3). The Ralph autonomous loop (Phase 4) requires a CLI-based platform.
38
38
 
39
- | Platform | ID | Tier | Instructions File | Commands |
40
- | -------------- | ------------- | ------------------- | --------------------------------- | -------------------------------- |
41
- | Claude Code | `claude-code` | full | `CLAUDE.md` | `.claude/commands/` directory |
42
- | OpenAI Codex | `codex` | full | `AGENTS.md` | Codex Skills (`.agents/skills/`) |
43
- | Cursor | `cursor` | full (experimental) | `.cursor/rules/bmad.mdc` | `_bmad/COMMANDS.md` |
44
- | Windsurf | `windsurf` | instructions-only | `.windsurf/rules/bmad.md` | `_bmad/COMMANDS.md` |
45
- | GitHub Copilot | `copilot` | full (experimental) | `.github/copilot-instructions.md` | `_bmad/COMMANDS.md` |
46
- | Aider | `aider` | instructions-only | `CONVENTIONS.md` | `_bmad/COMMANDS.md` |
39
+ | Platform | ID | Tier | Instructions File | Commands |
40
+ | -------------- | ------------- | ------------------- | --------------------------------- | ------------------------------------- |
41
+ | Claude Code | `claude-code` | full | `CLAUDE.md` | `.claude/commands/` directory |
42
+ | OpenAI Codex | `codex` | full | `AGENTS.md` | Codex Skills (`.agents/skills/`) |
43
+ | OpenCode | `opencode` | full | `AGENTS.md` | OpenCode Skills (`.opencode/skills/`) |
44
+ | Cursor | `cursor` | full (experimental) | `.cursor/rules/bmad.mdc` | `_bmad/COMMANDS.md` |
45
+ | Windsurf | `windsurf` | instructions-only | `.windsurf/rules/bmad.md` | `_bmad/COMMANDS.md` |
46
+ | GitHub Copilot | `copilot` | full (experimental) | `.github/copilot-instructions.md` | `_bmad/COMMANDS.md` |
47
+ | Aider | `aider` | instructions-only | `CONVENTIONS.md` | `_bmad/COMMANDS.md` |
47
48
 
48
49
  **Tiers:**
49
50
 
@@ -55,7 +56,7 @@ bmalph works with multiple AI coding assistants. Each platform gets BMAD plannin
55
56
  - Node.js 20+
56
57
  - Bash (WSL or Git Bash on Windows)
57
58
  - A supported AI coding platform (see table above)
58
- - For Ralph loop (Phase 4): Claude Code (`claude`), Codex CLI (`codex`), Copilot CLI (`copilot`), or Cursor CLI (`cursor-agent`; older `agent` installs are also supported)
59
+ - For Ralph loop (Phase 4): Claude Code (`claude`), Codex CLI (`codex`), OpenCode (`opencode`), Copilot CLI (`copilot`), or Cursor CLI (`cursor-agent`; older `agent` installs are also supported)
59
60
 
60
61
  ## Installation
61
62
 
@@ -85,15 +86,15 @@ bmalph init
85
86
 
86
87
  **Platform resolution:** `--platform` flag > auto-detect from project markers > interactive prompt > default `claude-code`
87
88
 
88
- Strong markers such as `.cursor/`, `.claude/`, `.windsurf/`, `.github/copilot-instructions.md`, and `.aider.conf.yml` are auto-detected directly. Root-only `AGENTS.md` and `CLAUDE.md` are treated as weak hints and may still trigger the interactive platform prompt.
89
+ Strong markers such as `.cursor/`, `.claude/`, `.opencode/`, `.windsurf/`, `.github/copilot-instructions.md`, and `.aider.conf.yml` are auto-detected directly. Root-only `AGENTS.md` and `CLAUDE.md` are treated as weak hints and may still trigger the interactive platform prompt.
89
90
 
90
91
  This installs:
91
92
 
92
93
  - `_bmad/` — BMAD agents and workflows
93
- - `.ralph/` — Ralph loop, libs, templates (drivers for claude-code, codex, copilot, and cursor)
94
+ - `.ralph/` — Ralph loop, libs, templates (drivers for claude-code, codex, opencode, copilot, and cursor)
94
95
  - `bmalph/` — State management (config.json, stores selected platform)
95
96
  - Updates the platform's instructions file with BMAD workflow instructions (e.g. `CLAUDE.md`, `AGENTS.md`, `.cursor/rules/bmad.mdc`)
96
- - Delivers BMAD commands using the platform's native mechanism (Claude Code: `.claude/commands/`; Codex: `.agents/skills/`; Cursor, Windsurf, Copilot, and Aider: `_bmad/COMMANDS.md`)
97
+ - Delivers BMAD commands using the platform's native mechanism (Claude Code: `.claude/commands/`; Codex: `.agents/skills/`; OpenCode: `.opencode/skills/`; Cursor, Windsurf, Copilot, and Aider: `_bmad/COMMANDS.md`)
97
98
 
98
99
  ### Migrating from standalone BMAD
99
100
 
@@ -166,7 +167,7 @@ Available in any phase for supporting tasks:
166
167
 
167
168
  ### Step 3: Implement with Ralph (Phase 4)
168
169
 
169
- > **Note:** Ralph is only available on **full** tier platforms (Claude Code, OpenAI Codex, GitHub Copilot, Cursor). Instructions-only platforms (Windsurf, Aider) support Phases 1-3 only. GitHub Copilot and Cursor support is experimental.
170
+ > **Note:** Ralph is only available on **full** tier platforms (Claude Code, OpenAI Codex, OpenCode, GitHub Copilot, Cursor). Instructions-only platforms (Windsurf, Aider) support Phases 1-3 only. GitHub Copilot and Cursor support is experimental.
170
171
 
171
172
  Run `bmalph implement` from the terminal, or use the `/bmalph-implement` slash command in Claude Code.
172
173
 
@@ -231,12 +232,12 @@ BMAD (add Epic 2) → bmalph implement → Ralph sees changes + picks up Epic 2
231
232
 
232
233
  ### init options
233
234
 
234
- | Flag | Description | Default |
235
- | -------------------------- | ---------------------------------------------------------------------------------- | -------------- |
236
- | `-n, --name <name>` | Project name | directory name |
237
- | `-d, --description <desc>` | Project description | (prompted) |
238
- | `--platform <id>` | Target platform (`claude-code`, `codex`, `cursor`, `windsurf`, `copilot`, `aider`) | auto-detect |
239
- | `--dry-run` | Preview changes without writing files | |
235
+ | Flag | Description | Default |
236
+ | -------------------------- | ---------------------------------------------------------------------------------------------- | -------------- |
237
+ | `-n, --name <name>` | Project name | directory name |
238
+ | `-d, --description <desc>` | Project description | (prompted) |
239
+ | `--platform <id>` | Target platform (`claude-code`, `codex`, `opencode`, `cursor`, `windsurf`, `copilot`, `aider`) | auto-detect |
240
+ | `--dry-run` | Preview changes without writing files | |
240
241
 
241
242
  ### implement options
242
243
 
@@ -278,11 +279,12 @@ BMAD (add Epic 2) → bmalph implement → Ralph sees changes + picks up Epic 2
278
279
 
279
280
  ### run options
280
281
 
281
- | Flag | Description |
282
- | --------------------- | -------------------------------------------------------------- |
283
- | `--driver <platform>` | Override platform driver (claude-code, codex, copilot, cursor) |
284
- | `--interval <ms>` | Dashboard refresh interval in milliseconds (default: 2000) |
285
- | `--no-dashboard` | Run Ralph without the dashboard overlay |
282
+ | Flag | Description |
283
+ | --------------------- | ---------------------------------------------------------------------------------------- |
284
+ | `--driver <platform>` | Override platform driver (claude-code, codex, opencode, copilot, cursor) |
285
+ | `--review [mode]` | Quality review: `enhanced` (every 5 loops) or `ultimate` (every story). Claude Code only |
286
+ | `--interval <ms>` | Dashboard refresh interval in milliseconds (default: 2000) |
287
+ | `--no-dashboard` | Run Ralph without the dashboard overlay |
286
288
 
287
289
  ### watch options
288
290
 
@@ -294,10 +296,11 @@ BMAD (add Epic 2) → bmalph implement → Ralph sees changes + picks up Epic 2
294
296
 
295
297
  ## Command Delivery
296
298
 
297
- bmalph bundles 51 BMAD and bmalph command definitions. Delivery varies by platform:
299
+ bmalph bundles 54 BMAD and bmalph command definitions. Delivery varies by platform:
298
300
 
299
301
  - **Claude Code** — installed as files in `.claude/commands/` (invoke with `/command-name`)
300
302
  - **OpenAI Codex** — delivered as Codex Skills in `.agents/skills/` (invoke with `$command-name`)
303
+ - **OpenCode** — delivered as OpenCode Skills in `.opencode/skills/`
301
304
  - **Cursor** — discoverable via `_bmad/COMMANDS.md`; ask Cursor to run the BMAD master agent
302
305
  - **Windsurf, Copilot, Aider** — discoverable via `_bmad/COMMANDS.md` reference index
303
306
 
@@ -324,6 +327,7 @@ For the full list:
324
327
 
325
328
  - Claude Code: run `/bmad-help`
326
329
  - OpenAI Codex: inspect `.agents/skills/`
330
+ - OpenCode: inspect `.opencode/skills/`
327
331
  - Cursor, Windsurf, Copilot, Aider: open `_bmad/COMMANDS.md`
328
332
 
329
333
  ### Transition to Ralph
@@ -358,7 +362,7 @@ project/
358
362
  │ ├── planning-artifacts/ # PRD, architecture, stories
359
363
  │ ├── implementation-artifacts/ # Sprint plans (optional)
360
364
  │ └── brainstorming/ # Brainstorm sessions (optional)
361
- ├── .ralph/ # Ralph autonomous loop (drivers for claude-code, codex, copilot, and cursor)
365
+ ├── .ralph/ # Ralph autonomous loop (drivers for claude-code, codex, opencode, copilot, and cursor)
362
366
  │ ├── ralph_loop.sh # Main loop script
363
367
  │ ├── ralph_import.sh # Import requirements into Ralph
364
368
  │ ├── ralph_monitor.sh # Monitor loop progress
@@ -367,6 +371,7 @@ project/
367
371
  │ ├── drivers/ # Platform driver scripts
368
372
  │ │ ├── claude-code.sh # Claude Code driver (uses `claude`)
369
373
  │ │ ├── codex.sh # OpenAI Codex driver (uses `codex exec`)
374
+ │ │ ├── opencode.sh # OpenCode driver (uses `opencode run`)
370
375
  │ │ ├── copilot.sh # GitHub Copilot driver (uses `copilot`, experimental)
371
376
  │ │ ├── cursor.sh # Cursor driver (uses `cursor-agent`/`agent`, experimental)
372
377
  │ │ └── cursor-agent-wrapper.sh # Wrapper for Windows .cmd Cursor installs
@@ -396,6 +401,7 @@ Ralph is a bash loop that spawns fresh AI coding sessions using a **platform dri
396
401
 
397
402
  - **Claude Code driver** — invokes `claude` with `--output-format json`, `--permission-mode bypassPermissions`, `--allowedTools`, and explicit `--resume <session_id>`
398
403
  - **Codex driver** — invokes `codex exec --json --sandbox workspace-write` with explicit `--resume <session_id>`
404
+ - **OpenCode driver** — invokes `opencode run --agent build --format json` with optional `--continue --session <session_id>`
399
405
  - **Copilot driver** _(experimental)_ — invokes `copilot --autopilot --yolo` with plain-text output
400
406
  - **Cursor driver** _(experimental)_ — invokes `cursor-agent -p --force --output-format json`, persists `session_id` for `--resume`, and switches to `stream-json` only for live output
401
407
 
@@ -410,6 +416,7 @@ Safety mechanisms:
410
416
 
411
417
  - **Circuit breaker** — prevents infinite loops on failing stories
412
418
  - **Response analyzer** — detects stuck or repeating outputs
419
+ - **Code review** — optional quality review (`--review [mode]`, Claude Code only). Enhanced: periodic review every 5 loops. Ultimate: review after every completed story. A read-only session analyzes git diffs and feeds structured findings into the next implementation loop
413
420
  - **Completion** — loop exits when all `@fix_plan.md` items are checked off
414
421
 
415
422
  Cursor-specific runtime checks:
@@ -478,7 +485,7 @@ Notes:
478
485
  | Ralph stops mid-loop | Circuit breaker detected stagnation. Check `.ralph/logs/` |
479
486
  | Doctor reports version drift | Run `bmalph upgrade` to update bundled assets |
480
487
  | Wrong platform detected | Re-run `bmalph init --platform <id>` with the correct platform |
481
- | Ralph unavailable on platform | Ralph requires a full tier platform (claude-code, codex, copilot, or cursor) |
488
+ | Ralph unavailable on platform | Ralph requires a full tier platform (claude-code, codex, opencode, copilot, or cursor) |
482
489
 
483
490
  ### Windows: Cursor Driver
484
491
 
@@ -513,6 +520,7 @@ Then remove the bmalph-managed sections from your instructions file. The file de
513
520
 
514
521
  - **Claude Code** — remove `.claude/commands/` and bmalph section from `CLAUDE.md`
515
522
  - **Codex** — remove bmalph sections from `AGENTS.md`
523
+ - **OpenCode** — remove `.opencode/skills/bmad-*/` and bmalph sections from `AGENTS.md`
516
524
  - **Cursor** — remove `.cursor/rules/bmad.mdc`
517
525
  - **Windsurf** — remove `.windsurf/rules/bmad.md`
518
526
  - **Copilot** — remove bmalph sections from `.github/copilot-instructions.md`
@@ -598,6 +606,21 @@ bmalph run
598
606
  # Then: bmalph run
599
607
  ```
600
608
 
609
+ **OpenCode:**
610
+
611
+ ```bash
612
+ # 1. Open your project in your AI coding assistant
613
+
614
+ # 2. Use OpenCode Skills such as $analyst, $create-prd, and $architect
615
+ # See .opencode/skills/ and _bmad/COMMANDS.md for the full catalog
616
+
617
+ # 3. Follow phases: Analysis -> Planning -> Solutioning
618
+
619
+ # 4. Transition to Ralph
620
+ # Run: bmalph implement
621
+ # Then: bmalph run
622
+ ```
623
+
601
624
  **Cursor, Copilot, Windsurf, Aider:**
602
625
 
603
626
  ```bash
package/dist/cli.js CHANGED
@@ -59,7 +59,7 @@ program
59
59
  .description("Initialize bmalph in the current project")
60
60
  .option("-n, --name <name>", "Project name")
61
61
  .option("-d, --description <desc>", "Project description")
62
- .option("--platform <id>", "Target platform (claude-code, codex, cursor, windsurf, copilot, aider)")
62
+ .option("--platform <id>", "Target platform (claude-code, codex, opencode, cursor, windsurf, copilot, aider)")
63
63
  .option("--dry-run", "Preview changes without writing files")
64
64
  .action(async (opts) => initCommand({ ...opts, projectDir: await resolveAndValidateProjectDir() }));
65
65
  program
@@ -102,9 +102,11 @@ program
102
102
  program
103
103
  .command("run")
104
104
  .description("Start Ralph loop with live dashboard")
105
- .option("--driver <platform>", "Override platform driver (claude-code, codex, copilot, cursor)")
105
+ .option("--driver <platform>", "Override platform driver (claude-code, codex, opencode, copilot, cursor)")
106
106
  .option("--interval <ms>", "Dashboard refresh interval in milliseconds (default: 2000)")
107
107
  .option("--no-dashboard", "Run Ralph without the dashboard overlay")
108
+ .option("--review [mode]", "Quality review: enhanced (~10-14% tokens) or ultimate (~20-30%)")
109
+ .option("--no-review", "Disable code review")
108
110
  .action(async (opts) => runCommand({ ...opts, projectDir: await resolveAndValidateProjectDir() }));
109
111
  void program.parseAsync();
110
112
  //# sourceMappingURL=cli.js.map
@@ -15,7 +15,7 @@ export async function checkCommandAvailable(command) {
15
15
  return false;
16
16
  }
17
17
  }
18
- export async function checkNodeVersion(_projectDir) {
18
+ export function checkNodeVersion(_projectDir) {
19
19
  const major = parseInt(process.versions.node.split(".")[0]);
20
20
  return {
21
21
  label: "Node version >= 20",
@@ -4,9 +4,10 @@ import { readConfig } from "../utils/config.js";
4
4
  import { parseGitignoreLines } from "../utils/file-system.js";
5
5
  import { getBundledVersions } from "../installer.js";
6
6
  import { isEnoent, formatError } from "../utils/errors.js";
7
+ import { GITIGNORE_ENTRIES } from "../utils/constants.js";
7
8
  export async function checkGitignore(projectDir) {
8
9
  const label = ".gitignore has required entries";
9
- const required = [".ralph/logs/", "_bmad-output/"];
10
+ const required = [...GITIGNORE_ENTRIES];
10
11
  try {
11
12
  const content = await readFile(join(projectDir, ".gitignore"), "utf-8");
12
13
  const existingLines = parseGitignoreLines(content);
@@ -1,5 +1,6 @@
1
1
  import chalk from "chalk";
2
- import { select, input } from "@inquirer/prompts";
2
+ import select from "@inquirer/select";
3
+ import input from "@inquirer/input";
3
4
  import { writeConfig } from "../utils/config.js";
4
5
  import { installProject, mergeInstructionsFile, isInitialized, previewInstall, getBundledVersions, } from "../installer.js";
5
6
  import { formatDryRunSummary } from "../utils/dryrun.js";
@@ -101,6 +102,7 @@ async function runInit(options) {
101
102
  const bundledVersions = await getBundledVersions();
102
103
  const config = {
103
104
  name: validatedName,
105
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive against future refactors
104
106
  description: description ?? "",
105
107
  createdAt: new Date().toISOString(),
106
108
  platform: platform.id,
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { confirm } from "@inquirer/prompts";
2
+ import confirm from "@inquirer/confirm";
3
3
  import { isInitialized } from "../installer.js";
4
4
  import { buildResetPlan, executeResetPlan, planToDryRunActions } from "../reset.js";
5
5
  import { formatDryRunSummary } from "../utils/dryrun.js";
@@ -24,6 +24,13 @@ async function executeRun(options) {
24
24
  if (platform.experimental) {
25
25
  console.log(chalk.yellow(`Warning: ${platform.displayName} support is experimental`));
26
26
  }
27
+ const reviewMode = await resolveReviewMode(options.review, platform);
28
+ if (reviewMode === "enhanced") {
29
+ console.log(chalk.cyan("Enhanced mode: code review every 5 implementation loops"));
30
+ }
31
+ else if (reviewMode === "ultimate") {
32
+ console.log(chalk.cyan("Ultimate mode: code review after every completed story"));
33
+ }
27
34
  const interval = parseInterval(options.interval);
28
35
  let useDashboard = dashboard;
29
36
  if (useDashboard) {
@@ -39,9 +46,10 @@ async function executeRun(options) {
39
46
  }
40
47
  const ralph = spawnRalphLoop(projectDir, platform.id, {
41
48
  inheritStdio: !useDashboard,
49
+ reviewMode,
42
50
  });
43
51
  if (useDashboard) {
44
- await startRunDashboard({ projectDir, interval, ralph });
52
+ await startRunDashboard({ projectDir, interval, ralph, reviewMode });
45
53
  if (ralph.state === "stopped") {
46
54
  applyRalphExitCode(ralph.exitCode);
47
55
  }
@@ -65,4 +73,44 @@ function resolvePlatform(driverOverride, configPlatform) {
65
73
  }
66
74
  return getPlatform(id);
67
75
  }
76
+ const VALID_REVIEW_MODES = new Set(["enhanced", "ultimate"]);
77
+ async function resolveReviewMode(reviewFlag, platform) {
78
+ if (reviewFlag === false) {
79
+ return "off";
80
+ }
81
+ if (reviewFlag === true || typeof reviewFlag === "string") {
82
+ if (platform.id !== "claude-code") {
83
+ throw new Error("--review requires Claude Code (other drivers lack read-only enforcement)");
84
+ }
85
+ if (reviewFlag === true) {
86
+ return "enhanced";
87
+ }
88
+ if (!VALID_REVIEW_MODES.has(reviewFlag)) {
89
+ throw new Error(`Unknown review mode: ${reviewFlag}. Valid modes: enhanced, ultimate`);
90
+ }
91
+ return reviewFlag;
92
+ }
93
+ if (platform.id !== "claude-code") {
94
+ return "off";
95
+ }
96
+ if (!process.stdin.isTTY) {
97
+ return "off";
98
+ }
99
+ const { default: select } = await import("@inquirer/select");
100
+ return select({
101
+ message: "Quality mode:",
102
+ choices: [
103
+ { name: "Standard — no code review (no extra cost)", value: "off" },
104
+ {
105
+ name: "Enhanced — periodic code review every 5 loops (~10-14% more tokens)",
106
+ value: "enhanced",
107
+ },
108
+ {
109
+ name: "Ultimate — review after every completed story (~20-30% more tokens)",
110
+ value: "ultimate",
111
+ },
112
+ ],
113
+ default: "off",
114
+ });
115
+ }
68
116
  //# sourceMappingURL=run.js.map
@@ -8,9 +8,6 @@ import { resolveProjectPlatform } from "../platform/resolve.js";
8
8
  import { getFullTierPlatformNames } from "../platform/registry.js";
9
9
  import { getPlatformAnalysisHint, getPlatformPrdHint } from "../platform/guidance.js";
10
10
  import { scanProjectArtifacts } from "../transition/artifact-scan.js";
11
- function getCursorNextAction() {
12
- return "Read _bmad/COMMANDS.md and ask Cursor to run the BMAD master agent";
13
- }
14
11
  export async function statusCommand(options) {
15
12
  await withErrorHandling(() => runStatus(options));
16
13
  }
@@ -151,14 +148,8 @@ function getNextAction(phase, status, ralphStatus, platform) {
151
148
  }
152
149
  switch (phase) {
153
150
  case 1:
154
- if (platform.id === "cursor") {
155
- return getCursorNextAction();
156
- }
157
151
  return getPlatformAnalysisHint(platform);
158
152
  case 2:
159
- if (platform.id === "cursor") {
160
- return getCursorNextAction();
161
- }
162
153
  return getPlatformPrdHint(platform);
163
154
  case 3:
164
155
  return "Run: bmalph implement";
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { confirm } from "@inquirer/prompts";
2
+ import confirm from "@inquirer/confirm";
3
3
  import { isInitialized, copyBundledAssets, mergeInstructionsFile, previewUpgrade, getBundledVersions, } from "../installer.js";
4
4
  import { readConfig, writeConfig } from "../utils/config.js";
5
5
  import { formatDryRunSummary } from "../utils/dryrun.js";
@@ -22,7 +22,7 @@ export async function getBundledVersions() {
22
22
  const versionsPath = join(__dirname, "..", "..", "bundled-versions.json");
23
23
  try {
24
24
  const versions = JSON.parse(await readFile(versionsPath, "utf-8"));
25
- if (!versions || typeof versions.bmadCommit !== "string") {
25
+ if (typeof versions.bmadCommit !== "string") {
26
26
  throw new Error("Invalid bundled-versions.json structure: missing bmadCommit");
27
27
  }
28
28
  return {
@@ -2,7 +2,7 @@ import { mkdir, readFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { atomicWriteFile, exists, parseGitignoreLines, replaceSection, } from "../utils/file-system.js";
4
4
  import { isEnoent } from "../utils/errors.js";
5
- import { CONFIG_FILE, STATE_DIR } from "../utils/constants.js";
5
+ import { CONFIG_FILE, GITIGNORE_ENTRIES, STATE_DIR } from "../utils/constants.js";
6
6
  import { getDefaultPlatform } from "./metadata.js";
7
7
  import { isTemplateCustomized } from "./template-files.js";
8
8
  export async function updateGitignore(projectDir) {
@@ -16,8 +16,7 @@ export async function updateGitignore(projectDir) {
16
16
  throw err;
17
17
  }
18
18
  const existingLines = parseGitignoreLines(existing);
19
- const entries = [".ralph/logs/", "_bmad-output/"];
20
- const newEntries = entries.filter((e) => !existingLines.has(e));
19
+ const newEntries = GITIGNORE_ENTRIES.filter((e) => !existingLines.has(e));
21
20
  if (newEntries.length === 0)
22
21
  return;
23
22
  const suffix = existing.length > 0 && !existing.endsWith("\n")
@@ -8,8 +8,10 @@ export async function installRalphAssets(projectDir, ralphDir, platform, package
8
8
  await mkdir(join(projectDir, ".ralph"), { recursive: true });
9
9
  const promptPath = join(projectDir, ".ralph/PROMPT.md");
10
10
  const agentPath = join(projectDir, ".ralph/@AGENT.md");
11
+ const reviewPromptPath = join(projectDir, ".ralph/REVIEW_PROMPT.md");
11
12
  const promptCustomized = await isTemplateCustomized(promptPath, "PROMPT.md");
12
13
  const agentCustomized = await isTemplateCustomized(agentPath, "AGENT.md");
14
+ const reviewPromptCustomized = await isTemplateCustomized(reviewPromptPath, "REVIEW_PROMPT.md");
13
15
  if (!promptCustomized) {
14
16
  await cp(join(ralphDir, "templates/PROMPT.md"), promptPath, {
15
17
  dereference: false,
@@ -20,6 +22,11 @@ export async function installRalphAssets(projectDir, ralphDir, platform, package
20
22
  dereference: false,
21
23
  });
22
24
  }
25
+ if (!reviewPromptCustomized) {
26
+ await cp(join(ralphDir, "templates/REVIEW_PROMPT.md"), reviewPromptPath, {
27
+ dereference: false,
28
+ });
29
+ }
23
30
  await cp(join(ralphDir, "RALPH-REFERENCE.md"), join(projectDir, ".ralph/RALPH-REFERENCE.md"), {
24
31
  dereference: false,
25
32
  });
@@ -83,6 +90,7 @@ export async function installRalphAssets(projectDir, ralphDir, platform, package
83
90
  ".ralph/lib/",
84
91
  ...(!promptCustomized ? [".ralph/PROMPT.md"] : []),
85
92
  ...(!agentCustomized ? [".ralph/@AGENT.md"] : []),
93
+ ...(!reviewPromptCustomized ? [".ralph/REVIEW_PROMPT.md"] : []),
86
94
  ...(!ralphrcCustomized && currentRalphrc !== renderedRalphrc ? [".ralph/.ralphrc"] : []),
87
95
  ".ralph/RALPH-REFERENCE.md",
88
96
  ],
@@ -5,6 +5,7 @@ import { getBundledRalphDir } from "./metadata.js";
5
5
  const TEMPLATE_PLACEHOLDERS = {
6
6
  "PROMPT.md": "[YOUR PROJECT NAME]",
7
7
  "AGENT.md": "pip install -r requirements.txt",
8
+ "REVIEW_PROMPT.md": "[YOUR PROJECT NAME]",
8
9
  };
9
10
  const RALPHRC_TEMPLATE_NAME = "RALPHRC";
10
11
  const CLAUDE_ALLOWED_TOOLS_TEMPLATE_LINE = 'ALLOWED_TOOLS="Write,Read,Edit,MultiEdit,Glob,Grep,Task,TodoWrite,WebFetch,WebSearch,EnterPlanMode,ExitPlanMode,NotebookEdit,Bash"';
@@ -49,6 +50,39 @@ QUALITY_GATE_TIMEOUT="\${QUALITY_GATE_TIMEOUT:-120}"
49
50
  # Only run gates when the agent signals completion (EXIT_SIGNAL=true)
50
51
  QUALITY_GATE_ON_COMPLETION_ONLY="\${QUALITY_GATE_ON_COMPLETION_ONLY:-false}"
51
52
 
53
+ `;
54
+ const REVIEW_TEMPLATE_BLOCK = `# =============================================================================
55
+ # PERIODIC CODE REVIEW
56
+ # =============================================================================
57
+
58
+ # Review mode: off, enhanced, or ultimate (set via 'bmalph run --review [mode]')
59
+ # - off: no code review (default)
60
+ # - enhanced: periodic review every REVIEW_INTERVAL loops (~10-14% more tokens)
61
+ # - ultimate: review after every completed story (~20-30% more tokens)
62
+ # The review agent analyzes git diffs and outputs findings for the next implementation loop.
63
+ # Currently supported on Claude Code only.
64
+ REVIEW_MODE="\${REVIEW_MODE:-off}"
65
+
66
+ # (Legacy) Enables review — prefer REVIEW_MODE instead
67
+ REVIEW_ENABLED="\${REVIEW_ENABLED:-false}"
68
+
69
+ # Number of implementation loops between review sessions (enhanced mode only)
70
+ REVIEW_INTERVAL="\${REVIEW_INTERVAL:-5}"
71
+
72
+ `;
73
+ const PREVIOUS_REVIEW_TEMPLATE_BLOCK = `# =============================================================================
74
+ # PERIODIC CODE REVIEW
75
+ # =============================================================================
76
+
77
+ # Enable periodic code review loops (set via 'bmalph run --review' or manually)
78
+ # When enabled, Ralph runs a read-only review session every REVIEW_INTERVAL loops.
79
+ # The review agent analyzes git diffs and outputs findings for the next implementation loop.
80
+ # Currently supported on Claude Code only.
81
+ REVIEW_ENABLED="\${REVIEW_ENABLED:-false}"
82
+
83
+ # Number of implementation loops between review sessions (default: 5)
84
+ REVIEW_INTERVAL="\${REVIEW_INTERVAL:-5}"
85
+
52
86
  `;
53
87
  const LEGACY_RALPHRC_TEMPLATE = `# .ralphrc - Ralph project configuration
54
88
  # Generated by: ralph enable
@@ -209,6 +243,26 @@ async function isRalphrcCustomized(filePath, platformId) {
209
243
  if (matchesManagedPermissionVariants(content, templateWithoutQG)) {
210
244
  return false;
211
245
  }
246
+ // Check variants without review block (pre-review installs)
247
+ const templateWithoutReview = currentTemplate.replace(REVIEW_TEMPLATE_BLOCK, "");
248
+ if (matchesManagedPermissionVariants(content, templateWithoutReview)) {
249
+ return false;
250
+ }
251
+ // Check variants with previous review block (pre-ultimate installs)
252
+ const templateWithPreviousReview = currentTemplate.replace(REVIEW_TEMPLATE_BLOCK, PREVIOUS_REVIEW_TEMPLATE_BLOCK);
253
+ if (matchesManagedPermissionVariants(content, templateWithPreviousReview)) {
254
+ return false;
255
+ }
256
+ // Check variants without both quality gates and review blocks
257
+ const templateWithoutQGAndReview = templateWithoutQG.replace(REVIEW_TEMPLATE_BLOCK, "");
258
+ if (matchesManagedPermissionVariants(content, templateWithoutQGAndReview)) {
259
+ return false;
260
+ }
261
+ // Check variants without quality gates but with previous review block
262
+ const templateWithoutQGButPreviousReview = templateWithoutQG.replace(REVIEW_TEMPLATE_BLOCK, PREVIOUS_REVIEW_TEMPLATE_BLOCK);
263
+ if (matchesManagedPermissionVariants(content, templateWithoutQGButPreviousReview)) {
264
+ return false;
265
+ }
212
266
  const legacyTemplate = normalizeManagedRalphrcContent(renderLegacyRalphrcTemplate(platformId));
213
267
  return content !== legacyTemplate;
214
268
  }
package/dist/reset.js CHANGED
@@ -3,7 +3,7 @@ import { join, posix } from "node:path";
3
3
  import { getSlashCommandsDir } from "./installer.js";
4
4
  import { exists, atomicWriteFile, parseGitignoreLines, replaceSection, } from "./utils/file-system.js";
5
5
  import { isEnoent } from "./utils/errors.js";
6
- import { BMAD_DIR, RALPH_DIR, BMALPH_DIR, BMAD_OUTPUT_DIR, SKILLS_PREFIX, } from "./utils/constants.js";
6
+ import { BMAD_DIR, RALPH_DIR, BMALPH_DIR, BMAD_OUTPUT_DIR, GITIGNORE_ENTRIES, SKILLS_PREFIX, } from "./utils/constants.js";
7
7
  export async function buildResetPlan(projectDir, platform) {
8
8
  const plan = {
9
9
  directories: [],
@@ -74,8 +74,7 @@ export async function buildResetPlan(projectDir, platform) {
74
74
  try {
75
75
  const content = await readFile(join(projectDir, ".gitignore"), "utf-8");
76
76
  const existingLines = parseGitignoreLines(content);
77
- const bmalpEntries = [".ralph/logs/", "_bmad-output/"];
78
- for (const entry of bmalpEntries) {
77
+ for (const entry of GITIGNORE_ENTRIES) {
79
78
  if (existingLines.has(entry)) {
80
79
  plan.gitignoreLines.push(entry);
81
80
  }
@@ -74,10 +74,10 @@ export async function runBashCommand(command, options = {}) {
74
74
  }
75
75
  finish(() => reject(new Error(`bash command timed out: ${command}`)));
76
76
  }, timeoutMs);
77
- child.stdout?.on("data", (chunk) => {
77
+ child.stdout.on("data", (chunk) => {
78
78
  stdout += chunk.toString();
79
79
  });
80
- child.stderr?.on("data", (chunk) => {
80
+ child.stderr.on("data", (chunk) => {
81
81
  stderr += chunk.toString();
82
82
  });
83
83
  child.on("close", (exitCode) => finish(() => resolve({
@@ -124,9 +124,19 @@ export async function validateRalphLoop(projectDir) {
124
124
  }
125
125
  }
126
126
  export function spawnRalphLoop(projectDir, platformId, options) {
127
+ const env = { ...process.env, PLATFORM_DRIVER: platformId };
128
+ if (options.reviewMode) {
129
+ env.REVIEW_MODE = options.reviewMode;
130
+ if (options.reviewMode !== "off") {
131
+ env.REVIEW_ENABLED = "true";
132
+ if (options.reviewMode === "enhanced") {
133
+ env.REVIEW_INTERVAL = "5";
134
+ }
135
+ }
136
+ }
127
137
  const child = spawn(cachedBashCommand ?? "bash", [BASH_RALPH_LOOP_PATH], {
128
138
  cwd: projectDir,
129
- env: { ...process.env, PLATFORM_DRIVER: platformId },
139
+ env,
130
140
  stdio: options.inheritStdio ? "inherit" : ["ignore", "pipe", "pipe"],
131
141
  detached: process.platform !== "win32",
132
142
  windowsHide: true,
@@ -2,11 +2,12 @@ import { createRefreshCallback } from "../watch/dashboard.js";
2
2
  import { createTerminalFrameWriter } from "../watch/frame-writer.js";
3
3
  import { FileWatcher } from "../watch/file-watcher.js";
4
4
  import { renderFooterLine } from "../watch/renderer.js";
5
- export function renderStatusBar(ralph) {
5
+ export function renderStatusBar(ralph, reviewMode) {
6
6
  const pid = ralph.child.pid ?? "?";
7
+ const badge = reviewMode === "ultimate" ? " [ultimate]" : reviewMode === "enhanced" ? " [review]" : "";
7
8
  switch (ralph.state) {
8
9
  case "running":
9
- return `Ralph: running (PID ${pid}) | q: stop/detach`;
10
+ return `Ralph: running (PID ${pid})${badge} | q: stop/detach`;
10
11
  case "stopped":
11
12
  return `Ralph: stopped (exit ${ralph.exitCode ?? "?"}) | q: quit`;
12
13
  case "detached":
@@ -17,12 +18,12 @@ export function renderQuitPrompt() {
17
18
  return "Stop (s) | Detach (d) | Cancel (c)";
18
19
  }
19
20
  export async function startRunDashboard(options) {
20
- const { projectDir, interval, ralph } = options;
21
+ const { projectDir, interval, ralph, reviewMode } = options;
21
22
  const frameWriter = createTerminalFrameWriter();
22
23
  let showingPrompt = false;
23
24
  let stopped = false;
24
25
  const footerRenderer = (lastUpdated, cols) => {
25
- const leftText = showingPrompt ? renderQuitPrompt() : renderStatusBar(ralph);
26
+ const leftText = showingPrompt ? renderQuitPrompt() : renderStatusBar(ralph, reviewMode);
26
27
  return renderFooterLine(leftText, `Updated: ${lastUpdated.toISOString().slice(11, 19)}`, cols);
27
28
  };
28
29
  const refresh = createRefreshCallback(projectDir, (frame) => {
@@ -100,6 +101,7 @@ export async function startRunDashboard(options) {
100
101
  }
101
102
  resolve();
102
103
  };
104
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- setRawMode absent in pseudo-TTY
103
105
  if (process.stdin.isTTY && process.stdin.setRawMode) {
104
106
  process.stdin.setRawMode(true);
105
107
  process.stdin.resume();
@@ -3,10 +3,7 @@ import { relative } from "node:path";
3
3
  import { findArtifactsDir } from "./artifacts.js";
4
4
  import { ARTIFACT_DEFINITIONS } from "../utils/artifact-definitions.js";
5
5
  import { getPlatform } from "../platform/registry.js";
6
- import { getPlatformAnalysisHint, getPlatformArchitectureHint, getPlatformEpicsStoriesHint, getPlatformPrdHint, getPlatformReadinessHint, } from "../platform/guidance.js";
7
- function getCursorNextAction() {
8
- return "Read _bmad/COMMANDS.md and ask Cursor to run the BMAD master agent for the next BMAD workflow";
9
- }
6
+ import { getPlatformAnalysisHint, getPlatformArchitectureHint, getPlatformEpicsStoriesHint, getPlatformMasterAgentHint, getPlatformPrdHint, getPlatformReadinessHint, } from "../platform/guidance.js";
10
7
  export function classifyArtifact(filename) {
11
8
  for (const rule of ARTIFACT_DEFINITIONS) {
12
9
  if (rule.pattern.test(filename)) {
@@ -47,13 +44,13 @@ export function getMissing(phases) {
47
44
  export function suggestNext(phases, detectedPhase, platformId) {
48
45
  const foundNames = new Set([...phases[1], ...phases[2], ...phases[3]].map((a) => a.name));
49
46
  const platform = platformId ? getPlatform(platformId) : null;
50
- if (platformId === "cursor") {
47
+ if (platform && platform.commandDelivery.kind === "index") {
51
48
  const allPlanningArtifactsPresent = foundNames.has("PRD") &&
52
49
  foundNames.has("Architecture") &&
53
50
  foundNames.has("Epics & Stories") &&
54
51
  foundNames.has("Readiness Report");
55
52
  if (!allPlanningArtifactsPresent) {
56
- return getCursorNextAction();
53
+ return getPlatformMasterAgentHint(platform);
57
54
  }
58
55
  }
59
56
  if (detectedPhase <= 1 && phases[1].length === 0) {
@@ -11,7 +11,7 @@ export function extractSectionWithInfo(content, headingPattern, maxLength = SECT
11
11
  // Determine heading level from the match
12
12
  const headingLevelMatch = match[0].match(/^(#{1,6})\s/);
13
13
  const level = headingLevelMatch ? headingLevelMatch[1].length : 2;
14
- const startIndex = (match.index ?? 0) + match[0].length;
14
+ const startIndex = match.index + match[0].length;
15
15
  const rest = content.slice(startIndex);
16
16
  // Find next heading of same or higher level
17
17
  const nextHeadingPattern = new RegExp(`^#{1,${level}}\\s`, "m");