Skip to content

Commit 011fb2f

Browse files
VibeWriter Userclaude
andcommitted
feat: move NightyTidy reports to audit-reports/ with 00_ prefix
Reports are now written to audit-reports/00_NIGHTYTIDY-REPORT_NN_*.md instead of the project root. The 00_ prefix ensures reports sort to the top of the audit-reports folder alongside other audit artifacts. - buildReportNames() now returns { reportFile, reportDir } - generateReport() creates audit-reports/ directory if needed - Updated all callers (cli.js, orchestrator.js) to use full paths - Updated 10 test files to expect new location/format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8ca433c commit 011fb2f

16 files changed

Lines changed: 93 additions & 56 deletions

.claude/memory/report-generation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ Assumes CLAUDE.md loaded. Report logic in `src/report.js`, action plan logic in
66

77
| Function | Purpose |
88
|----------|---------|
9-
| `generateReport(results, narration, metadata, { actionPlanText, reportFile })` | Writes NIGHTYTIDY-REPORT.md (with inline action plan) + updates CLAUDE.md |
9+
| `generateReport(results, narration, metadata, { actionPlanText, reportFile, reportDir })` | Writes report to `audit-reports/00_NIGHTYTIDY-REPORT_*.md` (with inline action plan) + updates CLAUDE.md |
1010
| `cleanNarration(text)` | Strips conversational preamble from AI-generated narration (applied internally by `generateReport`) |
1111
| `formatDuration(ms)` | Format milliseconds to human-readable string |
1212
| `getVersion()` | Returns version from package.json (lazy-cached, defaults to '0.1.0' on error) |
13-
| `buildReportNames(projectDir, startTime)` | Returns `{ reportFile }` with auto-incremented number + timestamp |
13+
| `buildReportNames(projectDir, startTime)` | Returns `{ reportFile, reportDir }` — reportFile has `00_` prefix, reportDir is `audit-reports/` subdirectory |
1414

1515
## Report Structure (single file — no separate ACTIONS file)
1616

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ src/
4747
dashboard-tui.js # Standalone TUI progress display (spawned in separate terminal window)
4848
lock.js # Atomic lock file to prevent concurrent runs (async with TTY prompt)
4949
logger.js # File + stdout logger with chalk coloring
50-
report.js # NIGHTYTIDY-REPORT.md generation + CLAUDE.md update
50+
report.js # Report generation (audit-reports/00_NIGHTYTIDY-REPORT_*.md) + CLAUDE.md update
5151
consolidation.js # Post-run action plan — consolidates step recommendations for inline report embedding
5252
setup.js # --setup command: generates CLAUDE.md integration snippet for target projects
5353
sync.js # Google Doc prompt sync — fetches, parses, diffs, updates local prompt files
@@ -230,7 +230,7 @@ NightyTidy creates these files/artifacts in the project it runs against:
230230
| `nightytidy-run.log` | Full run log (timestamped) | No |
231231
| `nightytidy-progress.json` | Live progress state (read by TUI window) | No (deleted on stop) |
232232
| `nightytidy-dashboard.url` | Dashboard URL — Claude reads this and shares with user | No (deleted on stop) |
233-
| `NIGHTYTIDY-REPORT_NN_YYYY-MM-DD-HHMM.md` | Run summary with step results + inline action plan (numbered + timestamped) | Yes (on run branch) |
233+
| `audit-reports/00_NIGHTYTIDY-REPORT_NN_YYYY-MM-DD-HHMM.md` | Run summary with step results + inline action plan (numbered + timestamped) | Yes (on run branch) |
234234
| `CLAUDE.md` (appended section) | "NightyTidy — Last Run" with undo tag | Yes (on run branch) |
235235
| `nightytidy.lock` | Prevents concurrent runs (PID + timestamp) | No (auto-removed on exit; persistent in orchestrator mode) |
236236
| `nightytidy-gui.log` | GUI session log (startup, API requests, errors, shutdown) | No |
@@ -332,7 +332,7 @@ bin/nightytidy.js
332332
5. **Execution**: Run each step (improvement + doc update in same session via `--continue`), with fallback commits
333333
6. **Rate-limit handling**: If a step hits a rate limit, the run pauses with exponential backoff (2min → 2hr cap). API is probed periodically; on success the failed step is retried. SIGINT during pause stops the run and gets partial results. GUI mode shows a pause overlay with countdown, "Resume Now", and "Finish with Partial Results" buttons.
334334
7. **Abort handling**: SIGINT generates partial report; second SIGINT force-exits
335-
8. **Reporting**: Changelog → action plan → NIGHTYTIDY-REPORT.md (with inline action plan) → commit → merge back to original branch
335+
8. **Reporting**: Changelog → action plan → audit-reports/00_NIGHTYTIDY-REPORT_*.md (with inline action plan) → commit → merge back to original branch
336336
9. **Notifications**: Desktop notifications at start, on step failure, and on completion
337337

