Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions packages/squad-cli/src/cli/core/nap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface NapMetrics {
}

export interface NapAction {
type: 'compress' | 'prune' | 'archive' | 'merge' | 'cleanup';
type: 'compress' | 'prune' | 'archive' | 'merge' | 'cleanup' | 'warning';
target: string;
description: string;
bytesSaved: number;
Expand Down Expand Up @@ -97,6 +97,19 @@ function humanTokens(bytes: number): string {
return `${(tokens / 1000).toFixed(0)}K`;
}

/** Build a warning action when decisions.md is oversized but nothing was archivable. */
function oversizedDecisionsWarning(squadDir: string): NapAction | null {
const decisionsPath = path.join(squadDir, 'decisions.md');
const decisionsSize = fileSize(decisionsPath);
if (decisionsSize <= DECISION_THRESHOLD) return null;
return {
type: 'warning',
target: decisionsPath,
description: `⚠️ decisions.md is ${humanBytes(decisionsSize)} but no entries could be archived (all recent or undated). Consider manual cleanup or adding dates to undated entries.`,
bytesSaved: 0,
};
}

function daysAgoFromLine(line: string): number | null {
// Match dates like 2026-02-28 or ### 2026-02-28
const m = line.match(/(\d{4}-\d{2}-\d{2})/);
Expand Down Expand Up @@ -472,7 +485,12 @@ export async function runNap(options: NapOptions): Promise<NapResult> {

// Decision archival
const archiveAction = archiveDecisions(squadDir, dryRun);
if (archiveAction) actions.push(archiveAction);
if (archiveAction) {
actions.push(archiveAction);
} else {
const warning = oversizedDecisionsWarning(squadDir);
if (warning) actions.push(warning);
}

} finally {
if (!dryRun) removeJournal(squadDir);
Expand Down Expand Up @@ -524,7 +542,12 @@ export function runNapSync(options: NapOptions): NapResult {
actions.push(...cleanInbox(squadDir, dryRun));

const archiveAction = archiveDecisions(squadDir, dryRun);
if (archiveAction) actions.push(archiveAction);
if (archiveAction) {
actions.push(archiveAction);
} else {
const warning = oversizedDecisionsWarning(squadDir);
if (warning) actions.push(warning);
}
} finally {
if (!dryRun) removeJournal(squadDir);
}
Expand Down
46 changes: 46 additions & 0 deletions test/nap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,52 @@ describe('Nap — Decision archival', () => {
expect(archiveActions).toHaveLength(0);
expect(existsSync(join(squadDir, 'decisions-archive.md'))).toBe(false);
});
it('warns when decisions.md is over threshold but nothing is archivable', async () => {
// decisions.md >20KB with ONLY header content — no ### entries at all.
// archiveDecisions() finds no entries and returns null, but the file is oversized.
const headerOnly = '# Decisions\n\n' + 'x'.repeat(21 * 1024);
expect(Buffer.byteLength(headerOnly)).toBeGreaterThan(20 * 1024);

const squadDir = createTestSquadDir({ 'decisions.md': headerOnly });
const result = await runNap({ squadDir, dryRun: true });

const warnActions = result.actions.filter(
(a) => a.type === 'warning' && a.target.includes('decisions'),
);
expect(warnActions).toHaveLength(1);
expect(warnActions[0]!.bytesSaved).toBe(0);
expect(warnActions[0]!.description).toContain('no entries could be archived');
});

it('no warning when decisions.md is under threshold', async () => {
const smallContent = '# Decisions\n\n' + 'x'.repeat(5 * 1024);
expect(Buffer.byteLength(smallContent)).toBeLessThan(20 * 1024);

const squadDir = createTestSquadDir({ 'decisions.md': smallContent });
const result = await runNap({ squadDir });

const warnActions = result.actions.filter((a) => a.type === 'warning');
expect(warnActions).toHaveLength(0);
});

it('no warning when archival succeeds', async () => {
// decisions.md >20KB with old entries that DO get archived
let bigOldDecisions = '# Decisions\n\n';
for (let i = 0; i < 30; i++) {
bigOldDecisions += `### 2024-01-${String(i + 1).padStart(2, '0')}: Decision ${i + 1}\n`;
bigOldDecisions += 'w'.repeat(800) + '\n\n';
}
expect(Buffer.byteLength(bigOldDecisions)).toBeGreaterThan(20 * 1024);

const squadDir = createTestSquadDir({ 'decisions.md': bigOldDecisions });
const result = await runNap({ squadDir });

const archiveActions = result.actions.filter((a) => a.type === 'archive');
expect(archiveActions.length).toBeGreaterThan(0);

const warnActions = result.actions.filter((a) => a.type === 'warning');
expect(warnActions).toHaveLength(0);
});
});

// ============================================================================
Expand Down
Loading