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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-prd",
3
- "version": "1.0.2",
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
+ }
@@ -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
  }
@@ -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: string) ensures the directory exists
15
- * openStep(n, name, phaseName) returns a StepLog
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 - Absolute path to the run directory.
83
- * Created here if it doesn't exist yet.
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
- mkdirSync(logsDir, { recursive: true });
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
  }
@@ -313,11 +313,13 @@ export async function runVerificationLoop({
313
313
 
314
314
  const { verdict: initialVerdict, failureNotes: initialNotes } = parseVerdict(initialText);
315
315
 
316
- writeFileSync(
317
- join(logWriter.logsDir, `verdict-phase-${phaseNum}-verification.md`),
318
- `VERDICT: ${initialVerdict}\n` + (initialNotes ? `\nFAILURE_NOTES:\n${initialNotes}\n` : ''),
319
- 'utf8'
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
- writeFileSync(
367
- join(logWriter.logsDir, `verdict-phase-${phaseNum}-re-verification-${attempt}.md`),
368
- `VERDICT: ${verdict}\n` + (failureNotes ? `\nFAILURE_NOTES:\n${failureNotes}\n` : ''),
369
- 'utf8'
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 };
@@ -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
- console.log(`Logs: ${logsDir}`);
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
- mkdirSync(logsDir, { recursive: true });
365
- const dryRunLines = [
366
- `Ralph dry-run ${new Date().toISOString()}`,
367
- `Plan: ${planPath}`,
368
- `Branch: ${branch}`,
369
- `Repos: ${repos.filter(r => !r.writableOnly).map(r => r.path).join(', ')}`,
370
- '',
371
- 'Phases:',
372
- ...phases.map(p => {
373
- const done = state.completedPhases.includes(p.index);
374
- const check = done ? 'x' : ' ';
375
- const detail = p.hasVerification
376
- ? ` (${p.acceptanceCriteria.length} criteria)`
377
- : ' (no verification)';
378
- return ` [${check}] ${p.title}${detail}`;
379
- }),
380
- ];
381
- writeFileSync(resolve(logsDir, 'dry-run.log'), dryRunLines.join('\n') + '\n', 'utf8');
382
- console.log(`\n[dry-run] Log written to: ${logsDir}/dry-run.log`);
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
  ]);
@@ -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 step-N-name.log convention', () => {
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('step-3-implementation.log'));
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
  }