From 8012f9c4950d8023a84ab4a5d0368bc49ab01c6f Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:03 +0000 Subject: [PATCH 1/8] fix(tools): skip the replay corpus check when the corpus is not installed checkReplayCorpus shelled `bun test test/regression` with no existsSync guard, but the payload ships no test/ dir, so it exited non-zero with no (fail) lines and raised a BLOCKING finding: /ic was permanently red on every fresh public install. Matches sibling checkRetirementRegistry. --- LifeOS/install/LIFEOS/TOOLS/IntegrityCheck.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/LifeOS/install/LIFEOS/TOOLS/IntegrityCheck.ts b/LifeOS/install/LIFEOS/TOOLS/IntegrityCheck.ts index fa7cceeb04..a327068427 100755 --- a/LifeOS/install/LIFEOS/TOOLS/IntegrityCheck.ts +++ b/LifeOS/install/LIFEOS/TOOLS/IntegrityCheck.ts @@ -859,6 +859,15 @@ function checkRuleDuplication(): void { function checkReplayCorpus(): void { const findings: Finding[] = []; let note: string | undefined; + // Same reason as checkRetirementRegistry: the replay fixtures live in the + // private source tree and are stripped from every public release. Without this + // guard `bun test` exited non-zero with no (fail) lines on every public + // install, recording a BLOCKING finding for a directory that is not supposed to + // exist there — a permanently red /ic on a clean install. + if (!existsSync(join(CLAUDE_DIR, 'test', 'regression'))) { + record('replay-corpus', [], 'skipped — replay corpus not installed'); + return; + } try { const out = execFileSync('bun', ['test', join(CLAUDE_DIR, 'test', 'regression')], { encoding: 'utf8', cwd: CLAUDE_DIR, stdio: ['ignore', 'pipe', 'pipe'], timeout: 120_000, From 65ff8cc1f50264200e1a52ade59399c81b99fe30 Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:03 +0000 Subject: [PATCH 2/8] fix(tools): match every writing-gate block reason in the recurrence ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The filter tested decision === "block", which WritingGate.hook.ts never writes — "block" is its response to the harness, not its telemetry vocabulary (it logs block-strong-no-run). Every writing-gate block was dropped from the ledger. --- LifeOS/install/LIFEOS/TOOLS/RecurrenceLedger.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/LifeOS/install/LIFEOS/TOOLS/RecurrenceLedger.ts b/LifeOS/install/LIFEOS/TOOLS/RecurrenceLedger.ts index ba4d3d3493..504449b285 100644 --- a/LifeOS/install/LIFEOS/TOOLS/RecurrenceLedger.ts +++ b/LifeOS/install/LIFEOS/TOOLS/RecurrenceLedger.ts @@ -144,7 +144,13 @@ const STREAMS: StreamDef[] = [ }, { file: "writing-gate.jsonl", - toEvent: r => r.decision === "block" + // The hook's `decision: "block"` is its RESPONSE to the harness; the row it + // appends carries the telemetry vocabulary (hooks/WritingGate.hook.ts writes + // block-strong-no-run / pass-run-verified / telemetry-weak / no-content / + // skip-recovery / telemetry-no-detector). Matching the literal "block" + // dropped every writing-gate block from this ledger. Prefix-match so new + // block-* reasons land here without a third place to edit. + toEvent: r => String(r.decision ?? "").startsWith("block") ? { classId: "wgate:block", ts: r.ts, source: "writing-gate", detail: `strong=${r.strong} weak=${r.weak}`, sessionId: r.session_id } : null, }, From 84d46b5053fef52d4262d7a5b8a6e6507b3f60e7 Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:03 +0000 Subject: [PATCH 3/8] fix(tools): reject a NaN iteration instead of writing null to the corpus Number("x") is NaN, which is typeof "number", and NaN < 0 is false, so a malformed --iteration passed validate and JSON.stringify wrote it as null into the corpus this gate exists to protect. --- LifeOS/install/LIFEOS/TOOLS/Reflect.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/LifeOS/install/LIFEOS/TOOLS/Reflect.ts b/LifeOS/install/LIFEOS/TOOLS/Reflect.ts index d0eb3f313e..f9b9c752f5 100644 --- a/LifeOS/install/LIFEOS/TOOLS/Reflect.ts +++ b/LifeOS/install/LIFEOS/TOOLS/Reflect.ts @@ -139,7 +139,10 @@ export function validate(r: Partial): string[] { for (const f of ["ts", "session_id", "slug"] as const) { if (typeof r[f] !== "string" || !r[f]) errs.push(`${f} must be a non-empty string`); } - if (typeof r.iteration !== "number" || r.iteration < 0) errs.push("iteration must be a non-negative number"); + // Number.isFinite, not just typeof: `Number("x")` is NaN, which is typeof + // "number" and fails every comparison, so `NaN < 0` waved it through — and + // JSON.stringify then wrote it as `null` into the corpus this gate protects. + if (typeof r.iteration !== "number" || !Number.isFinite(r.iteration) || r.iteration < 0) errs.push("iteration must be a non-negative number"); for (const f of ["claims_closed", "evidence_classes", "deploys"] as const) { if (!Array.isArray(r[f])) errs.push(`${f} must be an array`); else if ((r[f] as unknown[]).some((x) => typeof x !== "string")) errs.push(`${f} must contain only strings`); From 25ca9a202a07759835fb7e84f4f9ef87358aa17d Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:03 +0000 Subject: [PATCH 4/8] fix(tools): create the reflections directory before appending appendFileSync with no mkdirSync, where every sibling appender in this release does the mkdir first. An unhandled ENOENT means the reflection is simply lost. --- LifeOS/install/LIFEOS/TOOLS/Reflect.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/LifeOS/install/LIFEOS/TOOLS/Reflect.ts b/LifeOS/install/LIFEOS/TOOLS/Reflect.ts index f9b9c752f5..fa701d8df6 100644 --- a/LifeOS/install/LIFEOS/TOOLS/Reflect.ts +++ b/LifeOS/install/LIFEOS/TOOLS/Reflect.ts @@ -41,8 +41,8 @@ * written. A malformed record is never appended — that is the corpus gate. */ -import { existsSync, appendFileSync, readFileSync } from "node:fs"; -import { join } from "node:path"; +import { existsSync, appendFileSync, mkdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; const LIFEOS = process.env.LIFEOS_DIR ?? join(process.env.HOME ?? "~", ".claude", "LIFEOS"); const REFLECTIONS = join(LIFEOS, "MEMORY", "LEARNING", "REFLECTIONS", "algorithm-reflections.jsonl"); @@ -224,6 +224,7 @@ if (import.meta.main) { console.log(line); process.exit(0); } + mkdirSync(dirname(REFLECTIONS), { recursive: true }); appendFileSync(REFLECTIONS, line + "\n", "utf8"); const wb = record.within_budget === null ? "null (unaudited)" : String(record.within_budget); console.log(`✅ reflection appended · within_budget=${wb} · verdict=${spend.verdict ?? "none"} · dispatches=${spend.dispatches}`); From 31c1bfe2c17791efe0b81aac18e75ccbd2651d7b Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:03 +0000 Subject: [PATCH 5/8] fix(tools): stop ISARender clamping off-bracket phases to "Marking" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ASCENT_BRACKETS is a three-slot subset of the six run states, so indexOf returned -1 for verify/traverse/idle and Math.max(0, …) sent all three to index 0 — a `phase: verify` ISA rendered Marking in the bar while the badge on the same page said ANCHORING. Folds to the nearest earlier bracket by ASCENT order. --- LifeOS/install/LIFEOS/TOOLS/ISARender.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/LifeOS/install/LIFEOS/TOOLS/ISARender.ts b/LifeOS/install/LIFEOS/TOOLS/ISARender.ts index 3895262570..5ea05b2509 100644 --- a/LifeOS/install/LIFEOS/TOOLS/ISARender.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ISARender.ts @@ -19,7 +19,7 @@ import { readFileSync, writeFileSync, existsSync, statSync, renameSync, readdirS import { resolve, dirname, basename, join } from "node:path"; import { spawn } from "node:child_process"; import { homedir } from "node:os"; -import { ASCENT, ASCENT_BRACKETS, PHASE_TO_ASCENT } from "./ascent"; +import { ASCENT, ASCENT_BRACKETS, PHASE_TO_ASCENT, type AscentState } from "./ascent"; const HOME = process.env.HOME || homedir(); const TOOLS_DIR = resolve(HOME, ".claude/LIFEOS/TOOLS"); @@ -38,8 +38,25 @@ const WORK_JSON = resolve(HOME, ".claude/LIFEOS/MEMORY/STATE/work.json"); // and the Pulse board use. Never define a private stage vocabulary here: a second list is // how a mirror ends up disagreeing with the tab generated beside it. const STAGES = ASCENT_BRACKETS; + +// ASCENT_BRACKETS is a three-slot subset of the six run states, so a phase that +// maps to an off-bracket state has no slot of its own: `verify` → `anchoring`, +// `native` → `traverse`, `idle` → `idle`. Letting indexOf's -1 clamp to 0 sent +// all three to "Marking", so a `phase: verify` ISA rendered Marking in the bar +// while renderHeroBadges on the same page said ANCHORING. Fold to the last +// bracket at or before the state in the table's arc order instead. +function bracketIndex(state: AscentState): number { + const exact = ASCENT_BRACKETS.indexOf(state); + if (exact >= 0) return exact; + let idx = 0; + for (let i = 0; i < ASCENT_BRACKETS.length; i++) { + if (ASCENT[ASCENT_BRACKETS[i]].order <= ASCENT[state].order) idx = i; + } + return idx; +} + const STAGE_MAP: Record = Object.fromEntries( - Object.entries(PHASE_TO_ASCENT).map(([phase, state]) => [phase, Math.max(0, ASCENT_BRACKETS.indexOf(state))]), + Object.entries(PHASE_TO_ASCENT).map(([phase, state]) => [phase, bracketIndex(state)]), ); // ─────────── BRAND LOGO LOADER ─────────── From 8b6a3036635e0662ed4b7db7c4b3755cb99e5714 Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:03 +0000 Subject: [PATCH 6/8] fix(tools): make the config-eval lock atomic existsSync-then-write is not a lock: two sentinel files edited in the same instant both passed the check, both ran the suite and both notified. Uses the wx flag with stale reclaim and a lost-race fallback. --- .../LIFEOS/TOOLS/ConfigEvalOnChange.ts | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/LifeOS/install/LIFEOS/TOOLS/ConfigEvalOnChange.ts b/LifeOS/install/LIFEOS/TOOLS/ConfigEvalOnChange.ts index 88aa648074..f5d219c754 100644 --- a/LifeOS/install/LIFEOS/TOOLS/ConfigEvalOnChange.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ConfigEvalOnChange.ts @@ -14,7 +14,7 @@ * change together. Regression (suite below threshold) POSTs a voice/Pulse notice. */ -import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync, appendFileSync } from 'node:fs'; +import { mkdirSync, writeFileSync, rmSync, readFileSync, appendFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { homedir } from 'node:os'; import { runSuite } from '../../skills/Evals/Tools/EvalRunner.ts'; @@ -61,17 +61,30 @@ async function main(): Promise { const trigger = process.argv[2] ?? 'unknown'; mkdirSync(RESULTS_DIR, { recursive: true }); - // Single-flight: stale lock older than 10 min is ignored. - if (existsSync(LOCK)) { + // Single-flight: stale lock older than 10 min is reclaimed. The 'wx' flag is + // load-bearing — an existsSync-then-write let two sentinel files edited in the + // same instant both pass the check, both run the suite, and both notify. + try { + writeFileSync(LOCK, new Date().toISOString(), { flag: 'wx' }); + } catch { + let stale = true; try { const age = Date.now() - Date.parse(readFileSync(LOCK, 'utf8').trim() || ''); - if (Number.isFinite(age) && age < 10 * 60_000) { - log({ event: 'skip-locked', trigger }); - return; - } - } catch { /* fall through and re-acquire */ } + stale = !Number.isFinite(age) || age >= 10 * 60_000; + } catch { /* unreadable lock — treat as stale and reclaim */ } + if (!stale) { + log({ event: 'skip-locked', trigger }); + return; + } + rmSync(LOCK, { force: true }); + try { + writeFileSync(LOCK, new Date().toISOString(), { flag: 'wx' }); + } catch { + // Another run reclaimed the stale lock first; it owns this fire. + log({ event: 'skip-locked', trigger }); + return; + } } - writeFileSync(LOCK, new Date().toISOString()); try { const result = await runSuite(SUITE); From cd00168afc33a11f9899f2d9a799a89c610136c3 Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:03 +0000 Subject: [PATCH 7/8] fix(tools): notify when the behavioural suite cannot run The error was swallowed into a JSONL line nobody tails, so a permanently broken suite looked exactly like a clean pass on every config edit. A suite that cannot run is the same operational fact as one that fails: the change went unverified. --- LifeOS/install/LIFEOS/TOOLS/ConfigEvalOnChange.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/LifeOS/install/LIFEOS/TOOLS/ConfigEvalOnChange.ts b/LifeOS/install/LIFEOS/TOOLS/ConfigEvalOnChange.ts index f5d219c754..071a8bd9fe 100644 --- a/LifeOS/install/LIFEOS/TOOLS/ConfigEvalOnChange.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ConfigEvalOnChange.ts @@ -97,7 +97,14 @@ async function main(): Promise { ); } } catch (e) { - log({ event: 'error', trigger, error: (e as Error)?.message ?? String(e) }); + // Notify, don't just log. A suite that cannot RUN is the same operational + // fact as a suite that fails: the config change went unverified. Logging to + // a JSONL nobody tails made a permanently-broken suite — the shipped one is + // still legacy v1 `tasks:`, which the runner cannot execute — look exactly + // like a clean pass on every config edit. + const msg = (e as Error)?.message ?? String(e); + log({ event: 'error', trigger, error: msg }); + await notify(`Behavioural eval could not run after editing ${trigger}: ${SUITE} errored (${msg}). The change is unverified.`); } finally { try { rmSync(LOCK, { force: true }); } catch { /* best-effort */ } } From 3f9a521e68f91d5e15d3e5305e969807e2721957 Mon Sep 17 00:00:00 2001 From: elhoim Date: Mon, 3 Aug 2026 07:01:03 +0000 Subject: [PATCH 8/8] fix(tools): exit non-zero when CreateUpdate fails main().catch(console.error) exited 0, and IntegrityMaintenance.ts checks only code === 0, so it booked a ledger entry for work that never happened. --- LifeOS/install/LIFEOS/TOOLS/CreateUpdate.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/LifeOS/install/LIFEOS/TOOLS/CreateUpdate.ts b/LifeOS/install/LIFEOS/TOOLS/CreateUpdate.ts index 666f0e8bb0..b9b1617018 100755 --- a/LifeOS/install/LIFEOS/TOOLS/CreateUpdate.ts +++ b/LifeOS/install/LIFEOS/TOOLS/CreateUpdate.ts @@ -675,4 +675,9 @@ async function main() { console.log(`Change Type: ${changeType}`); } -main().catch(console.error); +// Exit non-zero on failure: IntegrityMaintenance.ts records a ledger entry on +// `code === 0`, so swallowing the error into a 0 booked work that never happened. +main().catch((e) => { + console.error(e); + process.exit(1); +});