Skip to content
40 changes: 30 additions & 10 deletions LifeOS/install/LIFEOS/TOOLS/ConfigEvalOnChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -61,17 +61,30 @@ async function main(): Promise<void> {
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);
Expand All @@ -84,7 +97,14 @@ async function main(): Promise<void> {
);
}
} 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 */ }
}
Expand Down
7 changes: 6 additions & 1 deletion LifeOS/install/LIFEOS/TOOLS/CreateUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
21 changes: 19 additions & 2 deletions LifeOS/install/LIFEOS/TOOLS/ISARender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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<string, number> = 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 ───────────
Expand Down
9 changes: 9 additions & 0 deletions LifeOS/install/LIFEOS/TOOLS/IntegrityCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion LifeOS/install/LIFEOS/TOOLS/RecurrenceLedger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
10 changes: 7 additions & 3 deletions LifeOS/install/LIFEOS/TOOLS/Reflect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -139,7 +139,10 @@ export function validate(r: Partial<Reflection>): 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`);
Expand Down Expand Up @@ -221,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}`);
Expand Down