// hooks/lib/post-edit.mjs // Shared by the post-edit guard (P7, hooks/scripts/post-edit-check.mjs) and // the step check (K9, lib/verification/step-check.mjs): when the guard is on, // and where its log lives. // // On (operator decision 06.10): while Voyage executes a plan in the repo -- a // `.claude/projects/*/progress.json` (or legacy `.trekexecute-progress-*.json`) // with status "in_progress" -- or when the repo has switched it on with // `.claude/voyage-guards.json` = { "post_edit_check": true }. Off everywhere // else, because the guard is wired for every session in every repo. import { existsSync, readFileSync, readdirSync, mkdirSync, appendFileSync } from 'node:fs'; import { join } from 'node:path'; export const OPT_IN_FILE = join('.claude', 'voyage-guards.json'); function readJson(p) { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; } } function inProgress(p) { return readJson(p)?.status === 'in_progress'; } export function executingPlan(root) { const projects = join(root, '.claude', 'projects'); try { for (const d of readdirSync(projects, { withFileTypes: true })) { if (d.isDirectory() && inProgress(join(projects, d.name, 'progress.json'))) return true; } } catch { /* no projects dir */ } try { for (const f of readdirSync(root)) { if (/^\.trekexecute-progress-.+\.json$/.test(f) && inProgress(join(root, f))) return true; } } catch { /* unreadable root */ } return false; } export function optedIn(root) { const p = join(root, OPT_IN_FILE); return existsSync(p) && readJson(p)?.post_edit_check === true; } /** @returns {'plan'|'opt-in'|null} why the guard is on, or null when it is off */ export function postEditActive(root) { if (optedIn(root)) return 'opt-in'; if (executingPlan(root)) return 'plan'; return null; } export function postEditLogPath(gitDir) { return join(gitDir, 'voyage', 'post-edit.jsonl'); } export function appendPostEditLog(gitDir, record) { const p = postEditLogPath(gitDir); mkdirSync(join(gitDir, 'voyage'), { recursive: true }); appendFileSync(p, JSON.stringify(record) + '\n'); return p; } // The latest record per file (repo-relative path). export function latestPostEditRecords(gitDir) { const p = postEditLogPath(gitDir); const out = new Map(); if (!existsSync(p)) return out; for (const line of readFileSync(p, 'utf8').split('\n')) { if (!line.trim()) continue; try { const r = JSON.parse(line); if (r && typeof r.file === 'string') out.set(r.file, r); } catch { /* a torn line is skipped, never read as clean */ } } return out; }