ralph-prd 1.0.2 → 1.0.3
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/package.json +2 -2
- package/ralph/lib/config.mjs +5 -1
- package/ralph/lib/log-writer.mjs +27 -8
- package/ralph/lib/verifier.mjs +14 -10
- package/ralph/ralph-claude.mjs +41 -26
- package/ralph/ralph.config.sample.yaml +6 -0
- package/ralph/test/committer.test.mjs +7 -7
- package/ralph/test/e2e.test.mjs +3 -3
- package/ralph/test/log-writer.test.mjs +49 -13
- package/ralph/test/phase-executor.test.mjs +5 -5
- package/ralph/test/verifier.test.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ralph-prd",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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"
|
|
@@ -30,4 +30,4 @@
|
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=18"
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
package/ralph/lib/config.mjs
CHANGED
|
@@ -28,6 +28,7 @@ function isGitRepo(dirPath) {
|
|
|
28
28
|
* @property {boolean} waitForIt - Pause for user confirmation before each commit step
|
|
29
29
|
* @property {number} maxRepairs - Max repair attempts per phase before hard-stopping (default 3)
|
|
30
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")
|
|
31
32
|
*/
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -58,7 +59,7 @@ function isGitRepo(dirPath) {
|
|
|
58
59
|
function parseConfigYaml(content) {
|
|
59
60
|
const repos = [];
|
|
60
61
|
const writableDirs = [];
|
|
61
|
-
const flags = { iDidThis: false, sendIt: false, waitForIt: false, maxRepairs: 3, onlyPhase: null };
|
|
62
|
+
const flags = { iDidThis: false, sendIt: false, waitForIt: false, maxRepairs: 3, onlyPhase: null, logLevel: 'necessary' };
|
|
62
63
|
const hooks = { afterCommit: null };
|
|
63
64
|
let section = null;
|
|
64
65
|
let current = null;
|
|
@@ -119,6 +120,9 @@ function parseConfigYaml(content) {
|
|
|
119
120
|
} else if (key === 'onlyPhase') {
|
|
120
121
|
const n = parseInt(trimmedVal, 10);
|
|
121
122
|
if (!isNaN(n) && n > 0) flags.onlyPhase = n;
|
|
123
|
+
} else if (key === 'logLevel') {
|
|
124
|
+
const valid = ['none', 'necessary', 'dump'];
|
|
125
|
+
if (valid.includes(trimmedVal)) flags.logLevel = trimmedVal;
|
|
122
126
|
} else {
|
|
123
127
|
flags[key] = trimmedVal === 'true';
|
|
124
128
|
}
|
package/ralph/lib/log-writer.mjs
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Log writer for Ralph phase sessions.
|
|
5
5
|
*
|
|
6
|
+
* Log levels:
|
|
7
|
+
* "none" — no files written at all
|
|
8
|
+
* "necessary" — headers, footers, and verdict files only (pass/fail, progress)
|
|
9
|
+
* "dump" — full streamed output (current behaviour)
|
|
10
|
+
*
|
|
6
11
|
* Each run gets a directory (passed in from the orchestrator, derived from the
|
|
7
12
|
* plan name + timestamp). Within that directory:
|
|
8
13
|
*
|
|
@@ -11,8 +16,9 @@
|
|
|
11
16
|
*
|
|
12
17
|
* Public API:
|
|
13
18
|
* class LogWriter
|
|
14
|
-
* constructor(logsDir
|
|
15
|
-
* openStep(n, name, phaseName)
|
|
19
|
+
* constructor(logsDir, logLevel) ensures the directory exists (unless "none")
|
|
20
|
+
* openStep(n, name, phaseName) returns a StepLog
|
|
21
|
+
* logLevel: string
|
|
16
22
|
*
|
|
17
23
|
* class StepLog
|
|
18
24
|
* writeHeader()
|
|
@@ -24,6 +30,9 @@
|
|
|
24
30
|
import { mkdirSync, appendFileSync, writeFileSync } from 'fs';
|
|
25
31
|
import { join } from 'path';
|
|
26
32
|
|
|
33
|
+
/** Valid log levels. */
|
|
34
|
+
export const LOG_LEVELS = ['none', 'necessary', 'dump'];
|
|
35
|
+
|
|
27
36
|
// ─── StepLog ─────────────────────────────────────────────────────────────────
|
|
28
37
|
|
|
29
38
|
export class StepLog {
|
|
@@ -33,17 +42,20 @@ export class StepLog {
|
|
|
33
42
|
* @param {number} taskNum - 1-based task number within the phase
|
|
34
43
|
* @param {string} name - Task name, e.g. "implementation"
|
|
35
44
|
* @param {string} phaseName
|
|
45
|
+
* @param {string} logLevel - "none" | "necessary" | "dump"
|
|
36
46
|
*/
|
|
37
|
-
constructor(logsDir, phaseNum, _taskNum, name, phaseName) {
|
|
47
|
+
constructor(logsDir, phaseNum, _taskNum, name, phaseName, logLevel) {
|
|
38
48
|
this._logsDir = logsDir;
|
|
39
49
|
this._lastMessagePath = join(logsDir, 'last-message.txt');
|
|
40
50
|
this.filePath = join(logsDir, `phase-${phaseNum}-${name}.log`);
|
|
41
51
|
this._phaseName = phaseName;
|
|
42
52
|
this._name = name;
|
|
53
|
+
this._logLevel = logLevel;
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
/** Write the step header line. */
|
|
46
57
|
writeHeader() {
|
|
58
|
+
if (this._logLevel === 'none') return;
|
|
47
59
|
const ts = new Date().toISOString();
|
|
48
60
|
const line =
|
|
49
61
|
`=== Phase: ${this._phaseName} | Step: ${this._name} | Started: ${ts} ===\n\n`;
|
|
@@ -54,9 +66,12 @@ export class StepLog {
|
|
|
54
66
|
* Append a streamed text chunk to the step log and overwrite last-message.txt.
|
|
55
67
|
* Calling this repeatedly builds up both files incrementally.
|
|
56
68
|
*
|
|
69
|
+
* Only writes at "dump" level — "necessary" skips the verbose stream.
|
|
70
|
+
*
|
|
57
71
|
* @param {string} text
|
|
58
72
|
*/
|
|
59
73
|
writeChunk(text) {
|
|
74
|
+
if (this._logLevel !== 'dump') return;
|
|
60
75
|
appendFileSync(this.filePath, text, 'utf8');
|
|
61
76
|
writeFileSync(this._lastMessagePath, text, 'utf8');
|
|
62
77
|
}
|
|
@@ -68,6 +83,7 @@ export class StepLog {
|
|
|
68
83
|
* @param {number} durationMs
|
|
69
84
|
*/
|
|
70
85
|
writeFooter(ok, durationMs) {
|
|
86
|
+
if (this._logLevel === 'none') return;
|
|
71
87
|
const status = ok ? 'ok' : 'failed';
|
|
72
88
|
const seconds = (durationMs / 1000).toFixed(1);
|
|
73
89
|
const line = `\n\n=== Exit: ${status} | Duration: ${seconds}s ===\n`;
|
|
@@ -79,12 +95,15 @@ export class StepLog {
|
|
|
79
95
|
|
|
80
96
|
export class LogWriter {
|
|
81
97
|
/**
|
|
82
|
-
* @param {string} logsDir
|
|
83
|
-
*
|
|
98
|
+
* @param {string} logsDir - Absolute path to the run directory.
|
|
99
|
+
* @param {string} [logLevel="necessary"] - "none" | "necessary" | "dump"
|
|
84
100
|
*/
|
|
85
|
-
constructor(logsDir) {
|
|
101
|
+
constructor(logsDir, logLevel = 'necessary') {
|
|
86
102
|
this.logsDir = logsDir;
|
|
87
|
-
|
|
103
|
+
this.logLevel = logLevel;
|
|
104
|
+
if (logLevel !== 'none') {
|
|
105
|
+
mkdirSync(logsDir, { recursive: true });
|
|
106
|
+
}
|
|
88
107
|
}
|
|
89
108
|
|
|
90
109
|
/**
|
|
@@ -97,6 +116,6 @@ export class LogWriter {
|
|
|
97
116
|
* @returns {StepLog}
|
|
98
117
|
*/
|
|
99
118
|
openStep(phaseNum, taskNum, name, phaseName) {
|
|
100
|
-
return new StepLog(this.logsDir, phaseNum, taskNum, name, phaseName);
|
|
119
|
+
return new StepLog(this.logsDir, phaseNum, taskNum, name, phaseName, this.logLevel);
|
|
101
120
|
}
|
|
102
121
|
}
|
package/ralph/lib/verifier.mjs
CHANGED
|
@@ -313,11 +313,13 @@ export async function runVerificationLoop({
|
|
|
313
313
|
|
|
314
314
|
const { verdict: initialVerdict, failureNotes: initialNotes } = parseVerdict(initialText);
|
|
315
315
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
316
|
+
if (logWriter.logLevel !== 'none') {
|
|
317
|
+
writeFileSync(
|
|
318
|
+
join(logWriter.logsDir, `verdict-phase-${phaseNum}-verification.md`),
|
|
319
|
+
`VERDICT: ${initialVerdict}\n` + (initialNotes ? `\nFAILURE_NOTES:\n${initialNotes}\n` : ''),
|
|
320
|
+
'utf8'
|
|
321
|
+
);
|
|
322
|
+
}
|
|
321
323
|
|
|
322
324
|
if (initialVerdict === 'PASS') {
|
|
323
325
|
return { nextTaskNum: taskNum, repairCount: 0 };
|
|
@@ -363,11 +365,13 @@ export async function runVerificationLoop({
|
|
|
363
365
|
|
|
364
366
|
const { verdict, failureNotes } = parseVerdict(reVerifyText);
|
|
365
367
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
368
|
+
if (logWriter.logLevel !== 'none') {
|
|
369
|
+
writeFileSync(
|
|
370
|
+
join(logWriter.logsDir, `verdict-phase-${phaseNum}-re-verification-${attempt}.md`),
|
|
371
|
+
`VERDICT: ${verdict}\n` + (failureNotes ? `\nFAILURE_NOTES:\n${failureNotes}\n` : ''),
|
|
372
|
+
'utf8'
|
|
373
|
+
);
|
|
374
|
+
}
|
|
371
375
|
|
|
372
376
|
if (verdict === 'PASS') {
|
|
373
377
|
return { nextTaskNum: taskNum, repairCount: attempt };
|
package/ralph/ralph-claude.mjs
CHANGED
|
@@ -109,11 +109,19 @@ const onlyPhaseArg = (() => {
|
|
|
109
109
|
if (eqArg) return parseInt(eqArg.slice('--only-phase='.length), 10);
|
|
110
110
|
return null;
|
|
111
111
|
})();
|
|
112
|
+
// Log level: none | necessary | dump (default: necessary from config)
|
|
113
|
+
const logLevelArg = (() => {
|
|
114
|
+
const idx = args.indexOf('--log-level');
|
|
115
|
+
if (idx !== -1 && args[idx + 1] !== undefined) return args[idx + 1];
|
|
116
|
+
const eqArg = args.find(a => a.startsWith('--log-level='));
|
|
117
|
+
if (eqArg) return eqArg.slice('--log-level='.length);
|
|
118
|
+
return null;
|
|
119
|
+
})();
|
|
112
120
|
|
|
113
121
|
if (!planArg) {
|
|
114
122
|
console.error(
|
|
115
123
|
'Usage: node ralph-claude.mjs <plan-file.md> ' +
|
|
116
|
-
'[--reset|--dry-run|--i-did-this|--send-it|--wait-for-it|--only-phase N|--version]'
|
|
124
|
+
'[--reset|--dry-run|--i-did-this|--send-it|--wait-for-it|--only-phase N|--log-level none|necessary|dump|--version]'
|
|
117
125
|
);
|
|
118
126
|
process.exit(1);
|
|
119
127
|
}
|
|
@@ -211,7 +219,7 @@ function pushAndOpenPR(repos, branch, planPath, planContent, phases) {
|
|
|
211
219
|
}
|
|
212
220
|
}
|
|
213
221
|
|
|
214
|
-
function printHeader({ planPath, branch, repos, logsDir, phases, currentPhaseIndex, state }) {
|
|
222
|
+
function printHeader({ planPath, branch, repos, logsDir, logLevel, phases, currentPhaseIndex, state }) {
|
|
215
223
|
const primaryRepos = repos.filter(r => !r.writableOnly);
|
|
216
224
|
const writableDirs = repos.filter(r => r.writableOnly);
|
|
217
225
|
const allDone = currentPhaseIndex === null;
|
|
@@ -234,7 +242,11 @@ function printHeader({ planPath, branch, repos, logsDir, phases, currentPhaseInd
|
|
|
234
242
|
for (const r of writableDirs) console.log(` ${r.path}`);
|
|
235
243
|
}
|
|
236
244
|
|
|
237
|
-
|
|
245
|
+
if (logLevel !== 'none') {
|
|
246
|
+
console.log(`Logs: ${logsDir} (${logLevel})`);
|
|
247
|
+
} else {
|
|
248
|
+
console.log('Logs: disabled');
|
|
249
|
+
}
|
|
238
250
|
|
|
239
251
|
if (allDone) {
|
|
240
252
|
console.log(`Status: all ${phases.length} phases complete`);
|
|
@@ -304,6 +316,7 @@ async function main() {
|
|
|
304
316
|
const sendIt = sendItArg || configFlags.sendIt;
|
|
305
317
|
const waitForIt = waitForItArg || configFlags.waitForIt;
|
|
306
318
|
const onlyPhase = onlyPhaseArg ?? configFlags.onlyPhase ?? null;
|
|
319
|
+
const logLevel = logLevelArg ?? configFlags.logLevel ?? 'necessary';
|
|
307
320
|
|
|
308
321
|
// Load state & find first incomplete phase
|
|
309
322
|
const state = loadState(planPath);
|
|
@@ -331,7 +344,7 @@ async function main() {
|
|
|
331
344
|
}
|
|
332
345
|
|
|
333
346
|
// Print run header
|
|
334
|
-
printHeader({ planPath, branch, repos, logsDir, phases, currentPhaseIndex: onlyPhase !== null ? (onlyPhase - 1) : currentPhaseIndex, state });
|
|
347
|
+
printHeader({ planPath, branch, repos, logsDir, logLevel, phases, currentPhaseIndex: onlyPhase !== null ? (onlyPhase - 1) : currentPhaseIndex, state });
|
|
335
348
|
|
|
336
349
|
// Show update notice if available
|
|
337
350
|
const update = await updateCheck;
|
|
@@ -360,26 +373,28 @@ async function main() {
|
|
|
360
373
|
console.log(` (${detail})`);
|
|
361
374
|
}
|
|
362
375
|
|
|
363
|
-
// Write dry-run log
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
376
|
+
// Write dry-run log (unless logging is disabled)
|
|
377
|
+
if (logLevel !== 'none') {
|
|
378
|
+
mkdirSync(logsDir, { recursive: true });
|
|
379
|
+
const dryRunLines = [
|
|
380
|
+
`Ralph dry-run — ${new Date().toISOString()}`,
|
|
381
|
+
`Plan: ${planPath}`,
|
|
382
|
+
`Branch: ${branch}`,
|
|
383
|
+
`Repos: ${repos.filter(r => !r.writableOnly).map(r => r.path).join(', ')}`,
|
|
384
|
+
'',
|
|
385
|
+
'Phases:',
|
|
386
|
+
...phases.map(p => {
|
|
387
|
+
const done = state.completedPhases.includes(p.index);
|
|
388
|
+
const check = done ? 'x' : ' ';
|
|
389
|
+
const detail = p.hasVerification
|
|
390
|
+
? ` (${p.acceptanceCriteria.length} criteria)`
|
|
391
|
+
: ' (no verification)';
|
|
392
|
+
return ` [${check}] ${p.title}${detail}`;
|
|
393
|
+
}),
|
|
394
|
+
];
|
|
395
|
+
writeFileSync(resolve(logsDir, 'dry-run.log'), dryRunLines.join('\n') + '\n', 'utf8');
|
|
396
|
+
console.log(`\n[dry-run] Log written to: ${logsDir}/dry-run.log`);
|
|
397
|
+
}
|
|
383
398
|
process.exit(0);
|
|
384
399
|
}
|
|
385
400
|
|
|
@@ -423,7 +438,7 @@ async function main() {
|
|
|
423
438
|
|
|
424
439
|
// planContent already read during validation above
|
|
425
440
|
const safetyHeader = loadSafetyHeader(__dirname);
|
|
426
|
-
const logWriter = new LogWriter(logsDir);
|
|
441
|
+
const logWriter = new LogWriter(logsDir, logLevel);
|
|
427
442
|
|
|
428
443
|
// Track phases already complete before this run (for summary)
|
|
429
444
|
const previouslyCompleted = phases
|
|
@@ -653,7 +668,7 @@ async function main() {
|
|
|
653
668
|
console.log(`Duration: ${durationSec}s`);
|
|
654
669
|
if (totalCost > 0) console.log(`API cost: ${totalCost.toFixed(4)}`);
|
|
655
670
|
console.log(`Branch: ${branch}`);
|
|
656
|
-
console.log(`Logs: ${logsDir}`);
|
|
671
|
+
if (logLevel !== 'none') console.log(`Logs: ${logsDir}`);
|
|
657
672
|
console.log(LINE);
|
|
658
673
|
|
|
659
674
|
notify('Ralph — complete', `${phaseResults.length} phase${phaseResults.length === 1 ? '' : 's'} done in ${durationSec}s`);
|
|
@@ -42,3 +42,9 @@ flags:
|
|
|
42
42
|
# gives up and exits. Increase if your phases are complex and need
|
|
43
43
|
# more attempts to pass all acceptance criteria.
|
|
44
44
|
maxRepairs: 3
|
|
45
|
+
|
|
46
|
+
# Log level controls what gets written to disk.
|
|
47
|
+
# none — no log files at all
|
|
48
|
+
# necessary — verdicts (pass/fail), missed criteria, phase progress (default)
|
|
49
|
+
# dump — full streamed model output (verbose)
|
|
50
|
+
logLevel: necessary
|
|
@@ -20,7 +20,7 @@ describe('runCommitStep', () => {
|
|
|
20
20
|
test('no changed repos → skips send(), returns anyCommitted=false', async () => {
|
|
21
21
|
const repoDir = makeTempRepo(); // clean after initial commit
|
|
22
22
|
const logDir = makeTempDir();
|
|
23
|
-
const lw = new LogWriter(logDir);
|
|
23
|
+
const lw = new LogWriter(logDir, 'dump');
|
|
24
24
|
let sendCalled = false;
|
|
25
25
|
const send = async () => { sendCalled = true; return ''; };
|
|
26
26
|
|
|
@@ -43,7 +43,7 @@ describe('runCommitStep', () => {
|
|
|
43
43
|
writeFileSync(join(repoDir, 'feature.txt'), 'content\n');
|
|
44
44
|
|
|
45
45
|
const logDir = makeTempDir();
|
|
46
|
-
const lw = new LogWriter(logDir);
|
|
46
|
+
const lw = new LogWriter(logDir, 'dump');
|
|
47
47
|
const send = makeFakeSend([
|
|
48
48
|
'REPO: myrepo\nFILES:\n- feature.txt\nCOMMIT: ralph: add feature',
|
|
49
49
|
]);
|
|
@@ -67,7 +67,7 @@ describe('runCommitStep', () => {
|
|
|
67
67
|
writeFileSync(join(repoDir, 'irrelevant.txt'), 'stuff\n');
|
|
68
68
|
|
|
69
69
|
const logDir = makeTempDir();
|
|
70
|
-
const lw = new LogWriter(logDir);
|
|
70
|
+
const lw = new LogWriter(logDir, 'dump');
|
|
71
71
|
const send = makeFakeSend(['REPO: myrepo\nSKIP']);
|
|
72
72
|
|
|
73
73
|
const { anyCommitted } = await runCommitStep({
|
|
@@ -87,7 +87,7 @@ describe('runCommitStep', () => {
|
|
|
87
87
|
writeFileSync(join(repoDir, 'foo.txt'), 'foo\n');
|
|
88
88
|
|
|
89
89
|
const logDir = makeTempDir();
|
|
90
|
-
const lw = new LogWriter(logDir);
|
|
90
|
+
const lw = new LogWriter(logDir, 'dump');
|
|
91
91
|
// Model omits the "ralph:" prefix — runner should add it
|
|
92
92
|
const send = makeFakeSend([
|
|
93
93
|
'REPO: r\nFILES:\n- foo.txt\nCOMMIT: add foo feature',
|
|
@@ -112,7 +112,7 @@ describe('runCommitStep', () => {
|
|
|
112
112
|
writeFileSync(join(dirtyRepo, 'new.txt'), 'hi\n');
|
|
113
113
|
|
|
114
114
|
const logDir = makeTempDir();
|
|
115
|
-
const lw = new LogWriter(logDir);
|
|
115
|
+
const lw = new LogWriter(logDir, 'dump');
|
|
116
116
|
const send = makeFakeSend([
|
|
117
117
|
'REPO: dirty\nFILES:\n- new.txt\nCOMMIT: ralph: add new file',
|
|
118
118
|
]);
|
|
@@ -145,7 +145,7 @@ describe('runCommitStep', () => {
|
|
|
145
145
|
writeFileSync(join(repoDir, 'x.txt'), 'x\n');
|
|
146
146
|
|
|
147
147
|
const logDir = makeTempDir();
|
|
148
|
-
const lw = new LogWriter(logDir);
|
|
148
|
+
const lw = new LogWriter(logDir, 'dump');
|
|
149
149
|
const send = async () => { throw new Error('transport down'); };
|
|
150
150
|
|
|
151
151
|
await assert.rejects(
|
|
@@ -170,7 +170,7 @@ describe('runCommitStep', () => {
|
|
|
170
170
|
writeFileSync(join(repoDir, 'f.txt'), 'f\n');
|
|
171
171
|
|
|
172
172
|
const logDir = makeTempDir();
|
|
173
|
-
const lw = new LogWriter(logDir);
|
|
173
|
+
const lw = new LogWriter(logDir, 'dump');
|
|
174
174
|
const send = makeFakeSend([
|
|
175
175
|
'REPO: r\nFILES:\n- f.txt\nCOMMIT: ralph: commit f',
|
|
176
176
|
]);
|
package/ralph/test/e2e.test.mjs
CHANGED
|
@@ -78,7 +78,7 @@ describe('e2e: full single-phase run with fake transport', () => {
|
|
|
78
78
|
const repoDir = makeTempRepo();
|
|
79
79
|
const planPath = makePlanFile(SINGLE_PHASE_PLAN);
|
|
80
80
|
const logDir = makeTempDir();
|
|
81
|
-
const lw = new LogWriter(logDir);
|
|
81
|
+
const lw = new LogWriter(logDir, 'dump');
|
|
82
82
|
|
|
83
83
|
const { phases } = parsePlanContent(SINGLE_PHASE_PLAN);
|
|
84
84
|
const phase = phases[0];
|
|
@@ -129,7 +129,7 @@ describe('e2e: repair-loop run (fail → repair → pass)', () => {
|
|
|
129
129
|
const repoDir = makeTempRepo();
|
|
130
130
|
const planPath = makePlanFile(SINGLE_PHASE_PLAN);
|
|
131
131
|
const logDir = makeTempDir();
|
|
132
|
-
const lw = new LogWriter(logDir);
|
|
132
|
+
const lw = new LogWriter(logDir, 'dump');
|
|
133
133
|
|
|
134
134
|
const { phases } = parsePlanContent(SINGLE_PHASE_PLAN);
|
|
135
135
|
const phase = phases[0];
|
|
@@ -187,7 +187,7 @@ describe('e2e: double-fail run exits non-zero and preserves logs', () => {
|
|
|
187
187
|
const repoDir = makeTempRepo();
|
|
188
188
|
const planPath = makePlanFile(SINGLE_PHASE_PLAN);
|
|
189
189
|
const logDir = makeTempDir();
|
|
190
|
-
const lw = new LogWriter(logDir);
|
|
190
|
+
const lw = new LogWriter(logDir, 'dump');
|
|
191
191
|
|
|
192
192
|
const { phases } = parsePlanContent(SINGLE_PHASE_PLAN);
|
|
193
193
|
const phase = phases[0];
|
|
@@ -9,21 +9,21 @@ describe('log-writer', () => {
|
|
|
9
9
|
|
|
10
10
|
test('constructor creates the run directory', () => {
|
|
11
11
|
const dir = join(makeTempDir(), 'logs', 'run-1');
|
|
12
|
-
new LogWriter(dir);
|
|
12
|
+
new LogWriter(dir, 'dump');
|
|
13
13
|
assert.ok(existsSync(dir));
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
test('step log filename follows
|
|
16
|
+
test('step log filename follows phase-N-name.log convention', () => {
|
|
17
17
|
const dir = makeTempDir();
|
|
18
|
-
const lw = new LogWriter(dir);
|
|
19
|
-
const step = lw.openStep(3, 'implementation', 'Phase 1');
|
|
20
|
-
assert.ok(step.filePath.endsWith('
|
|
18
|
+
const lw = new LogWriter(dir, 'dump');
|
|
19
|
+
const step = lw.openStep(3, 1, 'implementation', 'Phase 1');
|
|
20
|
+
assert.ok(step.filePath.endsWith('phase-3-implementation.log'));
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
test('writeHeader includes phase name, step name, and timestamp', () => {
|
|
24
24
|
const dir = makeTempDir();
|
|
25
|
-
const lw = new LogWriter(dir);
|
|
26
|
-
const step = lw.openStep(1, 'verification', 'Phase 2: Transport');
|
|
25
|
+
const lw = new LogWriter(dir, 'dump');
|
|
26
|
+
const step = lw.openStep(1, 1, 'verification', 'Phase 2: Transport');
|
|
27
27
|
step.writeHeader();
|
|
28
28
|
|
|
29
29
|
const content = readFileSync(step.filePath, 'utf8');
|
|
@@ -35,7 +35,7 @@ describe('log-writer', () => {
|
|
|
35
35
|
|
|
36
36
|
test('writeChunk appends to step log', () => {
|
|
37
37
|
const dir = makeTempDir();
|
|
38
|
-
const lw = new LogWriter(dir);
|
|
38
|
+
const lw = new LogWriter(dir, 'dump');
|
|
39
39
|
const step = lw.openStep(1, 'implementation', 'P1');
|
|
40
40
|
step.writeHeader();
|
|
41
41
|
step.writeChunk('hello ');
|
|
@@ -47,7 +47,7 @@ describe('log-writer', () => {
|
|
|
47
47
|
|
|
48
48
|
test('writeChunk updates last-message.txt with latest chunk', () => {
|
|
49
49
|
const dir = makeTempDir();
|
|
50
|
-
const lw = new LogWriter(dir);
|
|
50
|
+
const lw = new LogWriter(dir, 'dump');
|
|
51
51
|
const step = lw.openStep(1, 'implementation', 'P1');
|
|
52
52
|
step.writeChunk('first');
|
|
53
53
|
step.writeChunk('second');
|
|
@@ -59,7 +59,7 @@ describe('log-writer', () => {
|
|
|
59
59
|
|
|
60
60
|
test('writeFooter includes exit status and duration', () => {
|
|
61
61
|
const dir = makeTempDir();
|
|
62
|
-
const lw = new LogWriter(dir);
|
|
62
|
+
const lw = new LogWriter(dir, 'dump');
|
|
63
63
|
const step = lw.openStep(1, 'commit', 'P1');
|
|
64
64
|
step.writeHeader();
|
|
65
65
|
step.writeFooter(true, 2500);
|
|
@@ -71,7 +71,7 @@ describe('log-writer', () => {
|
|
|
71
71
|
|
|
72
72
|
test('writeFooter with ok=false includes "failed"', () => {
|
|
73
73
|
const dir = makeTempDir();
|
|
74
|
-
const lw = new LogWriter(dir);
|
|
74
|
+
const lw = new LogWriter(dir, 'dump');
|
|
75
75
|
const step = lw.openStep(1, 'verification', 'P1');
|
|
76
76
|
step.writeHeader();
|
|
77
77
|
step.writeFooter(false, 1000);
|
|
@@ -82,7 +82,7 @@ describe('log-writer', () => {
|
|
|
82
82
|
|
|
83
83
|
test('step log file persists even if footer not written (failure mid-stream)', () => {
|
|
84
84
|
const dir = makeTempDir();
|
|
85
|
-
const lw = new LogWriter(dir);
|
|
85
|
+
const lw = new LogWriter(dir, 'dump');
|
|
86
86
|
const step = lw.openStep(1, 'implementation', 'P1');
|
|
87
87
|
step.writeHeader();
|
|
88
88
|
step.writeChunk('partial output…');
|
|
@@ -95,7 +95,7 @@ describe('log-writer', () => {
|
|
|
95
95
|
|
|
96
96
|
test('multiple steps in same run get separate log files', () => {
|
|
97
97
|
const dir = makeTempDir();
|
|
98
|
-
const lw = new LogWriter(dir);
|
|
98
|
+
const lw = new LogWriter(dir, 'dump');
|
|
99
99
|
const s1 = lw.openStep(1, 'implementation', 'P1');
|
|
100
100
|
const s2 = lw.openStep(2, 'verification', 'P1');
|
|
101
101
|
s1.writeHeader();
|
|
@@ -108,4 +108,40 @@ describe('log-writer', () => {
|
|
|
108
108
|
assert.ok(readFileSync(s2.filePath, 'utf8').includes('verify output'));
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
// ─── Log level: necessary ──────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
test('necessary level writes header and footer but skips chunks', () => {
|
|
114
|
+
const dir = makeTempDir();
|
|
115
|
+
const lw = new LogWriter(dir, 'necessary');
|
|
116
|
+
const step = lw.openStep(1, 'implementation', 'P1');
|
|
117
|
+
step.writeHeader();
|
|
118
|
+
step.writeChunk('this should not appear');
|
|
119
|
+
step.writeFooter(true, 1000);
|
|
120
|
+
|
|
121
|
+
const content = readFileSync(step.filePath, 'utf8');
|
|
122
|
+
assert.ok(content.includes('P1'));
|
|
123
|
+
assert.ok(content.includes('ok'));
|
|
124
|
+
assert.ok(!content.includes('this should not appear'));
|
|
125
|
+
assert.ok(!existsSync(join(dir, 'last-message.txt')));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ─── Log level: none ───────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
test('none level does not create logs directory', () => {
|
|
131
|
+
const dir = join(makeTempDir(), 'should-not-exist');
|
|
132
|
+
new LogWriter(dir, 'none');
|
|
133
|
+
assert.ok(!existsSync(dir));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('none level writes nothing', () => {
|
|
137
|
+
const dir = makeTempDir();
|
|
138
|
+
const lw = new LogWriter(dir, 'none');
|
|
139
|
+
const step = lw.openStep(1, 'implementation', 'P1');
|
|
140
|
+
step.writeHeader();
|
|
141
|
+
step.writeChunk('invisible');
|
|
142
|
+
step.writeFooter(true, 500);
|
|
143
|
+
|
|
144
|
+
assert.ok(!existsSync(step.filePath));
|
|
145
|
+
});
|
|
146
|
+
|
|
111
147
|
});
|
|
@@ -95,7 +95,7 @@ describe('runImplementation', () => {
|
|
|
95
95
|
|
|
96
96
|
test('returns the full response text from send()', async () => {
|
|
97
97
|
const dir = makeTempDir();
|
|
98
|
-
const lw = new LogWriter(dir);
|
|
98
|
+
const lw = new LogWriter(dir, 'dump');
|
|
99
99
|
const send = makeFakeSend(['I implemented the CLI.']);
|
|
100
100
|
|
|
101
101
|
const result = await runImplementation({
|
|
@@ -114,7 +114,7 @@ describe('runImplementation', () => {
|
|
|
114
114
|
|
|
115
115
|
test('writes a step log with header and footer', async () => {
|
|
116
116
|
const dir = makeTempDir();
|
|
117
|
-
const lw = new LogWriter(dir);
|
|
117
|
+
const lw = new LogWriter(dir, 'dump');
|
|
118
118
|
const send = makeFakeSend(['done']);
|
|
119
119
|
|
|
120
120
|
const step = lw.openStep(1, 'implementation', PHASE.title);
|
|
@@ -141,7 +141,7 @@ describe('runImplementation', () => {
|
|
|
141
141
|
|
|
142
142
|
test('isDryRun → does not call send, logs prompt instead', async () => {
|
|
143
143
|
const dir = makeTempDir();
|
|
144
|
-
const lw = new LogWriter(dir);
|
|
144
|
+
const lw = new LogWriter(dir, 'dump');
|
|
145
145
|
let sendCalled = false;
|
|
146
146
|
const send = async () => { sendCalled = true; return ''; };
|
|
147
147
|
|
|
@@ -161,7 +161,7 @@ describe('runImplementation', () => {
|
|
|
161
161
|
|
|
162
162
|
test('send() failure → throws PhaseExecutorError with phase and step info', async () => {
|
|
163
163
|
const dir = makeTempDir();
|
|
164
|
-
const lw = new LogWriter(dir);
|
|
164
|
+
const lw = new LogWriter(dir, 'dump');
|
|
165
165
|
const send = async () => { throw new Error('network timeout'); };
|
|
166
166
|
|
|
167
167
|
await assert.rejects(
|
|
@@ -186,7 +186,7 @@ describe('runImplementation', () => {
|
|
|
186
186
|
|
|
187
187
|
test('step log preserved even when send() throws', async () => {
|
|
188
188
|
const dir = makeTempDir();
|
|
189
|
-
const lw = new LogWriter(dir);
|
|
189
|
+
const lw = new LogWriter(dir, 'dump');
|
|
190
190
|
const send = async (prompt, { onChunk } = {}) => {
|
|
191
191
|
onChunk?.('partial…');
|
|
192
192
|
throw new Error('boom');
|
|
@@ -17,7 +17,7 @@ const PHASE = {
|
|
|
17
17
|
function makeSetup() {
|
|
18
18
|
const dir = makeTempDir();
|
|
19
19
|
const repoDir = makeTempRepo();
|
|
20
|
-
const lw = new LogWriter(dir);
|
|
20
|
+
const lw = new LogWriter(dir, 'dump');
|
|
21
21
|
const repos = [{ name: 'r', path: repoDir }];
|
|
22
22
|
return { dir, lw, repos };
|
|
23
23
|
}
|