338338
### Orchestrator Mode (Claude Code)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ Changes are always on the run branch — your original branch is safe.
229229

230230
After all steps complete, NightyTidy generates:
231231

232-
- **NIGHTYTIDY-REPORT.md** — AI-narrated run summary with per-step results, costs, token usage, duration, and a prioritized action plan
232+
- **`audit-reports/00_NIGHTYTIDY-REPORT_*.md`** — AI-narrated run summary with per-step results, costs, token usage, duration, and a prioritized action plan (the `00_` prefix ensures reports sort to the top of the audit-reports folder)
233233
- **CLAUDE.md update** — Appends a "Last Run" section with the run date and undo instructions
234234
- **Audit trail** — All 33 step prompts are copied to `audit-reports/refactor-prompts/` so you can see exactly what was asked
235235

@@ -239,7 +239,7 @@ If the AI report fails verification (junk detection), NightyTidy falls back to a
239239

240240
| File | Committed? | Purpose |
241241
|------|-----------|---------|
242-
| `NIGHTYTIDY-REPORT_NN_YYYY-MM-DD-HHMM.md` | Yes | Run summary with step results + action plan |
242+
| `audit-reports/00_NIGHTYTIDY-REPORT_NN_YYYY-MM-DD-HHMM.md` | Yes | Run summary with step results + action plan |
243243
| `CLAUDE.md` (appended section) | Yes | "NightyTidy — Last Run" with undo tag |
244244
| `audit-reports/refactor-prompts/*.md` | Yes | All 33 prompts for audit trail |
245245
| `nightytidy-before-*` git tag | Yes (tag) | Safety snapshot for rollback |

