ktg-plugin-marketplace/plugins/okr/hooks/scripts/inject-okr-context.mjs
Kjell Tore Guttormsen ac95cd6a30 feat(okr): sync to v1.3.0 from ktg-privat
Syncs all changes from v1.0.0 through v1.3.0:

v1.1 (quick fixes):
- Fix deprecated Viva Goals references
- Add DFO-OKR terminology mapping
- Add tillitsvalgt/fagforening perspective
- Update Objectives recommendation from 3-5 to 2-3

v1.1 (persistent context):
- Deep onboarding interview (full/mvp)
- Persistent .claude/okr/ directory tree
- Context-aware commands
- Cycle archival with retrospective

v1.3 (AI-first differentiators):
- /okr:gap — tildelingsbrev gap analysis with coverage matrix
- /okr:analyse — cross-cycle Mermaid analytics
- SessionStart coaching hook (proactive, phase-aware)
- gapanalytiker + trendanalytiker agents
- inject-okr-context.mjs extended for historikk/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 20:31:49 +02:00

158 lines
5.4 KiB
JavaScript

#!/usr/bin/env node
// inject-okr-context.mjs
// Event: UserPromptSubmit
// Purpose: Inject OKR organization context from .claude/okr.local.md and .claude/okr/ tree.
// Zero npm dependencies. Target execution: <50ms.
import { readFileSync, existsSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
const cwd = process.cwd();
const configPath = join(cwd, '.claude', 'okr.local.md');
if (!existsSync(configPath)) {
process.exit(0);
}
try {
const content = readFileSync(configPath, 'utf8');
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) process.exit(0);
const fm = match[1];
const get = (key) => {
const m = fm.match(new RegExp(`${key}:\\s*["']?([^"'\\n]+)["']?`));
return m ? m[1].trim() : null;
};
// 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 = fm.includes('aktivert: true') || fm.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');
// Scan .claude/okr/ directory tree (cap at 50 files)
const okrDir = join(cwd, '.claude', 'okr');
const dirParts = [];
let totalFiles = 0;
if (existsSync(okrDir)) {
try {
const topEntries = readdirSync(okrDir, { withFileTypes: true });
for (const entry of topEntries) {
if (!entry.isDirectory()) continue;
if (totalFiles >= 50) break;
try {
const subEntries = readdirSync(join(okrDir, entry.name), { withFileTypes: true });
const mdFiles = [];
const subDirs = [];
for (const sub of subEntries) {
if (totalFiles >= 50) break;
if (sub.isFile() && sub.name.endsWith('.md')) {
mdFiles.push(sub.name);
totalFiles++;
} else if (sub.isDirectory()) {
subDirs.push(sub.name);
}
}
// Enumerate nested subdirectories (e.g. syklus/T1-2026/)
for (const sd of subDirs) {
if (totalFiles >= 50) break;
try {
const nested = readdirSync(join(okrDir, entry.name, sd), { withFileTypes: true });
const nestedMd = [];
for (const n of nested) {
if (totalFiles >= 50) break;
if (n.isFile() && n.name.endsWith('.md')) {
nestedMd.push(n.name);
totalFiles++;
}
}
if (nestedMd.length > 0) {
dirParts.push(`${entry.name}/${sd}/ (${nestedMd.length} fil${nestedMd.length > 1 ? 'er' : ''}: ${nestedMd.join(', ')})`);
}
} catch { /* skip unreadable nested dirs */ }
}
if (mdFiles.length > 0) {
dirParts.push(`${entry.name}/ (${mdFiles.length} fil${mdFiles.length > 1 ? 'er' : ''}: ${mdFiles.join(', ')})`);
}
} catch { /* skip unreadable dirs */ }
}
} catch { /* .claude/okr/ scan failed — continue without */ }
}
// Scan historikk/ for archived cycle count
const histDir = join(okrDir, 'historikk');
const archivedCycles = [];
if (existsSync(histDir)) {
try {
const histEntries = readdirSync(histDir, { withFileTypes: true });
for (const entry of histEntries) {
if (entry.isDirectory()) {
archivedCycles.push(entry.name);
}
}
} catch { /* skip */ }
}
// List active cycle files
const cycleId = syklus;
const cycleParts = [];
if (cycleId) {
const cyclePath = join(okrDir, 'syklus', cycleId);
if (existsSync(cyclePath)) {
try {
const cycleEntries = readdirSync(cyclePath, { withFileTypes: true });
for (const e of cycleEntries) {
if (e.isFile() && e.name.endsWith('.md')) {
cycleParts.push(e.name);
}
}
} catch { /* skip */ }
}
}
// Build rich systemMessage
let msg = `OKR-kontekst (fra .claude/okr.local.md): ${parts.join(', ')}.`;
if (dirParts.length > 0) {
msg += `\nTilgjengelige kontekstfiler: ${dirParts.join('; ')}.`;
}
if (cycleParts.length > 0) {
msg += `\nAktive OKR-filer i syklus ${cycleId}: ${cycleParts.join(', ')}.`;
}
if (archivedCycles.length > 0) {
msg += `\nArkiverte sykluser (${archivedCycles.length}): ${archivedCycles.sort().join(', ')}.`;
msg += ' Bruk /okr:analyse for trendanalyse.';
}
if (dirParts.length > 0 || cycleParts.length > 0) {
msg += '\nBruk disse filene automatisk nar relevant — ikke be brukeren om a lime inn innhold som allerede finnes.';
}
process.stdout.write(JSON.stringify({ systemMessage: msg }));
} catch {
// Graceful exit on any error — never block the user
process.exit(0);
}