okr/hooks/scripts/inject-okr-context.mjs

113 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
// inject-okr-context.mjs
// Event: UserPromptSubmit
// Purpose: Inject the core OKR organization context from .claude/okr.local.md
// (or the home org profile) plus ONE pointer to the second-brain index.md.
// Cycle/history/context retrieval is on-demand via the okr-second-brain-search
// skill — no longer pre-injected here. Zero npm dependencies. Target: <50ms.
import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { parseFrontmatter } from '../../lib/frontmatter.mjs';
const cwd = process.cwd();
const projectConfigPath = join(cwd, '.claude', 'okr.local.md');
const homeConfigPath = join(homedir(), '.claude', 'okr', 'org', 'profil.md');
// Topic guard (UserPromptSubmit): only inject OKR context when the user's
// prompt is plausibly OKR-related. The stdin read is non-blocking here -- under
// execFileSync (Claude Code's hook call and the tests) stdin is a closed pipe
// so EOF is immediate; the isTTY guard avoids blocking in an interactive shell.
// Default to PASS (inject) on any doubt -- empty/unparseable stdin, missing
// prompt field, or TTY -- so we never drop a legitimate OKR prompt. Only an
// explicit non-matching prompt is suppressed. No network.
let rawPrompt = '';
try {
if (!process.stdin.isTTY) rawPrompt = readFileSync(0, 'utf8');
} catch {
rawPrompt = '';
}
if (rawPrompt) {
let promptText = null;
try {
promptText = JSON.parse(rawPrompt).prompt;
} catch {
promptText = null;
}
if (typeof promptText === 'string' && promptText.length > 0) {
const okrPattern = /\bokr\b|objective|key result|n[oø]kkelresultat|noekkelresultat|\bkr\b|\bm[aå]l\b|\bmaal\b|tildelingsbrev|kaskade|syklus|tertial|kvartal/i;
if (!okrPattern.test(promptText)) {
process.exit(0);
}
}
}
// Hybrid org-profile resolution (most-specific-wins): project-local overrides
// the machine-global home profile. The home path is the Fase 3 migration target;
// this read is forward-compatible and stays inert until that file exists.
// NOTE: only the org PROFILE resolves to home. Cycle/work data stays cwd-bound
// and is no longer pre-injected -- it is retrieved on-demand via the
// okr-second-brain-search skill.
const configPath = existsSync(projectConfigPath)
? projectConfigPath
: (existsSync(homeConfigPath) ? homeConfigPath : null);
if (!configPath) {
process.exit(0);
}
try {
const content = readFileSync(configPath, 'utf8');
const { raw, get } = parseFrontmatter(content);
if (raw === null) process.exit(0);
// Core fields (backwards-compatible with old 4-field format)
const org = get('navn') || get('name');
const syklus = get('gjeldende') || get('id') || get('current_cycle');
const sektor = get('sektor') || get('sector') || get('domene');
const linear = raw.includes('aktivert: true') || raw.includes('enabled: true');
// New v1.1 fields (silently skipped if absent)
const modenhet = get('modenhetsnivaa');
const fase = get('fase');
const frikoblet = get('okr_frikoblet_fra_loenn');
const trygghet = get('psykologisk_trygghet');
const kortform = get('kortform');
if (!org) process.exit(0);
// Build message parts
const parts = [`Organisasjon: ${org}${kortform ? ` (${kortform})` : ''}`];
if (syklus) parts.push(`Syklus: ${syklus}${fase ? ` [${fase}]` : ''}`);
if (sektor) parts.push(`Sektor: ${sektor}`);
if (modenhet) parts.push(`Modenhet: ${modenhet}`);
if (frikoblet !== null) parts.push(`OKR frikoblet fra lonn: ${frikoblet}`);
if (trygghet) parts.push(`Psykologisk trygghet: ${trygghet}`);
if (linear) parts.push('Linear: aktivert');
// Base context line from the core profile fields only. The directory-tree
// enumeration that used to live here (the :92-191 MOVE block) is removed:
// retrieval is now on-demand via the okr-second-brain-search skill, not
// pre-injected. The payload is therefore independent of file count. (SC5)
let msg = `OKR-kontekst (fra .claude/okr.local.md): ${parts.join(', ')}.`;
// Resolve ONE pointer to the second-brain index.md, reusing most-specific-wins
// (project bundle preferred, else home bundle). Project root is cwd-bound
// (.claude/okr/); the home bundle root is ~/.claude/okr/org/ (two-root model).
const projectIndex = join(cwd, '.claude', 'okr', 'index.md');
const homeIndex = join(homedir(), '.claude', 'okr', 'org', 'index.md');
const indexPath = existsSync(projectIndex)
? projectIndex
: (existsSync(homeIndex) ? homeIndex : null);
if (indexPath) {
msg += `\nPersonlig OKF-kontekst finnes - sok wikien (skill: okr-second-brain-search) eller se ${indexPath}.`;
}
process.stdout.write(JSON.stringify({ systemMessage: msg }));
} catch {
// Graceful exit on any error — never block the user
process.exit(0);
}