193 lines
6 KiB
JavaScript
193 lines
6 KiB
JavaScript
#!/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, readFileSync, existsSync } from 'node:fs';
|
|
import { join, relative } from 'node:path';
|
|
import { spawn } from 'node:child_process';
|
|
import { getCacheDir } from '../../scripts/kb-update/lib/cross-platform-paths.mjs';
|
|
|
|
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 (from sitemap-based change report) ---
|
|
const now = Date.now();
|
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
const staleLevels = { critical: 0, high: 0, medium: 0 };
|
|
let lastPollDaysAgo = Infinity;
|
|
|
|
const changeReportPath = join(pluginRoot, 'scripts', 'kb-update', 'data', 'change-report.json');
|
|
if (existsSync(changeReportPath)) {
|
|
try {
|
|
const report = JSON.parse(readFileSync(changeReportPath, 'utf8'));
|
|
staleLevels.critical = report.by_priority?.critical || 0;
|
|
staleLevels.high = report.by_priority?.high || 0;
|
|
staleLevels.medium = report.by_priority?.medium || 0;
|
|
if (report.last_poll) {
|
|
lastPollDaysAgo = (now - new Date(report.last_poll).getTime()) / DAY_MS;
|
|
}
|
|
} catch {
|
|
// Ignore — fall back to showing no data
|
|
}
|
|
}
|
|
|
|
// Trigger background poll if >7 days since last check
|
|
if (lastPollDaysAgo > 7) {
|
|
const updateScript = join(pluginRoot, 'scripts', 'kb-update', 'run-weekly-update.mjs');
|
|
if (existsSync(updateScript)) {
|
|
try {
|
|
spawn('node', [updateScript], { detached: true, stdio: 'ignore' }).unref();
|
|
} catch {
|
|
// Non-critical — silent fail
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 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`);
|
|
|
|
// KB-update auto-cron status (written by scripts/kb-update/weekly-kb-cron.mjs).
|
|
// Surfaced BEFORE the staleness-poll block because cron failure is a higher-
|
|
// signal event (something the user actively configured stopped working) than
|
|
// the slower-moving "files are getting old" signal that follows.
|
|
try {
|
|
const kbStatusPath = join(getCacheDir('ms-ai-architect'), 'kb-update-status.json');
|
|
if (existsSync(kbStatusPath)) {
|
|
const kbStatus = JSON.parse(readFileSync(kbStatusPath, 'utf8'));
|
|
const surfaceStatuses = new Set(['failure', 'partial', 'budget_exceeded']);
|
|
if (kbStatus && surfaceStatuses.has(kbStatus.last_run_status)) {
|
|
parts.push(
|
|
`KB-update: ${kbStatus.last_run_status} (${kbStatus.last_run_ts}, log: ${kbStatus.log_file})`
|
|
);
|
|
}
|
|
}
|
|
} catch {
|
|
// Never block session start — silent on read or parse failure.
|
|
}
|
|
|
|
if (staleEntries.length > 0) {
|
|
const pollAge = lastPollDaysAgo < Infinity ? ` (pollet ${Math.floor(lastPollDaysAgo)}d siden)` : '';
|
|
parts.push(`KB: ${staleEntries.join(', ')} needs update${pollAge}`);
|
|
} else if (lastPollDaysAgo > 7) {
|
|
parts.push('KB: poll overdue');
|
|
}
|
|
|
|
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;
|
|
}
|
|
|