From af67362c686c15524faa92049d9276e1bd29f0ea Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 1 May 2026 20:51:42 +0200 Subject: [PATCH] feat(ultraplan-local): pre-compact-flush refreshes session-state.local.json [skip-docs] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the PreCompact hook with a sibling block that refreshes .session-state.local.json's updated_at when status is in_progress or partial. Per-project: runs after the existing progress.json mutation, inside the same loop iteration. Design: - Only refreshes existing state files; creation is the writer's job (ultraexecute Phase 8 / 2.55 / 4 + future helper command). - Monotonic guard: only updated_at is touched. project, status, next_session_brief_path, next_session_label remain owned by the writer. - Skips status in {completed, failed, stopped} — the latter two are operator-action-required and silently bumping updated_at would mask alert state. - Always exit 0; never blocks compaction. [skip-docs] rationale: README + CLAUDE.md updates land in Step 11. Co-Authored-By: Claude Opus 4.7 --- .../hooks/scripts/pre-compact-flush.mjs | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) 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++; } }