src/cli.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ function saveRunState(projectDir, ctx, snapshot) {
195195

196196
async function handleAbortedRun(executionResults, { projectDir, runBranch, tagName, originalBranch }) {
197197
info('Run interrupted by user');
198-
const { reportFile } = buildReportNames(projectDir, Date.now() - executionResults.totalDuration);
198+
const { reportFile, reportDir } = buildReportNames(projectDir, Date.now() - executionResults.totalDuration);
199199
const totalInputTokens = executionResults.results.reduce((sum, r) => sum + (r.cost?.inputTokens || 0), 0) || null;
200200
const totalOutputTokens = executionResults.results.reduce((sum, r) => sum + (r.cost?.outputTokens || 0), 0) || null;
201201
generateReport(executionResults, null, {
@@ -207,11 +207,12 @@ async function handleAbortedRun(executionResults, { projectDir, runBranch, tagNa
207207
endTime: Date.now(),
208208
totalInputTokens,
209209
totalOutputTokens,
210-
}, { reportFile });
210+
}, { reportFile, reportDir });
211211

212212
const gitInstance = getGitInstance();
213+
const reportPath = path.join(reportDir, reportFile);
213214
try {
214-
await gitInstance.add([reportFile]);
215+
await gitInstance.add([reportPath]);
215216
await gitInstance.commit('NightyTidy: Add partial run report');
216217
} catch (err) { debug(`Could not commit partial report: ${err.message}`); }
217218

@@ -718,7 +719,8 @@ async function finalizeRun(executionResults, projectDir, ctx) {
718719

719720
// Build unique report filename (numbered + timestamped)
720721
const startTime = Date.now() - executionResults.totalDuration;
721-
const { reportFile } = buildReportNames(projectDir, startTime);
722+
const { reportFile, reportDir } = buildReportNames(projectDir, startTime);
723+
const reportPath = path.join(reportDir, reportFile);
722724

723725
const totalInputTokens = executionResults.results.reduce((sum, r) => sum + (r.cost?.inputTokens || 0), 0) || null;
724726
const totalOutputTokens = executionResults.results.reduce((sum, r) => sum + (r.cost?.outputTokens || 0), 0) || null;
@@ -738,7 +740,7 @@ async function finalizeRun(executionResults, projectDir, ctx) {
738740
info('Generating report...');
739741
ctx.spinner = ora({ text: 'Generating report...', color: 'cyan' }).start();
740742

741-
const reportPrompt = buildReportPrompt(executionResults, metadata, { reportFile });
743+
const reportPrompt = buildReportPrompt(executionResults, metadata, { reportFile: reportPath });
742744
const reportResult = await runPrompt(SAFETY_PREAMBLE + reportPrompt, projectDir, {
743745
label: 'Report generation',
744746
timeout: ctx.timeoutMs,
@@ -747,7 +749,6 @@ async function finalizeRun(executionResults, projectDir, ctx) {
747749
ctx.spinner.stop();
748750

749751
// Verify the report file was created correctly
750-
const reportPath = path.join(projectDir, reportFile);
751752
let reportOk = false;
752753
try {
753754
if (existsSync(reportPath)) {
@@ -758,7 +759,7 @@ async function finalizeRun(executionResults, projectDir, ctx) {
758759

759760
if (!reportOk) {
760761
warn('AI report generation failed — using template fallback');
761-
generateReport(executionResults, null, metadata, { reportFile, skipClaudeMdUpdate: true });
762+
generateReport(executionResults, null, metadata, { reportFile, reportDir, skipClaudeMdUpdate: true });
762763
console.log(chalk.dim('Report generated with fallback template.'));
763764
} else {
764765
console.log(chalk.green('Report generated successfully.'));
@@ -770,7 +771,7 @@ async function finalizeRun(executionResults, projectDir, ctx) {
770771
// Commit report + CLAUDE.md (if not already committed by Claude)
771772
const gitInstance = getGitInstance();
772773
try {
773-
const filesToCommit = [reportFile, 'CLAUDE.md'];
774+
const filesToCommit = [reportPath, 'CLAUDE.md'];
774775
await gitInstance.add(filesToCommit);
775776
await gitInstance.commit('NightyTidy: Add run report and update CLAUDE.md');
776777
} catch (err) {
@@ -781,7 +782,7 @@ async function finalizeRun(executionResults, projectDir, ctx) {
781782
const mergeResult = await mergeRunBranch(ctx.originalBranch, ctx.runBranch);
782783

783784
// Completion notification + terminal summary
784-
printCompletionSummary(executionResults, mergeResult, { runBranch: ctx.runBranch, tagName: ctx.tagName, reportFile });
785+
printCompletionSummary(executionResults, mergeResult, { runBranch: ctx.runBranch, tagName: ctx.tagName, reportFile: reportPath });
785786

786787
// Update dashboard to completed and schedule shutdown
787788
if (ctx.dashState) {

src/orchestrator.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,8 @@ export async function finishRun(projectDir) {
771771
const stepsOutputTokens = executionResults.results.reduce((sum, r) => sum + (r.cost?.outputTokens || 0), 0) || null;
772772

773773
// Build unique report filename (numbered + timestamped)
774-
const { reportFile } = buildReportNames(projectDir, state.startTime);
774+
const { reportFile, reportDir } = buildReportNames(projectDir, state.startTime);
775+
const reportPath = path.join(reportDir, reportFile);
775776

776777
// ── Single AI call for report generation ──
777778
const finishStart = Date.now();
@@ -800,7 +801,7 @@ export async function finishRun(projectDir) {
800801

801802
// Generate report in a single fresh Claude session (like other steps)
802803
info('Generating report (narration + action plan)...');
803-
const reportPrompt = buildReportPrompt(executionResults, metadata, { reportFile });
804+
const reportPrompt = buildReportPrompt(executionResults, metadata, { reportFile: reportPath });
804805
const reportResult = await runPrompt(SAFETY_PREAMBLE + reportPrompt, projectDir, {
805806
label: 'Report generation',
806807
timeout: state.timeout || undefined,
@@ -811,7 +812,6 @@ export async function finishRun(projectDir) {
811812
const finishDuration = Date.now() - finishStart;
812813

813814
// Verify the report file was created correctly
814-
const reportPath = path.join(projectDir, reportFile);
815815
let reportOk = false;
816816
try {
817817
if (existsSync(reportPath)) {
@@ -823,7 +823,7 @@ export async function finishRun(projectDir) {
823823
// Fallback: generate report via JS template if AI failed
824824
if (!reportOk) {
825825
warn('AI report generation failed or produced invalid output — using template fallback');
826-
generateReport(executionResults, null, metadata, { reportFile, skipClaudeMdUpdate: true });
826+
generateReport(executionResults, null, metadata, { reportFile, reportDir, skipClaudeMdUpdate: true });
827827
}
828828

829829
// Update progress: mark finish step as completed
@@ -844,7 +844,7 @@ export async function finishRun(projectDir) {
844844
// Commit report + CLAUDE.md (if not already committed by Claude)
845845
const gitInstance = getGitInstance();
846846
try {
847-
const filesToCommit = [reportFile, 'CLAUDE.md'];
847+
const filesToCommit = [reportPath, 'CLAUDE.md'];
848848
await gitInstance.add(filesToCommit);
849849
await gitInstance.commit('NightyTidy: Add run report and update CLAUDE.md');
850850
} catch (err) {
@@ -853,7 +853,7 @@ export async function finishRun(projectDir) {
853853

854854
// Read report content for embedding in response (avoids fragile file-read API in GUI)
855855
let reportContent = null;
856-
try { reportContent = readFileSync(path.join(projectDir, reportFile), 'utf-8'); } catch { /* merge will bring file back */ }
856+
try { reportContent = readFileSync(reportPath, 'utf-8'); } catch { /* merge will bring file back */ }
857857

858858
// Merge
859859
const mergeResult = await mergeRunBranch(state.originalBranch, state.runBranch);
@@ -890,7 +890,7 @@ export async function finishRun(projectDir) {
890890
finishDuration,
891891
merged: mergeResult.success,
892892
mergeConflict: mergeResult.conflict || false,
893-
reportPath: reportFile,
893+
reportPath,
894894
reportContent,
895895
tagName: state.tagName,
896896
runBranch: state.runBranch,

src/report.js

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,45 @@
88
* Error contract: Warns but NEVER throws. Report failure must not crash a run.
99
*/
1010

11-
import { writeFileSync, readFileSync, existsSync, readdirSync } from 'fs';
11+
import { writeFileSync, readFileSync, existsSync, readdirSync, mkdirSync } from 'fs';
1212
import path from 'path';
1313
import { info, warn } from './logger.js';
1414
import { REPORT_PROMPT } from './prompts/loader.js';
1515

16-
const REPORT_PREFIX = 'NIGHTYTIDY-REPORT';
16+
const REPORT_PREFIX = '00_NIGHTYTIDY-REPORT';
17+
const REPORT_SUBDIR = 'audit-reports';
1718

1819
/**
1920
* Build a unique, numbered + timestamped filename for the report.
20-
* Scans the project directory for existing reports to auto-increment the number.
21+
* Scans the audit-reports/ directory for existing reports to auto-increment the number.
2122
* @param {string} projectDir - Target project directory
2223
* @param {number} [startTime] - Run start time (ms epoch); defaults to now
23-
* @returns {{ reportFile: string }}
24+
* @returns {{ reportFile: string, reportDir: string }}
2425
*/
2526
export function buildReportNames(projectDir, startTime = Date.now()) {
2627
const d = new Date(startTime);
2728
const pad2 = n => String(n).padStart(2, '0');
2829
const timestamp = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}-${pad2(d.getHours())}${pad2(d.getMinutes())}`;
2930

30-
// Find next available number by scanning existing report files
31+
const reportDir = path.join(projectDir, REPORT_SUBDIR);
32+
33+
// Find next available number by scanning existing report files in audit-reports/
3134
let maxNum = 0;
3235
try {
33-
const files = readdirSync(projectDir);
34-
const pattern = /^NIGHTYTIDY-REPORT_(\d+)_/;
36+
const files = readdirSync(reportDir);
37+
const pattern = /^00_NIGHTYTIDY-REPORT_(\d+)_/;
3538
for (const f of files) {
3639
const m = f.match(pattern);
3740
if (m) maxNum = Math.max(maxNum, parseInt(m[1], 10));
3841
}
39-
} catch { /* directory unreadable — start at 1 */ }
42+
} catch { /* directory unreadable or doesn't exist — start at 1 */ }
4043

4144
const num = pad2(maxNum + 1);
4245
const suffix = `_${num}_${timestamp}`;
4346

4447
return {
4548
reportFile: `${REPORT_PREFIX}${suffix}.md`,
49+
reportDir,
4650
};
4751
}
4852

@@ -68,6 +72,7 @@ export function buildReportNames(projectDir, startTime = Date.now()) {
6872
* @typedef {Object} ReportOptions
6973
* @property {string|null} [actionPlanText] - Inline action plan markdown (headings already downgraded)
7074
* @property {string} [reportFile] - Custom report filename
75+
* @property {string} [reportDir] - Custom report directory (defaults to audit-reports/)
7176
*/
7277

7378
/** @type {string|undefined} */
@@ -449,7 +454,7 @@ export function verifyReportContent(content, metadata) {
449454
* @param {ReportOptions} [options] - Report options
450455
* @returns {string} The report filename (basename only, e.g. 'NIGHTYTIDY-REPORT_01_2026-03-10-1448.md')
451456
*/
452-
export function generateReport(results, narration, metadata, { actionPlanText, reportFile, skipClaudeMdUpdate } = {}) {
457+
export function generateReport(results, narration, metadata, { actionPlanText, reportFile, reportDir, skipClaudeMdUpdate } = {}) {
453458
const date = formatDate(metadata.startTime);
454459

455460
let report = `# NightyTidy Report \u2014 ${date}\n\n`;
@@ -474,8 +479,15 @@ export function generateReport(results, narration, metadata, { actionPlanText, r
474479

475480
report += buildUndoSection(metadata);
476481

477-
const filename = reportFile || 'NIGHTYTIDY-REPORT.md';
478-
const reportPath = path.join(metadata.projectDir, filename);
482+
const filename = reportFile || '00_NIGHTYTIDY-REPORT.md';
483+
const targetDir = reportDir || path.join(metadata.projectDir, REPORT_SUBDIR);
484+
485+
// Ensure the report directory exists
486+
try {
487+
mkdirSync(targetDir, { recursive: true });
488+
} catch { /* directory already exists or can't be created — continue anyway */ }
489+
490+
const reportPath = path.join(targetDir, filename);
479491
writeFileSync(reportPath, report, 'utf8');
480492
info(`Report written to ${reportPath}`);
481493

test/cli-extended.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ vi.mock('../src/report.js', () => ({
112112
generateReport: vi.fn(),
113113
formatDuration: vi.fn((ms) => `${Math.round(ms / 1000)}s`),
114114
getVersion: vi.fn(() => '0.1.0'),
115-
buildReportNames: vi.fn(() => ({ reportFile: 'NIGHTYTIDY-REPORT_01_2026-01-01-0000.md' })),
115+
buildReportNames: vi.fn(() => ({ reportFile: '00_NIGHTYTIDY-REPORT_01_2026-01-01-0000.md', reportDir: '/fake/project/audit-reports' })),
116116
buildReportPrompt: vi.fn(() => 'mock report prompt'),
117117
verifyReportContent: vi.fn(() => true),
118118
updateClaudeMd: vi.fn(),

test/cli-resume.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ vi.mock('../src/report.js', () => ({
117117
generateReport: vi.fn(),
118118
formatDuration: vi.fn((ms) => `${Math.round(ms / 1000)}s`),
119119
getVersion: vi.fn(() => '0.1.0'),
120-
buildReportNames: vi.fn(() => ({ reportFile: 'NIGHTYTIDY-REPORT_01_2026-01-01-0000.md' })),
120+
buildReportNames: vi.fn(() => ({ reportFile: '00_NIGHTYTIDY-REPORT_01_2026-01-01-0000.md', reportDir: '/fake/project/audit-reports' })),
121121
buildReportPrompt: vi.fn(() => 'mock report prompt'),
122122
verifyReportContent: vi.fn(() => true),
123123
updateClaudeMd: vi.fn(),

test/cli.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ vi.mock('../src/report.js', () => ({
112112
generateReport: vi.fn(),
113113
formatDuration: vi.fn((ms) => `${Math.round(ms / 1000)}s`),
114114
getVersion: vi.fn(() => '0.1.0'),
115-
buildReportNames: vi.fn(() => ({ reportFile: 'NIGHTYTIDY-REPORT_01_2026-01-01-0000.md' })),
115+
buildReportNames: vi.fn(() => ({ reportFile: '00_NIGHTYTIDY-REPORT_01_2026-01-01-0000.md', reportDir: '/fake/project/audit-reports' })),
116116
buildReportPrompt: vi.fn(() => 'mock report prompt'),
117117
verifyReportContent: vi.fn(() => true),
118118
updateClaudeMd: vi.fn(),

test/contracts.test.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -633,8 +633,9 @@ describe('contract: report.js — getVersion and side effects', () => {
633633
expect(version.length).toBeGreaterThan(0);
634634
});
635635

636-
it('generateReport writes NIGHTYTIDY-REPORT.md to disk', async () => {
636+
it('generateReport writes report to audit-reports/ directory', async () => {
637637
const { generateReport } = await import('../src/report.js');
638+
const { readdirSync } = await import('fs');
638639

639640
const results = {
640641
results: [{ step: { number: 1, name: 'Lint' }, status: 'completed', output: 'ok', duration: 1000, attempts: 1, error: null }],
@@ -652,7 +653,11 @@ describe('contract: report.js — getVersion and side effects', () => {
652653

653654
generateReport(results, null, metadata);
654655

655-
expect(existsSync(path.join(tempDir, 'NIGHTYTIDY-REPORT.md'))).toBe(true);
656+
const auditDir = path.join(tempDir, 'audit-reports');
657+
expect(existsSync(auditDir)).toBe(true);
658+
const files = readdirSync(auditDir);
659+
const reportFile = files.find(f => f.startsWith('00_NIGHTYTIDY-REPORT'));
660+
expect(reportFile).toBeDefined();
656661
});
657662

658663
it('generateReport also writes/updates CLAUDE.md', async () => {
@@ -937,7 +942,7 @@ describe('contract: orchestrator.js — never throws, returns result objects', (
937942
vi.doMock('../src/report.js', () => ({
938943
generateReport: vi.fn(),
939944
formatDuration: vi.fn(() => '0m'),
940-
buildReportNames: vi.fn(() => ({ reportFile: 'NIGHTYTIDY-REPORT_01_2026-01-01-0000.md' })),
945+
buildReportNames: vi.fn(() => ({ reportFile: '00_NIGHTYTIDY-REPORT_01_2026-01-01-0000.md', reportDir: '/fake/project/audit-reports' })),
941946
buildReportPrompt: vi.fn(() => 'mock report prompt'),
942947
verifyReportContent: vi.fn(() => true),
943948
updateClaudeMd: vi.fn(),

0 commit comments

Comments
 (0)