From 9f057a23f2cb16ed39cc82fece72f692a3e29edc Mon Sep 17 00:00:00 2001 From: tzioup Date: Sun, 19 Jul 2026 07:38:02 +0200 Subject: [PATCH] InstallHooks: reconcile copied hooks against registered hooks after apply Copying the hook scripts and merging hooks.json are two independent operations, each verified on its own. A hook can land on disk without any settings entry naming it, and nothing currently compares the two sets. After --apply, shell the shipped reconciler (Doctor.ts --reconcile, already on disk at this point) and include its unwired list in the install report. Degrades loudly if Doctor.ts is absent rather than skipping silently. On a clean v7.1.1 payload this reports LoopDetector, DriftReminder and PostToolObserver. Refs #1539. --- LifeOS/Tools/InstallHooks.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/LifeOS/Tools/InstallHooks.ts b/LifeOS/Tools/InstallHooks.ts index 030fb92181..bfdd39f6af 100755 --- a/LifeOS/Tools/InstallHooks.ts +++ b/LifeOS/Tools/InstallHooks.ts @@ -15,6 +15,7 @@ */ import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; import { join } from "node:path"; import { detectDevTree, mergeHooks } from "./InstallEngine"; @@ -101,7 +102,28 @@ function main(): void { cpSync(hooksPayloadDir, hooksDestDir, { recursive: true }); const hookFilesCopied = countFilesRec(hooksDestDir); - console.log(JSON.stringify({ ...report, written: true, backup, hookFilesCopied }, null, 2)); + // Copying hook scripts and merging the manifest are two independent operations; + // a hook can land on disk without any settings entry naming it. Ask the shipped + // reconciler whether every copied hook is actually reachable, so an unwired hook + // is loud at install time rather than silent until someone runs Doctor by hand. + const doctorPath = join(configRoot, "LIFEOS", "TOOLS", "Doctor.ts"); + let unwired: string[] | undefined; + let reconcile: string | undefined; + if (existsSync(doctorPath)) { + try { + const out = execFileSync("bun", [doctorPath, "--reconcile"], { + encoding: "utf-8", + env: { ...process.env, CLAUDE_CONFIG_DIR: configRoot }, + }); + unwired = JSON.parse(out).unwired ?? []; + } catch (err) { + reconcile = `reconciler failed: ${err instanceof Error ? err.message : String(err)}`; + } + } else { + reconcile = `Doctor.ts not found at ${doctorPath} — hooks copied but not reconciled`; + } + + console.log(JSON.stringify({ ...report, written: true, backup, hookFilesCopied, unwired, reconcile }, null, 2)); process.exit(0); }