Skip to content

feat(cli): cleanup watch capability — stale file housekeeping (#791)#794

Closed
tamirdresher wants to merge 3 commits intodevfrom
squad/791-cleanup-ceremony
Closed

feat(cli): cleanup watch capability — stale file housekeeping (#791)#794
tamirdresher wants to merge 3 commits intodevfrom
squad/791-cleanup-ceremony

Conversation

@tamirdresher
Copy link
Copy Markdown
Collaborator

@tamirdresher tamirdresher commented Apr 3, 2026

What

New cleanup watch capability in the housekeeping phase that removes stale temp files, archives old logs, and warns about stale inbox files.

Why

Squad state directories accumulate files over time — old session logs, orchestration entries, scratch files. No automated cleanup exists. Closes #791

How

  • Clears .squad/.scratch/ (all ephemeral temp files)
  • Deletes orchestration-log and session-log entries older than N days (default: 30)
  • Warns about stale decision inbox files (>7 days)
  • Runs every N rounds (default: 10, configurable via everyNRounds)
  • Registered in capability barrel (index.ts)

Testing

  • npm run build passes
  • npm test passes (12 new tests — metadata, preflight, scratch clearing, log archival, inbox warnings, round skipping, custom config)

Docs

  • Changeset entry (cleanup-ceremony.md)
  • Feature doc page (will be added in follow-up commit)

Exports

N/A (CLI internal capability)

Breaking Changes

None

Waivers

None

…791)

- New cleanup capability in housekeeping phase
- Clears .squad/.scratch/ temp files
- Archives old orchestration-log and session-log entries (>30d)
- Warns about stale decision inbox files (>7d)
- Configurable: everyNRounds, maxAgeDays
- 12 new tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tamirdresher tamirdresher requested review from Copilot and diberry April 3, 2026 18:46
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit ed6c014

⚠️ 4 item(s) to address before review

Status Check Details
Single commit 3 commits — consider squashing before review
Not in draft Ready for review
Branch up to date Up to date with dev
Copilot review No Copilot review yet — it may still be processing
Changeset present Changeset file found
Scope clean No .squad/ or docs/proposals/ files
No merge conflicts No merge conflicts
Copilot threads resolved 2 unresolved Copilot thread(s) — fix and resolve before merging
CI passing 15 check(s) still running

This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new cleanup watch capability in the housekeeping phase to proactively remove stale .squad/ artifacts (scratch + old logs) and surface stale decision inbox items.

Changes:

  • Introduces CleanupCapability and registers it in the default watch capability registry.
  • Implements scratch clearing, age-based log/orchestration cleanup, and stale inbox warnings with configurable cadence/retention.
  • Adds a dedicated Vitest suite covering capability behavior and configuration.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
packages/squad-cli/src/cli/commands/watch/capabilities/cleanup.ts New cleanup capability implementation (scratch/log cleanup + inbox staleness warning).
packages/squad-cli/src/cli/commands/watch/capabilities/index.ts Registers CleanupCapability in the built-in capability registry.
test/watch-cleanup.test.ts Adds unit tests for cleanup metadata, preflight, execution behavior, and config handling.
.changeset/cleanup-ceremony.md Release note for the new watch capability.

Comment on lines +65 to +68
export class CleanupCapability implements WatchCapability {
readonly name = 'cleanup';
readonly description = 'Remove stale scratch files, archive old logs, warn about stale inbox';
readonly configShape = 'object' as const;
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The capability claims it “archives” old log/orchestration entries (and reports them as archived), but the implementation only unlinks the files (deleteSync). Either implement a real archive/move, or rename the user-facing strings/variable names to “deleted/removed” to avoid misleading housekeeping output.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +102
// 1. Clear .squad/.scratch/ — everything in here is ephemeral
const scratchDir = path.join(squadDir, '.scratch');
const scratchFiles = safeList(scratchDir);
let scratchDeleted = 0;
for (const f of scratchFiles) {
if (safeDelete(path.join(scratchDir, f))) scratchDeleted++;
}
if (scratchDeleted > 0) {
actions.push(`scratch: ${scratchDeleted} files cleared`);
}
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scratch cleanup iterates entries and calls deleteSync (unlink). If .squad/.scratch/ contains subdirectories (or nested trees), unlink will fail and the cleanup run may incorrectly report “nothing to do” or undercount deletions. Consider detecting directories and using deleteDirSync (recursive) or a recursive traversal to truly “delete everything” in scratch.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +32
function parseConfig(raw: Record<string, unknown>): CleanupConfig {
return {
everyNRounds: typeof raw.everyNRounds === 'number' ? raw.everyNRounds : DEFAULT_EVERY_N_ROUNDS,
maxAgeDays: typeof raw.maxAgeDays === 'number' ? raw.maxAgeDays : DEFAULT_MAX_AGE_DAYS,
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseConfig accepts any number values for everyNRounds / maxAgeDays (including 0, negatives, NaN, Infinity). everyNRounds <= 0 (or NaN) causes the modulo check to behave unexpectedly (often skipping forever except round 1), and negative ages invert retention. Clamp/validate to finite integers (e.g., everyNRounds >= 1, maxAgeDays >= 0) and fall back to defaults on invalid input.

Suggested change
function parseConfig(raw: Record<string, unknown>): CleanupConfig {
return {
everyNRounds: typeof raw.everyNRounds === 'number' ? raw.everyNRounds : DEFAULT_EVERY_N_ROUNDS,
maxAgeDays: typeof raw.maxAgeDays === 'number' ? raw.maxAgeDays : DEFAULT_MAX_AGE_DAYS,
function parseIntegerConfig(value: unknown, minimum: number, fallback: number): number {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return fallback;
}
const normalized = Math.floor(value);
return normalized >= minimum ? normalized : fallback;
}
function parseConfig(raw: Record<string, unknown>): CleanupConfig {
return {
everyNRounds: parseIntegerConfig(raw.everyNRounds, 1, DEFAULT_EVERY_N_ROUNDS),
maxAgeDays: parseIntegerConfig(raw.maxAgeDays, 0, DEFAULT_MAX_AGE_DAYS),

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +44
const fileDate = new Date(match[1]!);
if (isNaN(fileDate.getTime())) return false;
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
return fileDate < cutoff;
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Date parsing mixes UTC (new Date('YYYY-MM-DD') is parsed as UTC midnight) with a local-time cutoff (new Date(); cutoff.setDate(...)). This can produce off-by-one behavior around day boundaries/timezones. Consider computing the cutoff in UTC (setUTCDate / Date.UTC) or comparing by date-only strings consistently.

Suggested change
const fileDate = new Date(match[1]!);
if (isNaN(fileDate.getTime())) return false;
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
return fileDate < cutoff;
const fileDateString = match[1]!;
const fileDate = new Date(`${fileDateString}T00:00:00.000Z`);
if (isNaN(fileDate.getTime())) return false;
if (fileDate.toISOString().slice(0, 10) !== fileDateString) return false;
const cutoff = new Date();
cutoff.setUTCDate(cutoff.getUTCDate() - days);
const cutoffDateString = cutoff.toISOString().slice(0, 10);
return fileDateString < cutoffDateString;

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +12
- Warns about stale decision inbox files (>7 days)
- Configurable: `everyNRounds` (default: 10), `maxAgeDays` (default: 30)
- 11 new tests
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changeset claims “11 new tests”, but test/watch-cleanup.test.ts adds 12 it(...) cases. Update the count (or adjust the tests) so the release note matches the actual PR contents.

Copilot uses AI. Check for mistakes.
tamirdresher and others added 2 commits April 4, 2026 18:29
- Rename 'archive' to 'prune' in all log messages and comments
- Use rmSync with recursive to handle subdirectories in .scratch/
- Add Number.isFinite and >0 guards to parseConfig validation
- Use Date.now() consistently for UTC cutoff calculation
- Fix changeset: correct test count from 11 to 12

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tamirdresher
Copy link
Copy Markdown
Collaborator Author

Superseded by #830 — combined into a single watch next-gen PR for easier review.

@tamirdresher
Copy link
Copy Markdown
Collaborator Author

Closing — superseded by #830 (watch-next-gen combined PR)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Cleaning ceremony to remove stale temp/scratch files

2 participants