ktg-plugin-marketplace/plugins/ultraplan-local/lib/util/cleanup.mjs
Kjell Tore Guttormsen 14ecda886c feat(voyage)!: bulk content rewrite ultra -> voyage/trek prose [skip-docs]
Sed-pipeline (16 patterns, longest-match-first) sweeper residuelle ultra*-treff
i prose, command-narrativ, agent-prompts, hook-kommentarer, doc-prosa.

Pipeline-utvidelser fra V4-prompten:
- BSD-syntax [[:<:]]ultra[[:>:]] istedenfor \bultra\b (BSD sed mangler \b)
- 6 compound-patterns for ultraplan/ultraexecute/ultraresearch/ultrabrief/
  ultrareview/ultracontinue uten -local-suffiks
- ultra*-stats glob -> trek*-stats glob
- Linje-eksklusjon redusert til ultra-cc-architect (Q8); session-state-
  eksklusjonen var over-protektiv
- File-eksklusjon utvidet til settings.json, package.json, plugin.json,
  hele .claude/-treet (gitignored + V5-territorium)

Q8-undantak holdt: architecture-discovery.mjs + project-discovery.mjs urort.
Filnavn-konvensjon holdt: .session-state.local.json + *.local.* preservert.

Manuell narrative-fix: tests/lib/agent-frontmatter.test.mjs linje 10
mangled "/ultra*-local" til "/voyage*-local" (ingen slik kommando finnes);
korrigert til "/trek*".

Residualer utenfor scope (V5 handterer): package.json + .claude-plugin/
plugin.json (Step 12-14 versjons-bump). .claude/* er gitignored
spec-historikk med tilsiktet BEFORE/AFTER-narrativ.

Part of voyage-rebrand session 3 (Wave 4 / Step 10).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 15:08:20 +02:00

94 lines
3.4 KiB
JavaScript

// lib/util/cleanup.mjs
// Bug 4 — operator-invoked cleanup of completed-project state files.
//
// The trekplan pipeline does NOT auto-cleanup state on session-end:
// stale .session-state.local.json + NEXT-SESSION-PROMPT.local.md across many
// projects accumulate over time. This util removes them safely once the
// project is fully done (status === 'completed' as seen by validateSessionState).
//
// Invariants:
// - Strict equality on parsed.status === 'completed' (no soft-match).
// - Idempotent: re-running on a partially-cleaned dir succeeds with deleted: [].
// - Refuses dryRun: false without an explicit confirm: true (prevents accidents).
// - ENOENT counts as "already absent" — never an error.
// - Cleanup is operator-invoked from /trekcontinue --cleanup; no Bash binding here.
import { existsSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import { issue, fail, ok } from './result.mjs';
import { validateSessionState } from '../validators/session-state-validator.mjs';
const CANDIDATE_FILES = Object.freeze([
'.session-state.local.json',
'NEXT-SESSION-PROMPT.local.md',
]);
/**
* Clean up state files for a completed trekplan project.
*
* @param {string} projectDir - absolute or cwd-relative path to the project directory
* @param {{dryRun?: boolean, confirm?: boolean}} [opts]
* @returns {{valid: boolean, errors: object[], warnings: object[], parsed?: {wouldDelete?: string[], deleted?: string[]}}}
*/
export function cleanupProject(projectDir, opts = {}) {
const dryRun = opts.dryRun !== false; // default true
const confirm = opts.confirm === true;
if (!dryRun && !confirm) {
return fail(issue(
'CLEANUP_REQUIRES_CONFIRM',
'Refused: dryRun=false requires confirm=true (explicit operator confirmation)',
'Re-run with {dryRun: false, confirm: true} to actually delete files.',
));
}
if (typeof projectDir !== 'string' || projectDir.length === 0) {
return fail(issue('CLEANUP_INVALID_PROJECT_DIR', 'projectDir must be a non-empty string'));
}
const stateFile = join(projectDir, '.session-state.local.json');
if (!existsSync(stateFile)) {
return fail(issue(
'CLEANUP_NO_STATE_FILE',
`No state file at ${stateFile}; nothing to clean up`,
'cleanup is only valid for projects that have a .session-state.local.json with status: completed',
));
}
const validation = validateSessionState(stateFile);
if (!validation.valid) {
return fail(issue(
'CLEANUP_INVALID_STATE_FILE',
`State file at ${stateFile} is invalid: ${validation.errors.map(e => e.code).join(', ')}`,
));
}
if (validation.parsed.status !== 'completed') {
return fail(issue(
'CLEANUP_NOT_COMPLETED',
`Refused: status is "${validation.parsed.status}", not "completed"`,
'cleanup is reserved for fully-finished projects. Resume via /trekcontinue or wait until the run completes.',
));
}
const candidates = CANDIDATE_FILES.map(f => join(projectDir, f));
if (dryRun) {
const wouldDelete = candidates.filter(p => existsSync(p));
return { valid: true, errors: [], warnings: [], parsed: { wouldDelete, deleted: [] } };
}
const deleted = [];
for (const p of candidates) {
try {
unlinkSync(p);
deleted.push(p);
} catch (e) {
if (e && e.code === 'ENOENT') continue; // idempotent: already absent
return fail(issue('CLEANUP_UNLINK_FAILED', `Failed to delete ${p}: ${e.message}`));
}
}
return ok({ wouldDelete: [], deleted });
}