#!/usr/bin/env node // coaching-hook.mjs // Event: SessionStart // Purpose: Proactive coaching based on cycle position and OKR status. // Zero npm dependencies. Target execution: <200ms. import { readFileSync, existsSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { parseFrontmatter } from '../../lib/frontmatter.mjs'; 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 { raw, get } = parseFrontmatter(content); if (raw === null) process.exit(0); const cycleId = get('id'); const fase = get('fase'); if (!cycleId) process.exit(0); // Parse cycle type and dates const cycleMatch = cycleId.match(/^([TQ])(\d)-(\d{4})$/); if (!cycleMatch) process.exit(0); const [, type, num, year] = cycleMatch; const cycleNum = parseInt(num, 10); const cycleYear = parseInt(year, 10); // Calculate cycle start/end month (0-indexed) let startMonth, endMonth, totalWeeks; if (type === 'T') { startMonth = (cycleNum - 1) * 4; // T1=0(Jan), T2=4(May), T3=8(Sep) endMonth = startMonth + 3; // T1=3(Apr), T2=7(Aug), T3=11(Dec) totalWeeks = 16; } else { startMonth = (cycleNum - 1) * 3; endMonth = startMonth + 2; totalWeeks = 13; } // Testable clock seam: OKR_NOW (ISO date) overrides the wall clock so the // phase branches (early/mid/late) can be asserted deterministically. const now = process.env.OKR_NOW ? new Date(process.env.OKR_NOW) : new Date(); const cycleStart = new Date(cycleYear, startMonth, 1); const cycleEnd = new Date(cycleYear, endMonth + 1, 0); // last day of end month // Calculate current week in cycle const msPerWeek = 7 * 24 * 60 * 60 * 1000; const elapsed = now - cycleStart; const currentWeek = Math.max(1, Math.min(totalWeeks, Math.ceil(elapsed / msPerWeek))); // Determine phase let phase; if (now < cycleStart || now > cycleEnd) { phase = 'between'; } else if (currentWeek <= 4) { phase = 'early'; } else if (currentWeek <= totalWeeks - 4) { phase = 'mid'; } else { phase = 'late'; } // Check for at-risk KR in status let atRiskCount = 0; const okrDir = join(cwd, '.claude', 'okr'); const statusPath = join(okrDir, 'syklus', cycleId, 'status.md'); if (existsSync(statusPath)) { try { const statusContent = readFileSync(statusPath, 'utf8'); const riskMatches = statusContent.match(/[Ii] fare|[Bb]lokkert|risk/gi); if (riskMatches) atRiskCount = riskMatches.length; } catch { /* skip */ } } // Check last archived cycle for learnings let lastLearning = ''; const histDir = join(okrDir, 'historikk'); if (existsSync(histDir)) { try { const dirs = readdirSync(histDir, { withFileTypes: true }) .filter(d => d.isDirectory()) .map(d => d.name) .sort() .reverse(); if (dirs.length > 0) { const retroPath = join(histDir, dirs[0], 'retrospektiv.md'); if (existsSync(retroPath)) { const retro = readFileSync(retroPath, 'utf8'); const learningMatch = retro.match(/## (?:Laering|Læring) til neste syklus\n\n([\s\S]*?)(?:\n##|$)/); if (learningMatch) { const lines = learningMatch[1].trim().split('\n').filter(l => l.trim()); if (lines.length > 0) lastLearning = lines[0].replace(/^[-*]\s*/, ''); } } } } catch { /* skip */ } } // Build coaching message const parts = []; parts.push(`OKR coaching: Uke ${currentWeek} av ${totalWeeks} i ${cycleId}.`); if (phase === 'early') { parts.push('Tidlig i syklusen — fokus på alignment og kvalitetssikring av OKR.'); parts.push('Anbefalt: /okr:gap (dekning mot tildelingsbrev), /okr:kvalitet (kvalitetssjekk).'); } else if (phase === 'mid') { parts.push('Midtveis i syklusen — tid for fremdriftssjekk.'); parts.push('Anbefalt: /okr:sporing (statusoppdatering og scoring).'); if (atRiskCount > 0) { parts.push(`OBS: ${atRiskCount} KR er merket som i fare/blokkert i siste status.`); } } else if (phase === 'late') { parts.push('Syklusen nærmer seg slutt — fokus på sluttspurt og forberedelse.'); parts.push('Anbefalt: /okr:sporing (endelig scoring), /okr:møter (review-møte).'); if (currentWeek >= totalWeeks - 2) { parts.push('Mindre enn 2 uker igjen. Vurder /okr:oppsett arkiver for retrospektiv.'); } if (atRiskCount > 0) { parts.push(`OBS: ${atRiskCount} KR er i fare — vurder tiltak eller juster forventninger.`); } } else { // between cycles parts.push('Mellom sykluser. Anbefalt: /okr:oppsett arkiver eller /okr:skriv for ny syklus.'); } if (lastLearning) { parts.push(`Læring fra forrige syklus: "${lastLearning}"`); } const msg = parts.join(' '); process.stdout.write(JSON.stringify({ systemMessage: msg })); } catch { // Graceful exit on any error — never block the user process.exit(0); }