113 lines
4.7 KiB
JavaScript
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);
|
|
}
|