feat(ultraplan-local): Bug 4 — wire --cleanup into /ultracontinue-local [skip-docs]
Step 10 of v3.4.1 plan. commands/ultracontinue-local.md: - New Phase 0.5 between Phase 0 and Phase 1 — terminal cleanup mode triggered by parsed flags['--cleanup'] === true. Requires explicit positional[0] (no "clean all"), no template placeholders in the Bash invocation. Passes through to cleanupProject via inline ESM. Cleanup never falls through to Phase 1/2/3/4. - Phase 0 usage block updated to document --cleanup and --cleanup --confirm forms alongside the legacy <project-dir> form. tests/commands/ultracontinue.test.mjs: - Test (Bug 4 prose) — Phase 0.5 header present, references cleanupProject and flags['--cleanup'], appears between Phase 0 and Phase 1 in document order, usage mentions --cleanup --confirm. - Test (f-1) dry-run on completed project lists candidates without deleting; both files still on disk. - Test (f-2 + f-3) confirm-mode deletes both files; subsequent invocation on the already-cleaned dir signals CLEANUP_NO_STATE_FILE (deterministic terminal state, idempotent for operators). Tests 355 -> 358 (+3). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
7ffaa82207
commit
9fa83bdf2f
2 changed files with 133 additions and 3 deletions
|
|
@ -46,9 +46,11 @@ Phase 1. Do NOT print the usage block on empty args.
|
|||
/ultracontinue — Resume the next session in a multi-session ultraplan project.
|
||||
|
||||
Usage:
|
||||
/ultracontinue # auto-discover state file under cwd
|
||||
/ultracontinue <project-dir> # explicit project directory
|
||||
/ultracontinue --help # this message
|
||||
/ultracontinue # auto-discover state file under cwd
|
||||
/ultracontinue <project-dir> # explicit project directory
|
||||
/ultracontinue --cleanup <project-dir> # dry-run: list stale files
|
||||
/ultracontinue --cleanup --confirm <project-dir> # actually delete (requires status: completed)
|
||||
/ultracontinue --help # this message
|
||||
|
||||
Reads .claude/projects/<project>/.session-state.local.json (per-project,
|
||||
gitignored). On a valid resumable state, prints a 3-line summary and begins
|
||||
|
|
@ -72,6 +74,52 @@ Typical flow:
|
|||
/ultracontinue # reads session-state, runs next session
|
||||
```
|
||||
|
||||
## Phase 0.5 — Cleanup mode dispatch
|
||||
|
||||
After `parseArgs` has resolved `$ARGUMENTS`, check the parsed `flags`
|
||||
object directly (NOT a string contains-check on raw `$ARGUMENTS` — that
|
||||
substring pattern was the root cause of Bug 1).
|
||||
|
||||
If `flags['--cleanup'] === true`, switch into the terminal cleanup
|
||||
flow and do NOT proceed to Phase 1 or any later phase.
|
||||
|
||||
**Required positional:** an explicit `<project-dir>` (`positional[0]`).
|
||||
There is no "clean all" mode — accidental wholesale deletion would be
|
||||
irreversible. If `positional[0]` is missing, empty, or starts with `-`,
|
||||
print this usage block to stderr and exit non-zero:
|
||||
|
||||
```
|
||||
Error: /ultracontinue-local --cleanup requires <project-dir>.
|
||||
Usage:
|
||||
/ultracontinue-local --cleanup <project-dir> # dry-run: list stale files
|
||||
/ultracontinue-local --cleanup --confirm <project-dir> # actually delete (status: completed)
|
||||
```
|
||||
|
||||
**Compute mode from parsed flags:**
|
||||
|
||||
```
|
||||
dryRun = (flags['--confirm'] !== true)
|
||||
confirm = (flags['--confirm'] === true)
|
||||
```
|
||||
|
||||
**Invoke cleanup inline.** Emit the concrete project-dir path as a literal
|
||||
token in the Bash command — never a template placeholder — same
|
||||
anti-substitution rule as Phase 2:
|
||||
|
||||
```
|
||||
node --input-type=module -e "import {cleanupProject} from './lib/util/cleanup.mjs'; const [, dir, mode] = process.argv; const r = cleanupProject(dir, {dryRun: mode !== 'confirm', confirm: mode === 'confirm'}); console.log(JSON.stringify(r, null, 2)); process.exit(r.valid ? 0 : 1)" '<RESOLVED-PROJECT-DIR>' '<MODE>'
|
||||
```
|
||||
|
||||
Substitute `<RESOLVED-PROJECT-DIR>` with the literal `positional[0]`
|
||||
value you have in your working context, and `<MODE>` with either the
|
||||
literal string `dryrun` or the literal string `confirm` based on the
|
||||
booleans above. The validator emits a `{valid, errors, warnings, parsed}`
|
||||
JSON record. Print it to stdout. Exit with the validator's exit code.
|
||||
|
||||
**Cleanup is a terminal mode.** It must not fall through to Phase 1/2/3/4.
|
||||
Operators who want to resume after cleanup must invoke `/ultracontinue`
|
||||
again without `--cleanup`.
|
||||
|
||||
## Phase 1 — Resolve project directory
|
||||
|
||||
The parsed `positional[0]` from Phase 0 is the explicit project-dir argument,
|
||||
|
|
|
|||
|
|
@ -267,3 +267,85 @@ test('ultracontinue Bug 2 — Phase 2 contains no {state-file-path} or any {curl
|
|||
'Phase 2 must document the deterministic Read tool flow',
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Step 10 — Bug 4 regression tests (Phase 0.5 wire-up + cleanup f-1/f-2/f-3)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
test('ultracontinue Bug 4 — Phase 0.5 documents cleanup mode dispatch', () => {
|
||||
const cmd = readCommand();
|
||||
assert.match(cmd, /## Phase 0\.5 /, 'Phase 0.5 header must be present');
|
||||
// Phase 0.5 must come BETWEEN Phase 0 and Phase 1.
|
||||
const idx05 = cmd.indexOf('## Phase 0.5 ');
|
||||
const idx1 = cmd.indexOf('## Phase 1 ');
|
||||
assert.ok(idx05 !== -1 && idx1 !== -1 && idx05 < idx1,
|
||||
'Phase 0.5 must appear before Phase 1');
|
||||
// Must reference cleanupProject and parsed flags['--cleanup'].
|
||||
const phase05 = extractPhase(cmd, '## Phase 0.5 ');
|
||||
assert.match(phase05, /cleanupProject/, 'Phase 0.5 must invoke cleanupProject');
|
||||
assert.match(phase05, /flags\['--cleanup'\]/, "Phase 0.5 must dispatch via flags['--cleanup']");
|
||||
// Usage block must document both forms.
|
||||
assert.match(cmd, /--cleanup --confirm/, 'usage must mention --cleanup --confirm');
|
||||
});
|
||||
|
||||
test('ultracontinue Bug 4 (f-1) dry-run lists candidates without deleting', async () => {
|
||||
const { cleanupProject } = await import('../../lib/util/cleanup.mjs');
|
||||
const root = mkdtempSync(join(tmpdir(), 'ultracontinue-cleanup-'));
|
||||
try {
|
||||
const dir = join(root, 'project-completed');
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(join(dir, '.session-state.local.json'), JSON.stringify({
|
||||
schema_version: 1,
|
||||
project: dir,
|
||||
next_session_brief_path: join(dir, 'brief.md'),
|
||||
next_session_label: 'Done',
|
||||
status: 'completed',
|
||||
updated_at: '2026-05-04T16:00:00.000Z',
|
||||
}, null, 2));
|
||||
writeFileSync(join(dir, 'NEXT-SESSION-PROMPT.local.md'),
|
||||
'---\nproduced_by: ultraexecute-local\nproduced_at: 2026-05-04T16:00:00.000Z\n---\n\n# Done\n');
|
||||
const r = cleanupProject(dir, { dryRun: true });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.equal(r.parsed.wouldDelete.length, 2);
|
||||
assert.equal(readFileSync(join(dir, '.session-state.local.json'), 'utf8').length > 0, true);
|
||||
assert.equal(readFileSync(join(dir, 'NEXT-SESSION-PROMPT.local.md'), 'utf8').length > 0, true);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('ultracontinue Bug 4 (f-2) confirm deletes and (f-3) idempotent re-run handles already-clean dir', async () => {
|
||||
const { cleanupProject } = await import('../../lib/util/cleanup.mjs');
|
||||
const { existsSync } = await import('node:fs');
|
||||
const root = mkdtempSync(join(tmpdir(), 'ultracontinue-cleanup-'));
|
||||
try {
|
||||
const dir = join(root, 'project-completed');
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(join(dir, '.session-state.local.json'), JSON.stringify({
|
||||
schema_version: 1,
|
||||
project: dir,
|
||||
next_session_brief_path: join(dir, 'brief.md'),
|
||||
next_session_label: 'Done',
|
||||
status: 'completed',
|
||||
updated_at: '2026-05-04T16:00:00.000Z',
|
||||
}, null, 2));
|
||||
writeFileSync(join(dir, 'NEXT-SESSION-PROMPT.local.md'),
|
||||
'---\nproduced_by: ultraexecute-local\nproduced_at: 2026-05-04T16:00:00.000Z\n---\n\n# Done\n');
|
||||
|
||||
// f-2: confirm deletes
|
||||
const r2 = cleanupProject(dir, { dryRun: false, confirm: true });
|
||||
assert.equal(r2.valid, true, JSON.stringify(r2.errors));
|
||||
assert.equal(r2.parsed.deleted.length, 2);
|
||||
assert.equal(existsSync(join(dir, '.session-state.local.json')), false);
|
||||
assert.equal(existsSync(join(dir, 'NEXT-SESSION-PROMPT.local.md')), false);
|
||||
|
||||
// f-3: idempotent re-run on a fully-cleaned dir reports CLEANUP_NO_STATE_FILE
|
||||
// (no state file → nothing to clean) — a deterministic terminal signal,
|
||||
// not a crash. Operators can ignore it.
|
||||
const r3 = cleanupProject(dir, { dryRun: false, confirm: true });
|
||||
assert.equal(r3.valid, false);
|
||||
assert.ok(r3.errors.find(e => e.code === 'CLEANUP_NO_STATE_FILE'));
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue