Det rapporterende gulvet (§4 Port 3, P3b): SessionStart-hooken surfacer en KB-tillit-énlinjer fra cachet verified-staleness-report.json. Et GULV, ikke en gate (§4c — unngå alarm-tretthet på 84 %-signal). summarizeTrustFreshness (detection-schedule.mjs, ren, speiler summarizeSkillQuality): surfacer KUN drift (stale-since-verified) + kontraktbrudd (unverified) — det handlingsbare «bekreft mot kilde»-settet. unmigrated (Spor-1-backlog, 306 i dag) ekskluderes bevisst: ved det volumet ville det fyre hver økt og bli støy; migrerings-censusen hører i CT5 (P3c). Tolererer manglende/malformed cache → null (gitignored, fraværende i fersk klon, krasjer aldri hooken). session-start-context.mjs: leser cachen read-only (samme mønster som skill-quality-blokken), dytter linjen når den finnes. Ende-til-ende verifisert: injisert stale-rapport → «KB-tillit: 3 filer driftet siden verifisering, 1 uverifisert — bekreft mot kilde (aldri auto-fiks)»; ekte korpus (306 unmigrated) → taus (ingen falsk alarm). TDD (Iron Law): 8 nye tester. Suite 612/612. Plugin-validering PASS.
279 lines
10 KiB
JavaScript
279 lines
10 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 {
|
|
loadScheduleConfig,
|
|
shouldRunDetection,
|
|
summarizeSkillLifecycle,
|
|
summarizeCourses,
|
|
summarizeSkillQuality,
|
|
summarizeTrustFreshness,
|
|
} from '../../scripts/kb-update/lib/detection-schedule.mjs';
|
|
import {
|
|
resolveOrgDir,
|
|
ORG_FILES,
|
|
FREE_CONTEXT_FILE,
|
|
buildOrgSummary,
|
|
} from '../../scripts/kb-update/lib/user-data.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
|
|
}
|
|
}
|
|
|
|
// Opt-in background detection (Spor C / C1). Default OFF: nothing spawns unless
|
|
// the user enabled scheduled_detection in ms-ai-architect.local.md. When on +
|
|
// stale, run the Claude-FREE detection entrypoint (poll → report → discover →
|
|
// skill-lifecycle) in the background. This never invokes Claude — apply stays
|
|
// manual + in-session. See detection-schedule.mjs for the ToS rationale.
|
|
const scheduleConfig = loadScheduleConfig(pluginRoot);
|
|
if (shouldRunDetection(scheduleConfig, lastPollDaysAgo).run) {
|
|
const detectScript = join(pluginRoot, 'scripts', 'kb-update', 'run-detection.mjs');
|
|
if (existsSync(detectScript)) {
|
|
try {
|
|
spawn('node', [detectScript], { detached: true, stdio: 'ignore' }).unref();
|
|
} catch {
|
|
// Non-critical — silent fail
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 3. Check EU AI Act deadlines ---
|
|
const AI_ACT_DEADLINES = [
|
|
// NB: Digital Omnibus (prov. enighet 2026-05-07) utsatte høyrisiko; datoer foreløpige til OJ-publisering.
|
|
{ date: new Date('2025-02-02'), label: 'Forbudte AI-praksiser (Art. 5)' },
|
|
{ date: new Date('2025-08-02'), label: 'GPAI-krav + governance/sanksjoner (Art. 99)' },
|
|
{ date: new Date('2026-08-02'), label: 'Transparens Art. 50 (syntetisk innhold)' },
|
|
{ date: new Date('2026-12-02'), label: 'Art. 50(2) merking — frist for eksisterende generative systemer (Omnibus)' },
|
|
{ date: new Date('2027-12-02'), label: 'Annex III høyrisiko (provisorisk, Omnibus — utsatt fra 2026-08-02)' },
|
|
{ date: new Date('2028-08-02'), label: 'Annex I høyrisiko innebygd (provisorisk, Omnibus)' },
|
|
];
|
|
|
|
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. Onboarding status + ambient org context (C2.1) ---
|
|
// The user-owned org dir (~/.claude/ms-ai-architect/org) wins; the legacy
|
|
// plugin org/ dir is the backward-compat fallback. Read the files once: their
|
|
// count drives the status line, their content drives the ambient summary that
|
|
// makes org context present in EVERY session (not just a "run onboarding" nudge).
|
|
function readOrgFiles(dir) {
|
|
const files = {};
|
|
let count = 0;
|
|
if (existsSync(dir)) {
|
|
for (const f of ORG_FILES) {
|
|
const p = join(dir, f);
|
|
if (existsSync(p)) {
|
|
try {
|
|
files[f] = readFileSync(p, 'utf8');
|
|
count++;
|
|
} catch {
|
|
// Unreadable file — skip; advisory only
|
|
}
|
|
}
|
|
}
|
|
// Optional free-prose context (C2.2): read it for the ambient summary, but
|
|
// do NOT count it — it is optional and must not affect onboarding completeness.
|
|
const fp = join(dir, FREE_CONTEXT_FILE);
|
|
if (existsSync(fp)) {
|
|
try {
|
|
files[FREE_CONTEXT_FILE] = readFileSync(fp, 'utf8');
|
|
} catch {
|
|
// Unreadable — skip; advisory only
|
|
}
|
|
}
|
|
}
|
|
return { files, count };
|
|
}
|
|
const userOrg = readOrgFiles(resolveOrgDir());
|
|
const orgData = userOrg.count > 0 ? userOrg : readOrgFiles(join(pluginRoot, 'org'));
|
|
const orgComplete = orgData.count;
|
|
const orgSummary = buildOrgSummary(orgData.files);
|
|
|
|
// --- 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 (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) {
|
|
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');
|
|
}
|
|
|
|
// Skill-lifecycle signals (Spor B/C) — read-only one-liner; never spawns.
|
|
const skillReportPath = join(pluginRoot, 'scripts', 'kb-eval', 'data', 'skill-lifecycle-report.json');
|
|
if (existsSync(skillReportPath)) {
|
|
try {
|
|
const skillSummary = summarizeSkillLifecycle(JSON.parse(readFileSync(skillReportPath, 'utf8')));
|
|
if (skillSummary) parts.push(skillSummary);
|
|
} catch {
|
|
// Ignore — advisory only
|
|
}
|
|
}
|
|
|
|
// Course-detection signals (Spor C / C3.6) — read-only one-liner; never spawns.
|
|
// A lead is a signal that a topic exists, not auto-ingest: surfacing it here
|
|
// invites the operator to run /architect:kb-update §3c, which gates the lead
|
|
// into the courses ledger. Mirrors the skill-signaler block above.
|
|
const courseReportPath = join(pluginRoot, 'scripts', 'kb-update', 'data', 'course-detection-report.json');
|
|
if (existsSync(courseReportPath)) {
|
|
try {
|
|
const courseSummary = summarizeCourses(JSON.parse(readFileSync(courseReportPath, 'utf8')));
|
|
if (courseSummary) parts.push(`${courseSummary}. Kjør /architect:kb-update`);
|
|
} catch {
|
|
// Ignore — advisory only
|
|
}
|
|
}
|
|
|
|
// Skill-quality score signals (Spor D / Steg C) — read-only one-liner; never
|
|
// spawns, never scores live. Reads the CACHED skill-score-report.json (written
|
|
// by `score-skill.mjs --write`). The cache is gitignored => absent in a fresh
|
|
// clone, so this surfaces only for a maintainer who has produced it locally;
|
|
// summarizeSkillQuality tolerates the missing/malformed case → null.
|
|
const scoreCachePath = join(pluginRoot, 'scripts', 'kb-eval', 'data', 'skill-score-report.json');
|
|
if (existsSync(scoreCachePath)) {
|
|
try {
|
|
const qualitySummary = summarizeSkillQuality(JSON.parse(readFileSync(scoreCachePath, 'utf8')));
|
|
if (qualitySummary) parts.push(qualitySummary);
|
|
} catch {
|
|
// Ignore — advisory only
|
|
}
|
|
}
|
|
|
|
// KB-trust reporting floor (Spor 3 Port 3 / P3b) — read-only one-liner; never
|
|
// judges live. Reads the CACHED verified-staleness-report.json (written by
|
|
// report-verified-staleness.mjs on the KB-refresh cadence). Gitignored => absent
|
|
// in a fresh clone; summarizeTrustFreshness tolerates the missing/malformed case
|
|
// → null. Surfaces only drift + contract breaches (§4c — a floor, not a gate).
|
|
const stalenessCachePath = join(pluginRoot, 'scripts', 'kb-update', 'data', 'verified-staleness-report.json');
|
|
if (existsSync(stalenessCachePath)) {
|
|
try {
|
|
const trustSummary = summarizeTrustFreshness(JSON.parse(readFileSync(stalenessCachePath, 'utf8')));
|
|
if (trustSummary) parts.push(trustSummary);
|
|
} catch {
|
|
// Ignore — advisory only
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
// Ambient org context (C2.1): inject the compact summary so org facts are
|
|
// PRESENT in the session, not merely discoverable. Length-capped by
|
|
// buildOrgSummary. Only emitted when onboarding has produced content.
|
|
if (orgSummary) {
|
|
lines.push('Virksomhetskontekst (auto-injisert fra ~/.claude/ms-ai-architect/org):');
|
|
for (const l of orgSummary.split('\n')) lines.push(`- ${l}`);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|