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