#!/usr/bin/env node // write-org-profile.mjs // Purpose: atomically write the OKR org profile to the reinstall-surviving // home path (~/.claude/okr/org/profil.md). On ANY write failure // (EACCES/EROFS/ENOSPC/pathguard-block) it circuit-breaks to the // project-local, gitignored .claude/okr.local.md so internal org state never // reaches a public mirror. Never exits non-zero -- the calling command must // not be blocked. Reads the full profile content from stdin (fd 0). // Zero npm dependencies (node: builtins only). ASCII-clean identifiers. // // Mirrors the canonical home path defined in // hooks/scripts/inject-okr-context.mjs:14 (the most-specific-wins read side). import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { homedir } from 'node:os'; // Read the entire profile content from stdin. Under execFileSync (both Claude // Code's command call and the tests) the child's stdin is a closed pipe, so // EOF arrives immediately -- no hang. let content = ''; try { content = readFileSync(0, 'utf8'); } catch { content = ''; } const homeTarget = join(homedir(), '.claude', 'okr', 'org', 'profil.md'); // Atomic write: temp file in the SAME directory, then renameSync over the // target. rename is atomic on the same filesystem -- a crash mid-write can // never leave a half-written profil.md. function writeAtomic(target, data) { const dir = dirname(target); mkdirSync(dir, { recursive: true }); const tmp = join(dir, `profil.md.${process.pid}.tmp`); writeFileSync(tmp, data); renameSync(tmp, target); } // M4 (B2): the fallback target (.claude/okr.local.md) may already carry the // FULL project config (cycle id, fase, onboarding, Linear). The circuit-breaker // must never overwrite it -- merge instead: the incoming profile's frontmatter // lines go FIRST inside ONE block (the flat parser is first-match, so the new // profile wins per key), the existing frontmatter lines and body follow intact. function mergeIntoExisting(target, incoming) { const prior = readFileSync(target, 'utf8'); const incomingInner = (incoming.match(/^---\r?\n([\s\S]*?)\r?\n---/) || [])[1] ?? incoming.trim(); const m = prior.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); if (!m) { // Existing file without frontmatter: preserve it verbatim as body. return `---\n${incomingInner}\n---\n${prior}`; } return `---\n${incomingInner}\n${m[1]}\n---\n${m[2]}`; } try { writeAtomic(homeTarget, content); process.stdout.write(homeTarget); process.exit(0); } catch (err) { // Circuit-breaker: the home write failed. Fall back to the project-local, // gitignored config so org state stays off any public mirror. The cycle / // historikk tree remains cwd-bound regardless; only the profile migrates. const fallback = join(process.cwd(), '.claude', 'okr.local.md'); try { writeAtomic(fallback, existsSync(fallback) ? mergeIntoExisting(fallback, content) : content); process.stderr.write( `notice: kunne ikke skrive hjem-profil (${err.code || err.message}); ` + `falt tilbake til prosjektlokal ${fallback}\n`, ); process.stdout.write(fallback); } catch (err2) { process.stderr.write( `notice: kunne ikke skrive verken hjem-profil eller prosjektlokal fallback ` + `(${err2.code || err2.message})\n`, ); } process.exit(0); }