ralph-prd 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/bin/install.mjs +42 -29
- package/package.json +1 -2
- package/ralph/lib/config.mjs +9 -8
- package/ralph/lib/ship-checker.mjs +319 -0
- package/ralph/lib/verifier.mjs +1 -1
- package/ralph/ralph-claude.mjs +70 -2
- package/ralph/ralph.config.sample.yaml +10 -5
- package/ralph/test/ship-checker.test.mjs +268 -0
- package/skills/grill-me/SKILL.md +0 -10
- package/skills/grill-me/agents/openai.yaml +0 -4
- package/skills/prd-to-plan/SKILL.md +0 -107
- package/skills/prd-to-plan/agents/openai.yaml +0 -4
- package/skills/repo-doc-maintainer/SKILL.md +0 -101
- package/skills/repo-doc-maintainer/agents/openai.yaml +0 -4
- package/skills/repo-doc-maintainer/references/doc-update-criteria.md +0 -32
- package/skills/review-changes/SKILL.md +0 -90
- package/skills/review-changes/agents/openai.yaml +0 -4
- package/skills/review-changes/references/review-checklist.md +0 -37
- package/skills/ship-check/SKILL.md +0 -80
- package/skills/ship-check/agents/openai.yaml +0 -4
- package/skills/ship-check/references/validation-flow.md +0 -26
- package/skills/write-a-prd/SKILL.md +0 -74
- package/skills/write-a-prd/agents/openai.yaml +0 -4
package/README.md
CHANGED
|
@@ -22,12 +22,12 @@ cd your-project && ../ralph-prd/install.sh
|
|
|
22
22
|
|
|
23
23
|
This installs:
|
|
24
24
|
- `.claude/ralph/` — the phased runner
|
|
25
|
-
- `.claude/skills/` —
|
|
25
|
+
- `.claude/skills/` — 7 Claude Code skills fetched from [`tahaJemmali/skills`](https://github.com/tahaJemmali/skills) via `npx skills add`
|
|
26
26
|
- On first install, adds `.claude/ralph/` and `.claude/skills/` to `.gitignore` (skipped if `.claude/` already exists, so shared setups are respected)
|
|
27
27
|
|
|
28
28
|
### Updating
|
|
29
29
|
|
|
30
|
-
To update Ralph
|
|
30
|
+
To update Ralph and re-fetch skills, just re-run the install command:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
npx ralph-prd
|
|
@@ -35,6 +35,12 @@ npx ralph-prd
|
|
|
35
35
|
|
|
36
36
|
Ralph also checks for updates automatically on every run. If a newer version is available, you'll see a notice in the console output.
|
|
37
37
|
|
|
38
|
+
To update only the skills without reinstalling:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
node .claude/ralph/ralph-claude.mjs --update-skills
|
|
42
|
+
```
|
|
43
|
+
|
|
38
44
|
## Requirements
|
|
39
45
|
|
|
40
46
|
- Node.js 18+
|
|
@@ -88,6 +94,7 @@ Options:
|
|
|
88
94
|
--i-did-this Skip Claude self-commit; run separate commit step
|
|
89
95
|
--send-it Push branch + open PR when all phases complete
|
|
90
96
|
--wait-for-it Pause before each commit for review
|
|
97
|
+
--update-skills Re-fetch skills from tahaJemmali/skills and exit
|
|
91
98
|
--version, -v Print installed version and exit
|
|
92
99
|
```
|
|
93
100
|
|
|
@@ -114,10 +121,13 @@ node .claude/ralph/ralph-claude.mjs docs/auth-rework/plan.md --send-it
|
|
|
114
121
|
| `/grill-me` | Stress-test a plan by walking every branch of the decision tree |
|
|
115
122
|
| `/write-a-prd` | Interview-driven PRD creation with codebase exploration |
|
|
116
123
|
| `/prd-to-plan` | Turn a PRD into phased vertical-slice implementation plan |
|
|
124
|
+
| `/reality-check` | Brutally honest architectural critique and assumption stress-test |
|
|
117
125
|
| `/review-changes` | Review recent changes against project guidelines |
|
|
118
126
|
| `/repo-doc-maintainer` | Decide if AGENTS.md or docs need updating after changes |
|
|
119
127
|
| `/ship-check` | End-of-task validation: review + doc maintenance check |
|
|
120
128
|
|
|
129
|
+
Skills are fetched from [`tahaJemmali/skills`](https://github.com/tahaJemmali/skills) during install. You can also install or update them independently with `npx skills add tahaJemmali/skills`.
|
|
130
|
+
|
|
121
131
|
## Multi-Repo Support
|
|
122
132
|
|
|
123
133
|
Create `ralph.config.yaml` in `.claude/ralph/` for monorepo setups:
|
|
@@ -181,6 +191,10 @@ Ralph validates the plan structure before execution and checks off criteria as p
|
|
|
181
191
|
- **macOS notifications** — get notified when phases complete or fail
|
|
182
192
|
- **Safety** — optional `blocked-commands.txt` and `blocked-paths.txt` restrict what Claude can do
|
|
183
193
|
|
|
194
|
+
## Acknowledgments
|
|
195
|
+
|
|
196
|
+
Three of the skills in this repo — [`/grill-me`](https://skills.sh/mattpocock/skills/grill-me), [`/write-a-prd`](https://skills.sh/mattpocock/skills/write-a-prd), and [`/prd-to-plan`](https://skills.sh/mattpocock/skills/prd-to-plan) — are based on [Matt Pocock](https://github.com/mattpocock)'s work. Matt generously gave his blessing to include them here. If you find these useful, go check out his cohort and give him a star.
|
|
197
|
+
|
|
184
198
|
## License
|
|
185
199
|
|
|
186
200
|
MIT
|
package/bin/install.mjs
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* ralph-prd installer — called via `npx ralph-prd` or `npx ralph-prd init`
|
|
5
5
|
*
|
|
6
|
-
* Copies ralph/
|
|
6
|
+
* Copies ralph/ into .claude/ of the current project, then fetches skills
|
|
7
|
+
* from the skills repo via `npx skills add tahaJemmali/skills`.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
import { existsSync, mkdirSync, cpSync, rmSync,
|
|
10
|
-
import { resolve, dirname
|
|
10
|
+
import { existsSync, mkdirSync, cpSync, rmSync, readFileSync, writeFileSync } from 'fs';
|
|
11
|
+
import { resolve, dirname } from 'path';
|
|
11
12
|
import { fileURLToPath } from 'url';
|
|
13
|
+
import { spawnSync } from 'child_process';
|
|
12
14
|
|
|
13
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
16
|
const PKG_ROOT = resolve(__dirname, '..');
|
|
@@ -60,35 +62,46 @@ const pkg = JSON.parse(readFileSync(resolve(PKG_ROOT, 'package.json'), 'utf8'));
|
|
|
60
62
|
writeFileSync(resolve(ralphDst, '.ralph-version'), pkg.version + '\n', 'utf8');
|
|
61
63
|
ok(`Installed ralph runner v${pkg.version} -> .claude/ralph/`);
|
|
62
64
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
// Install skills via skills.sh
|
|
66
|
+
info('Installing skills from tahaJemmali/skills…');
|
|
67
|
+
const REQUIRED_SKILLS = [
|
|
68
|
+
'grill-me',
|
|
69
|
+
'write-a-prd',
|
|
70
|
+
'prd-to-plan',
|
|
71
|
+
'reality-check',
|
|
72
|
+
'ship-check',
|
|
73
|
+
'review-changes',
|
|
74
|
+
'repo-doc-maintainer',
|
|
75
|
+
];
|
|
76
|
+
const skillsResult = spawnSync(
|
|
77
|
+
'npx',
|
|
78
|
+
['skills', 'add', 'tahaJemmali/skills', ...REQUIRED_SKILLS.flatMap(s => ['--skill', s]), '-a', 'claude-code', '-y'],
|
|
79
|
+
{ cwd: projectRoot, stdio: 'inherit', encoding: 'utf8' },
|
|
80
|
+
);
|
|
81
|
+
if (skillsResult.status !== 0) {
|
|
82
|
+
fail('Skills installation failed — aborting. ralph-prd requires all skills to be installed.\n Retry by running: npx ralph-prd');
|
|
77
83
|
}
|
|
84
|
+
ok('Installed skills -> .claude/skills/');
|
|
85
|
+
|
|
86
|
+
const gitignorePath = resolve(projectRoot, '.gitignore');
|
|
87
|
+
let gitignoreContent = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
|
|
88
|
+
const lines = gitignoreContent.split('\n').map(l => l.trim());
|
|
78
89
|
|
|
79
|
-
//
|
|
90
|
+
// skills-lock.json is always a generated file — add to gitignore unconditionally.
|
|
91
|
+
const alwaysIgnore = ['skills-lock.json'];
|
|
92
|
+
const alwaysMissing = alwaysIgnore.filter(e => !lines.includes(e));
|
|
93
|
+
|
|
94
|
+
// Only add .claude/ entries on first install — if .claude/ already existed,
|
|
80
95
|
// the user may be sharing it via git intentionally.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
ok(`Added ${missing.join(', ')} to .gitignore`);
|
|
91
|
-
}
|
|
96
|
+
const conditionalIgnore = isFirstInstall ? ['.claude/ralph/', '.claude/skills/'] : [];
|
|
97
|
+
const conditionalMissing = conditionalIgnore.filter(e => !lines.includes(e));
|
|
98
|
+
|
|
99
|
+
const missing = [...alwaysMissing, ...conditionalMissing];
|
|
100
|
+
if (missing.length > 0) {
|
|
101
|
+
const block = (gitignoreContent.length > 0 && !gitignoreContent.endsWith('\n') ? '\n' : '') +
|
|
102
|
+
'\n# ralph-prd (installed via npx ralph-prd)\n' + missing.join('\n') + '\n';
|
|
103
|
+
writeFileSync(gitignorePath, gitignoreContent + block, 'utf8');
|
|
104
|
+
ok(`Added ${missing.join(', ')} to .gitignore`);
|
|
92
105
|
}
|
|
93
106
|
|
|
94
107
|
// Summary
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ralph-prd",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "AI-powered phased implementation runner for Claude Code — from PRD to shipped code",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ralph-prd": "./bin/install.mjs"
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"bin/",
|
|
25
25
|
"ralph/",
|
|
26
|
-
"skills/",
|
|
27
26
|
"install.sh",
|
|
28
27
|
"README.md"
|
|
29
28
|
],
|
package/ralph/lib/config.mjs
CHANGED
|
@@ -23,12 +23,13 @@ function isGitRepo(dirPath) {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* @typedef {Object} RalphFlags
|
|
26
|
-
* @property {boolean} iDidThis
|
|
27
|
-
* @property {boolean} sendIt
|
|
28
|
-
* @property {boolean} waitForIt
|
|
29
|
-
* @property {number} maxRepairs
|
|
30
|
-
* @property {number|null} onlyPhase
|
|
31
|
-
* @property {string} logLevel
|
|
26
|
+
* @property {boolean} iDidThis - Claude skips self-commit; separate commit step runs instead
|
|
27
|
+
* @property {boolean} sendIt - Push branch and open a PR after all phases complete
|
|
28
|
+
* @property {boolean} waitForIt - Pause for user confirmation before each commit step
|
|
29
|
+
* @property {number} maxRepairs - Max repair attempts per phase before hard-stopping (default 3)
|
|
30
|
+
* @property {number|null} onlyPhase - When set, only this 1-based phase index is run (force re-run)
|
|
31
|
+
* @property {string} logLevel - "none" | "necessary" | "dump" (default "necessary")
|
|
32
|
+
* @property {boolean} skipShipCheck - Skip the post-commit ship-check step for every phase
|
|
32
33
|
*/
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -59,7 +60,7 @@ function isGitRepo(dirPath) {
|
|
|
59
60
|
function parseConfigYaml(content) {
|
|
60
61
|
const repos = [];
|
|
61
62
|
const writableDirs = [];
|
|
62
|
-
const flags = { iDidThis: false, sendIt: false, waitForIt: false, maxRepairs: 3, onlyPhase: null, logLevel: 'necessary' };
|
|
63
|
+
const flags = { iDidThis: false, sendIt: false, waitForIt: false, maxRepairs: 3, onlyPhase: null, logLevel: 'necessary', skipShipCheck: false };
|
|
63
64
|
const hooks = { afterCommit: null };
|
|
64
65
|
let section = null;
|
|
65
66
|
let current = null;
|
|
@@ -159,7 +160,7 @@ function parseConfigYaml(content) {
|
|
|
159
160
|
*/
|
|
160
161
|
export function resolveRepos(runnerDir) {
|
|
161
162
|
const configPath = join(runnerDir, CONFIG_FILENAME);
|
|
162
|
-
const defaultFlags = { iDidThis: false, sendIt: false, waitForIt: false, maxRepairs: 3, onlyPhase: null };
|
|
163
|
+
const defaultFlags = { iDidThis: false, sendIt: false, waitForIt: false, maxRepairs: 3, onlyPhase: null, skipShipCheck: false };
|
|
163
164
|
const defaultHooks = { afterCommit: null };
|
|
164
165
|
|
|
165
166
|
if (!existsSync(configPath)) {
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/ship-checker.mjs
|
|
3
|
+
*
|
|
4
|
+
* Ship-Check coordinator — runs after commit, before markPhaseComplete.
|
|
5
|
+
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* 1. Read .claude/skills/ship-check/SKILL.md, strip YAML frontmatter.
|
|
8
|
+
* 2. Build ship-check prompt: skill body + phase context + repo state.
|
|
9
|
+
* 3. Run ship-check session via transport.
|
|
10
|
+
* 4. Parse VERDICT: APPROVED or VERDICT: REMARKS (case-insensitive).
|
|
11
|
+
* 5. APPROVED → return normally.
|
|
12
|
+
* 6. REMARKS → extract findings, run one repair session, re-run ship-check.
|
|
13
|
+
* 7. Second REMARKS → throw ShipCheckError with findings attached.
|
|
14
|
+
*
|
|
15
|
+
* Public API:
|
|
16
|
+
* class ShipCheckError extends Error
|
|
17
|
+
* .phaseName: string
|
|
18
|
+
* .findings: string
|
|
19
|
+
*
|
|
20
|
+
* runShipCheck({
|
|
21
|
+
* phase, repoState, logWriter, phaseNum, startTaskNum, send,
|
|
22
|
+
* _skillsBase // optional base dir override for testing
|
|
23
|
+
* }) → Promise<{ nextTaskNum: number }>
|
|
24
|
+
* throws ShipCheckError on second REMARKS verdict
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { readFileSync } from 'fs';
|
|
28
|
+
import { join } from 'path';
|
|
29
|
+
|
|
30
|
+
// ─── Error type ───────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export class ShipCheckError extends Error {
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} message
|
|
35
|
+
* @param {string} phaseName
|
|
36
|
+
* @param {string} findings
|
|
37
|
+
*/
|
|
38
|
+
constructor(message, phaseName, findings) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'ShipCheckError';
|
|
41
|
+
this.phaseName = phaseName;
|
|
42
|
+
this.findings = findings;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── SKILL.md loading ─────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const SKILL_REL_PATH = join('.claude', 'skills', 'ship-check', 'SKILL.md');
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Strip YAML frontmatter (--- ... ---) from skill content and return the body.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} content
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function stripFrontmatter(content) {
|
|
57
|
+
if (!content.startsWith('---')) return content;
|
|
58
|
+
const end = content.indexOf('\n---', 3);
|
|
59
|
+
if (end === -1) return content;
|
|
60
|
+
return content.slice(end + 4).trimStart();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read and parse the ship-check SKILL.md.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} baseDir - Repo root (or test override via _skillsBase)
|
|
67
|
+
* @returns {string} Skill body with frontmatter stripped
|
|
68
|
+
* @throws {Error} with a clear message if the file is missing
|
|
69
|
+
*/
|
|
70
|
+
function loadSkill(baseDir) {
|
|
71
|
+
const skillPath = join(baseDir, SKILL_REL_PATH);
|
|
72
|
+
let raw;
|
|
73
|
+
try {
|
|
74
|
+
raw = readFileSync(skillPath, 'utf8');
|
|
75
|
+
} catch (err) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Ship-check skill not found at ${skillPath}. ` +
|
|
78
|
+
`Create ${SKILL_REL_PATH} in your repository to enable ship-check. ` +
|
|
79
|
+
`(${err.code})`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return stripFrontmatter(raw);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Verdict parsing ──────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Scan response text for the first VERDICT line and extract findings on REMARKS.
|
|
89
|
+
*
|
|
90
|
+
* Findings may be delimited by FINDINGS_START / FINDINGS_END; if the model
|
|
91
|
+
* omits the delimiters, everything after the VERDICT line is used as findings.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} text
|
|
94
|
+
* @returns {{ verdict: 'APPROVED'|'REMARKS'|'UNKNOWN', findings: string }}
|
|
95
|
+
*/
|
|
96
|
+
function parseVerdict(text) {
|
|
97
|
+
const lines = text.split('\n');
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
const match = lines[i].trim().match(/^VERDICT:\s+(APPROVED|REMARKS)$/i);
|
|
101
|
+
if (!match) continue;
|
|
102
|
+
|
|
103
|
+
const verdict = match[1].toUpperCase();
|
|
104
|
+
|
|
105
|
+
if (verdict === 'APPROVED') {
|
|
106
|
+
return { verdict: 'APPROVED', findings: '' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// REMARKS — collect findings
|
|
110
|
+
const findingsLines = [];
|
|
111
|
+
let inFindings = false;
|
|
112
|
+
|
|
113
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
114
|
+
const t = lines[j].trim();
|
|
115
|
+
if (t === 'FINDINGS_START') { inFindings = true; continue; }
|
|
116
|
+
if (t === 'FINDINGS_END') break;
|
|
117
|
+
if (inFindings) findingsLines.push(lines[j]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fallback: no delimiters — take everything after the verdict line
|
|
121
|
+
if (findingsLines.length === 0) {
|
|
122
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
123
|
+
findingsLines.push(lines[j]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { verdict: 'REMARKS', findings: findingsLines.join('\n').trim() };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { verdict: 'UNKNOWN', findings: '' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Prompt builders ──────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build the ship-check prompt by combining the skill body with phase context
|
|
137
|
+
* and the current repository state.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} skillBody - Skill content with frontmatter stripped
|
|
140
|
+
* @param {import('./plan-parser.mjs').Phase} phase
|
|
141
|
+
* @param {string} repoState - Pre-computed git status / diff summary
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
function buildShipCheckPrompt(skillBody, phase, repoState) {
|
|
145
|
+
return [
|
|
146
|
+
skillBody,
|
|
147
|
+
'',
|
|
148
|
+
'---',
|
|
149
|
+
'',
|
|
150
|
+
`## Phase under review: ${phase.title}`,
|
|
151
|
+
'',
|
|
152
|
+
phase.body.trim(),
|
|
153
|
+
'',
|
|
154
|
+
'## Current repository state',
|
|
155
|
+
'',
|
|
156
|
+
repoState,
|
|
157
|
+
].join('\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build the repair prompt sent after a REMARKS verdict.
|
|
162
|
+
*
|
|
163
|
+
* @param {import('./plan-parser.mjs').Phase} phase
|
|
164
|
+
* @param {string} repoState
|
|
165
|
+
* @param {string} findings - Findings text from the ship-check session
|
|
166
|
+
* @returns {string}
|
|
167
|
+
*/
|
|
168
|
+
function buildRepairPrompt(phase, repoState, findings) {
|
|
169
|
+
return [
|
|
170
|
+
`The ship-check reviewer found issues with the implementation of phase "${phase.title}".`,
|
|
171
|
+
'Please address the following remarks, then the phase will be re-checked.',
|
|
172
|
+
'',
|
|
173
|
+
'## Reviewer findings',
|
|
174
|
+
'',
|
|
175
|
+
findings || '(No specific findings provided.)',
|
|
176
|
+
'',
|
|
177
|
+
'## Phase being reviewed',
|
|
178
|
+
'',
|
|
179
|
+
phase.body.trim(),
|
|
180
|
+
'',
|
|
181
|
+
'## Current repository state',
|
|
182
|
+
'',
|
|
183
|
+
repoState,
|
|
184
|
+
].join('\n');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Session helper ───────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Open a step log, send a prompt, stream the response, close the log.
|
|
191
|
+
*
|
|
192
|
+
* @param {object} opts
|
|
193
|
+
* @param {string} opts.stepName
|
|
194
|
+
* @param {string} opts.phaseName
|
|
195
|
+
* @param {string} opts.prompt
|
|
196
|
+
* @param {import('./log-writer.mjs').LogWriter} opts.logWriter
|
|
197
|
+
* @param {number} opts.phaseNum
|
|
198
|
+
* @param {number} opts.taskNum
|
|
199
|
+
* @param {Function} opts.send
|
|
200
|
+
* @returns {Promise<string>} full response text
|
|
201
|
+
*/
|
|
202
|
+
async function runSession({ stepName, phaseName, prompt, logWriter, phaseNum, taskNum, send }) {
|
|
203
|
+
const step = logWriter.openStep(phaseNum, taskNum, stepName, phaseName);
|
|
204
|
+
step.writeHeader();
|
|
205
|
+
|
|
206
|
+
const started = Date.now();
|
|
207
|
+
let fullText = '';
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
fullText = await send(prompt, {
|
|
211
|
+
onChunk(text) {
|
|
212
|
+
step.writeChunk(text);
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
step.writeFooter(true, Date.now() - started);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
step.writeChunk(`\n\n[error] ${err.message}\n`);
|
|
218
|
+
step.writeFooter(false, Date.now() - started);
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return fullText;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Run the full ship-check (+ optional repair) cycle for a single phase.
|
|
229
|
+
*
|
|
230
|
+
* Sessions written (each gets its own step log):
|
|
231
|
+
* startTaskNum+0 ship-check (initial check)
|
|
232
|
+
* startTaskNum+1 ship-check-repair (only on first REMARKS)
|
|
233
|
+
* startTaskNum+2 ship-check-re (re-check after repair)
|
|
234
|
+
*
|
|
235
|
+
* @param {object} opts
|
|
236
|
+
* @param {import('./plan-parser.mjs').Phase} opts.phase
|
|
237
|
+
* @param {string} opts.repoState - Pre-computed repo state string
|
|
238
|
+
* @param {import('./log-writer.mjs').LogWriter} opts.logWriter
|
|
239
|
+
* @param {number} opts.phaseNum
|
|
240
|
+
* @param {number} opts.startTaskNum
|
|
241
|
+
* @param {Function} opts.send
|
|
242
|
+
* @param {string} [opts._skillsBase] - Base dir for SKILL.md lookup (testing only)
|
|
243
|
+
* @returns {Promise<{ nextTaskNum: number }>}
|
|
244
|
+
* @throws {ShipCheckError} when re-check also returns REMARKS
|
|
245
|
+
*/
|
|
246
|
+
export async function runShipCheck({
|
|
247
|
+
phase,
|
|
248
|
+
repoState,
|
|
249
|
+
logWriter,
|
|
250
|
+
phaseNum,
|
|
251
|
+
startTaskNum,
|
|
252
|
+
send,
|
|
253
|
+
_skillsBase = process.cwd(),
|
|
254
|
+
}) {
|
|
255
|
+
const skillBody = loadSkill(_skillsBase);
|
|
256
|
+
let taskNum = startTaskNum;
|
|
257
|
+
|
|
258
|
+
// ── Initial ship-check ─────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
const initialPrompt = buildShipCheckPrompt(skillBody, phase, repoState);
|
|
261
|
+
|
|
262
|
+
const initialText = await runSession({
|
|
263
|
+
stepName: 'ship-check',
|
|
264
|
+
phaseName: phase.title,
|
|
265
|
+
prompt: initialPrompt,
|
|
266
|
+
logWriter,
|
|
267
|
+
phaseNum,
|
|
268
|
+
taskNum: taskNum++,
|
|
269
|
+
send,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const { verdict: initialVerdict, findings: initialFindings } = parseVerdict(initialText);
|
|
273
|
+
|
|
274
|
+
if (initialVerdict === 'APPROVED') {
|
|
275
|
+
return { nextTaskNum: taskNum };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const findings = initialFindings;
|
|
279
|
+
|
|
280
|
+
// ── Repair session ─────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
const repairPrompt = buildRepairPrompt(phase, repoState, findings);
|
|
283
|
+
|
|
284
|
+
await runSession({
|
|
285
|
+
stepName: 'ship-check-repair',
|
|
286
|
+
phaseName: phase.title,
|
|
287
|
+
prompt: repairPrompt,
|
|
288
|
+
logWriter,
|
|
289
|
+
phaseNum,
|
|
290
|
+
taskNum: taskNum++,
|
|
291
|
+
send,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ── Re-check ───────────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
const reCheckText = await runSession({
|
|
297
|
+
stepName: 'ship-check-re',
|
|
298
|
+
phaseName: phase.title,
|
|
299
|
+
prompt: buildShipCheckPrompt(skillBody, phase, repoState),
|
|
300
|
+
logWriter,
|
|
301
|
+
phaseNum,
|
|
302
|
+
taskNum: taskNum++,
|
|
303
|
+
send,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const { verdict: reVerdict, findings: reFindings } = parseVerdict(reCheckText);
|
|
307
|
+
|
|
308
|
+
if (reVerdict === 'APPROVED') {
|
|
309
|
+
return { nextTaskNum: taskNum };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Second REMARKS → hard stop ─────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
throw new ShipCheckError(
|
|
315
|
+
`Phase "${phase.title}" failed ship-check after one repair attempt. No further action will be taken.`,
|
|
316
|
+
phase.title,
|
|
317
|
+
reFindings || findings
|
|
318
|
+
);
|
|
319
|
+
}
|
package/ralph/lib/verifier.mjs
CHANGED
|
@@ -62,7 +62,7 @@ export class VerificationError extends Error {
|
|
|
62
62
|
* @param {import('./config.mjs').Repo[]} repos
|
|
63
63
|
* @returns {string}
|
|
64
64
|
*/
|
|
65
|
-
function gatherRepoState(repos) {
|
|
65
|
+
export function gatherRepoState(repos) {
|
|
66
66
|
const primaryRepos = repos.filter(r => !r.writableOnly);
|
|
67
67
|
const parts = [];
|
|
68
68
|
|
package/ralph/ralph-claude.mjs
CHANGED
|
@@ -23,8 +23,9 @@ import { prepareBranch, scanChangedRepos, GitCoordinatorError } from './lib/git-
|
|
|
23
23
|
import { LogWriter } from './lib/log-writer.mjs';
|
|
24
24
|
import { loadSafetyHeader } from './lib/safety.mjs';
|
|
25
25
|
import { runImplementation, PhaseExecutorError } from './lib/phase-executor.mjs';
|
|
26
|
-
import { runVerificationLoop, VerificationError } from './lib/verifier.mjs';
|
|
26
|
+
import { runVerificationLoop, VerificationError, gatherRepoState } from './lib/verifier.mjs';
|
|
27
27
|
import { runCommitStep, CommitError } from './lib/committer.mjs';
|
|
28
|
+
import { runShipCheck, ShipCheckError } from './lib/ship-checker.mjs';
|
|
28
29
|
import { mutateCheckboxes } from './lib/plan-mutator.mjs';
|
|
29
30
|
import { deriveBranchName } from './lib/utils.mjs';
|
|
30
31
|
|
|
@@ -91,6 +92,36 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
91
92
|
process.exit(0);
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
// ─── --update-skills ──────────────────────────────────────────────────────────
|
|
96
|
+
// Re-fetch skills from the skills repo and exit. Useful after an upgrade.
|
|
97
|
+
|
|
98
|
+
if (args.includes('--update-skills')) {
|
|
99
|
+
console.log('[ralph] Updating skills via npx skills add tahaJemmali/skills…');
|
|
100
|
+
const result = spawnSync('npx', ['skills', 'add', 'tahaJemmali/skills'], {
|
|
101
|
+
stdio: 'inherit',
|
|
102
|
+
encoding: 'utf8',
|
|
103
|
+
});
|
|
104
|
+
if (result.status !== 0) {
|
|
105
|
+
console.error('[ralph] Failed to update skills. See output above.');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
console.log('[ralph] Skills updated.');
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Skills preflight ─────────────────────────────────────────────────────────
|
|
113
|
+
// Fail fast if skills are not installed — gives a clear actionable message.
|
|
114
|
+
|
|
115
|
+
const skillsDir = resolve(__dirname, '..', 'skills');
|
|
116
|
+
if (!existsSync(skillsDir)) {
|
|
117
|
+
console.error(
|
|
118
|
+
'[ralph] Skills not installed. Run: npx ralph-prd to reinstall.\n' +
|
|
119
|
+
'[ralph] If you recently upgraded ralph-prd, re-run the installer to fetch\n' +
|
|
120
|
+
'[ralph] skills from the new source: npx ralph-prd'
|
|
121
|
+
);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
94
125
|
const planArg = args.find(a => !a.startsWith('--'));
|
|
95
126
|
const isDryRun = args.includes('--dry-run');
|
|
96
127
|
const isReset = args.includes('--reset');
|
|
@@ -101,6 +132,8 @@ const iDidThisArg = args.includes('--i-did-this');
|
|
|
101
132
|
const sendItArg = args.includes('--send-it');
|
|
102
133
|
// Pause and wait for your eyes before each commit. Trust issues? Fair enough.
|
|
103
134
|
const waitForItArg = args.includes('--wait-for-it');
|
|
135
|
+
// Skip the post-commit ship-check step. Use when you don't have a ship-check skill.
|
|
136
|
+
const skipShipCheckArg = args.includes('--skip-ship-check');
|
|
104
137
|
// Run only one specific phase (1-based), force re-run even if already complete.
|
|
105
138
|
const onlyPhaseArg = (() => {
|
|
106
139
|
const idx = args.indexOf('--only-phase');
|
|
@@ -121,7 +154,7 @@ const logLevelArg = (() => {
|
|
|
121
154
|
if (!planArg) {
|
|
122
155
|
console.error(
|
|
123
156
|
'Usage: node ralph-claude.mjs <plan-file.md> ' +
|
|
124
|
-
'[--reset|--dry-run|--i-did-this|--send-it|--wait-for-it|--only-phase N|--log-level none|necessary|dump|--version]'
|
|
157
|
+
'[--reset|--dry-run|--i-did-this|--send-it|--wait-for-it|--skip-ship-check|--only-phase N|--log-level none|necessary|dump|--update-skills|--version]'
|
|
125
158
|
);
|
|
126
159
|
process.exit(1);
|
|
127
160
|
}
|
|
@@ -315,6 +348,7 @@ async function main() {
|
|
|
315
348
|
const iDidThis = iDidThisArg || configFlags.iDidThis;
|
|
316
349
|
const sendIt = sendItArg || configFlags.sendIt;
|
|
317
350
|
const waitForIt = waitForItArg || configFlags.waitForIt;
|
|
351
|
+
const skipShipCheck = skipShipCheckArg || configFlags.skipShipCheck;
|
|
318
352
|
const onlyPhase = onlyPhaseArg ?? configFlags.onlyPhase ?? null;
|
|
319
353
|
const logLevel = logLevelArg ?? configFlags.logLevel ?? 'necessary';
|
|
320
354
|
|
|
@@ -658,6 +692,40 @@ async function main() {
|
|
|
658
692
|
console.log('ok');
|
|
659
693
|
}
|
|
660
694
|
|
|
695
|
+
// ── Ship-check ────────────────────────────────────────────────────────────
|
|
696
|
+
if (skipShipCheck) {
|
|
697
|
+
console.log(` [${ts()}] ship-check… skipped`);
|
|
698
|
+
} else {
|
|
699
|
+
const repoState = gatherRepoState(repos);
|
|
700
|
+
const shipCheckStart = Date.now();
|
|
701
|
+
process.stdout.write(` [${ts()}] ship-check… `);
|
|
702
|
+
try {
|
|
703
|
+
({ nextTaskNum: taskNum } = await runShipCheck({
|
|
704
|
+
phase,
|
|
705
|
+
repoState,
|
|
706
|
+
logWriter,
|
|
707
|
+
phaseNum,
|
|
708
|
+
startTaskNum: taskNum,
|
|
709
|
+
send,
|
|
710
|
+
}));
|
|
711
|
+
const dur = ((Date.now() - shipCheckStart) / 1000).toFixed(1);
|
|
712
|
+
console.log(`VERDICT: APPROVED (${dur}s)`);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
const dur = ((Date.now() - shipCheckStart) / 1000).toFixed(1);
|
|
715
|
+
if (err instanceof ShipCheckError) {
|
|
716
|
+
console.log(`VERDICT: REMARKS (${dur}s)`);
|
|
717
|
+
console.error(`\nShip-check failed for phase "${err.phaseName}":`);
|
|
718
|
+
if (err.findings) console.error(err.findings);
|
|
719
|
+
} else {
|
|
720
|
+
console.log('failed');
|
|
721
|
+
console.error(`\nUnexpected error during ship-check: ${err.message}`);
|
|
722
|
+
}
|
|
723
|
+
console.error(`Logs: ${logsDir}`);
|
|
724
|
+
notify('Ralph — failed', `Ship-check failed for "${phase.title}"`);
|
|
725
|
+
process.exit(1);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
661
729
|
// ── Checkbox mutation + state persistence ────────────────────────────────
|
|
662
730
|
// Skip state writes when --only-phase is active (it's a force re-run, not a progression).
|
|
663
731
|
if (onlyPhase === null) {
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
# ── Repositories ──────────────────────────────────────────────────────────────
|
|
8
8
|
# List every git repo Ralph is allowed to read and write.
|
|
9
9
|
# Paths are relative to this config file.
|
|
10
|
+
# repos:
|
|
11
|
+
# - name: backend
|
|
12
|
+
# path: ../backend
|
|
13
|
+
# - name: frontend
|
|
14
|
+
# path: ../frontend
|
|
10
15
|
|
|
11
|
-
repos:
|
|
12
|
-
- name: backend
|
|
13
|
-
path: ../../ebchat-saas-backend
|
|
14
|
-
- name: dashboard
|
|
15
|
-
path: ../../ebchat-saas-dashboard
|
|
16
16
|
|
|
17
17
|
# ── Extra writable directories ────────────────────────────────────────────────
|
|
18
18
|
# Directories that are not git roots but Ralph may write to (e.g. shared docs).
|
|
@@ -48,3 +48,8 @@ flags:
|
|
|
48
48
|
# necessary — verdicts (pass/fail), missed criteria, phase progress (default)
|
|
49
49
|
# dump — full streamed model output (verbose)
|
|
50
50
|
logLevel: necessary
|
|
51
|
+
|
|
52
|
+
# Skip the post-commit ship-check step for every phase. Set to true if you
|
|
53
|
+
# have not installed a ship-check skill or want to disable the review gate.
|
|
54
|
+
# CLI equivalent: --skip-ship-check
|
|
55
|
+
skipShipCheck: false
|