diff --git a/plugins/ultraplan-local/hooks/scripts/pre-compact-flush.mjs b/plugins/ultraplan-local/hooks/scripts/pre-compact-flush.mjs index c1f2c7a..a98febd 100644 --- a/plugins/ultraplan-local/hooks/scripts/pre-compact-flush.mjs +++ b/plugins/ultraplan-local/hooks/scripts/pre-compact-flush.mjs @@ -6,6 +6,10 @@ // Direct fix for the documented P0 in // docs/ultraexecute-v2-observations-from-config-audit-v4.md. // +// v3.3.0: also refreshes sibling .session-state.local.json +// (Handover 7) so /ultracontinue can detect a resumable session +// even after a compaction event mid-run. +// // Behavior: // 1. Locate {cwd}/.claude/projects/* / progress.json (any nested project) // 2. Read progress.json + sibling plan.md @@ -13,12 +17,17 @@ // 4. For each commit, match against plan steps' commit_message_pattern // 5. If derived current_step > stored current_step → write fresh checkpoint // atomically (tmp + rename), monotonic only (current_step never decreases). -// 6. Always exit 0 — NEVER blocks compaction. +// 6. Refresh sibling .session-state.local.json if present and status is +// resumable (in_progress | partial) — bumps updated_at only. Never +// creates the state file; creation is the writer's job at session-end. +// Skips if status is completed/failed/stopped (non-resumable or terminal). +// 7. Always exit 0 — NEVER blocks compaction. // // v3.3.0: // - atomicWrite extracted to lib/util/atomic-write.mjs for reuse // - File reformatted (removed pre-existing leading-whitespace syntax error // that silently broke the hook since v3.1.0; PreCompact swallowed it) +// - Added Handover 7 sibling-state refresh import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'; import { join, dirname } from 'node:path'; @@ -104,6 +113,25 @@ function repoRootOf(dir) { } catch { return null; } } +// Resumable statuses for .session-state.local.json. `completed` is terminal; +// `failed`/`stopped` are operator-action-required and should NOT be silently +// refreshed by a background hook (would mask the alert). We only bump +// updated_at for in_progress | partial — the active-work statuses. +const SESSION_STATE_REFRESHABLE = new Set(['in_progress', 'partial']); + +function refreshSessionState(projDir) { + const statePath = join(projDir, '.session-state.local.json'); + if (!existsSync(statePath)) return false; + const state = readJson(statePath); + if (!state || typeof state !== 'object') return false; + if (!SESSION_STATE_REFRESHABLE.has(state.status)) return false; + // Monotonic guard: only mutate updated_at. Never touch status, project, + // next_session_*. The writer (Phase 8 / helper) owns those fields. + state.updated_at = new Date().toISOString(); + atomicWriteJson(statePath, state); + return true; +} + let stdinPayload = ''; try { stdinPayload = readFileSync(0, 'utf-8'); } catch { /* fine */ } @@ -142,7 +170,15 @@ for (const { projDir, progPath, planPath } of progressFiles) { }; } atomicWriteJson(progPath, progress); - process.stderr.write(`[ultraplan-local] pre-compact flush: ${progPath} → current_step=${derivedStep}\n`); + process.stderr.write(`[ultraplan-local] pre-compact flush: ${progPath} -> current_step=${derivedStep}\n`); + mutationsMade++; + } + + // Sibling .session-state.local.json refresh (Handover 7). Independent of + // progress.json mutation — the state file may exist for a session that + // hasn't advanced step yet, and we still want updated_at to track liveness. + if (refreshSessionState(projDir)) { + process.stderr.write(`[ultraplan-local] pre-compact refresh: ${projDir}/.session-state.local.json\n`); mutationsMade++; } }