#!/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'; 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; }; 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; } const 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(/## [Ll]aering 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 pa 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 naermer seg slutt — fokus pa sluttspurt og forberedelse.'); parts.push('Anbefalt: /okr:sporing (endelig scoring), /okr:moter (review-mote).'); 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(`Laering 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); }