okr/scripts/write-org-profile.mjs
Kjell Tore Guttormsen 482effbad1 fix(okr): B2 ingestion-kode-hygiene + bump 1.7.1 (patch-lane)
8 hygiene-fikser fra review 2026-07-16 par.4 kode-lista, TDD roed-groenn
(suite 138 -> 149):
- innboks-split: slugify translittererer ae/oe (datatap-fiks)
- innboks-frontmatter: beskrivende feil ved manglende sourceMtime
- okf-index: --okf-version bumper eksisterende rot, flagg-tolerant CLI
  (exit 2 ved manglende verdi), sanitizeEntry strip C1/zero-width/bidi/
  Unicode-tag
- write-org-profile: circuit-breaker MERGER i stedet for aa overskrive
  full config (M4); test beviser at eksisterende config overlever
- compose-org-profile: intern ----linje trunkerer ikke blokken
- coaching-hook: at-risk teller status-markerte tabellrader (M1/m1)
- inject-okr-context: topic-guard treffer boeyningsformer (maalene)
- frontmatter: BOM/CRLF-toleranse (falsk mangler-type-fiks)

Versjonsflater bumpet til 1.7.1 (package/plugin/lock/CLAUDE/README/
SKILL x2/package-shape-test) + CHANGELOG 1.7.1-seksjon (B1+B2).
Release-tag + katalog-ref venter paa [G-B] operatoer-go.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 04:02:10 +02:00

81 lines
3.3 KiB
JavaScript

#!/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);
}