#!/usr/bin/env node // session-start-context.mjs // Shows active utredning sessions and KB staleness on session start. // Output: plain text to stdout (advisory, never blocking). import { readdirSync, statSync, existsSync } from 'node:fs'; import { join, relative } from 'node:path'; const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || join(process.cwd()); const cwd = process.cwd(); const lines = []; // --- 1. Check for active utredning sessions (.work/ directories) --- const workDir = join(cwd, '.work'); let activeUtredninger = 0; if (existsSync(workDir)) { try { const entries = readdirSync(workDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { activeUtredninger++; } } } catch { // Ignore read errors } } // Also check docs/**/utredning.md const docsDir = join(cwd, 'docs'); let utredningFiles = 0; if (existsSync(docsDir)) { try { utredningFiles = countFiles(docsDir, 'utredning.md'); } catch { // Ignore } } // --- 2. Check KB staleness (stat mtime, no content reading) --- const staleLevels = { critical: 0, high: 0, medium: 0 }; const now = Date.now(); const DAY_MS = 24 * 60 * 60 * 1000; const skillsDir = join(pluginRoot, 'skills'); if (existsSync(skillsDir)) { try { const skillDirs = readdirSync(skillsDir, { withFileTypes: true }); for (const skill of skillDirs) { if (!skill.isDirectory()) continue; const refsDir = join(skillsDir, skill.name, 'references'); if (!existsSync(refsDir)) continue; countStaleFiles(refsDir, staleLevels, now); } } catch { // Ignore } } // --- 3. Check EU AI Act deadlines --- const AI_ACT_DEADLINES = [ { date: new Date('2025-02-02'), label: 'Forbudte AI-praksiser (Art. 5)' }, { date: new Date('2025-08-02'), label: 'Governance + sanksjoner (Art. 99)' }, { date: new Date('2026-08-02'), label: 'GPAI-krav + høyrisiko i Annex III' }, { date: new Date('2027-08-02'), label: 'Alle høyrisiko-krav (full compliance)' }, ]; let nearestDeadline = null; for (const dl of AI_ACT_DEADLINES) { const daysLeft = Math.ceil((dl.date.getTime() - now) / DAY_MS); if (daysLeft > 0 && daysLeft <= 180) { if (!nearestDeadline || daysLeft < nearestDeadline.daysLeft) { nearestDeadline = { ...dl, daysLeft }; } } } // --- 4. Check onboarding status --- const orgDir = join(pluginRoot, 'org'); const ORG_FILES = [ 'organization-profile.md', 'technology-stack.md', 'security-compliance.md', 'architecture-decisions.md', 'business-references.md', ]; let orgComplete = 0; const orgExists = existsSync(orgDir); if (orgExists) { for (const f of ORG_FILES) { if (existsSync(join(orgDir, f))) orgComplete++; } } // --- 4. Build output --- const parts = []; if (activeUtredninger > 0) { parts.push(`${activeUtredninger} aktiv(e) utredning(er) i .work/`); } if (utredningFiles > 0) { parts.push(`${utredningFiles} utredningsdokument(er) i docs/`); } if (!orgExists || orgComplete === 0) { parts.push('Ingen virksomhetstilpasning. Kjør /architect:onboard (~5 min)'); } else if (orgComplete < ORG_FILES.length) { parts.push(`Onboarding ${orgComplete}/${ORG_FILES.length}. Kjør /architect:onboard for å fullføre`); } const staleEntries = []; if (staleLevels.critical > 0) staleEntries.push(`${staleLevels.critical} critical`); if (staleLevels.high > 0) staleEntries.push(`${staleLevels.high} high`); if (staleLevels.medium > 0) staleEntries.push(`${staleLevels.medium} medium`); if (staleEntries.length > 0) { parts.push(`KB stale: ${staleEntries.join(', ')}`); } if (nearestDeadline) { parts.push(`EU AI Act: ${nearestDeadline.daysLeft} dager til ${nearestDeadline.label}. Kjør /architect:classify`); } if (parts.length > 0) { lines.push(`Architect: ${parts.join('. ')}. /architect:help`); } else { lines.push('Architect: Ingen aktive sesjoner. KB oppdatert. /architect:help'); } if (lines.length > 0) { process.stdout.write(lines.join('\n') + '\n'); } // --- Helpers --- function countFiles(dir, filename) { let count = 0; try { const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { count += countFiles(fullPath, filename); } else if (entry.name === filename) { count++; } } } catch { // Ignore permission errors } return count; } function countStaleFiles(dir, levels, now) { try { const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { countStaleFiles(fullPath, levels, now); } else if (entry.name.endsWith('.md')) { try { const mtime = statSync(fullPath).mtimeMs; const ageDays = (now - mtime) / DAY_MS; if (ageDays > 180) levels.critical++; else if (ageDays > 90) levels.high++; else if (ageDays > 60) levels.medium++; } catch { // Skip unreadable files } } } } catch { // Ignore } }