Files
gbrain/src/core/advisor/run.ts
T
Garry TanandClaude Opus 4.8 8d0b779306 feat(chronicle): proactive advisor collector (#2390)
Life Chronicle Phase A.7. A brain-state advisor collector surfaces two
proactive signals in `gbrain advisor`: unresolved ontology conflicts (warn,
→ `gbrain ontology-contradictions`) and recent meetings with no timeline
coverage (info, → `gbrain chronicle-backfill`). Advisory-only (no dispatch_id);
runs over MCP too (not workspace-dependent); tolerant of pre-migration brains.
3 tests + advisor-op-gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:20:45 -07:00

94 lines
3.2 KiB
TypeScript

/**
* advisor/run.ts — runs the collectors and ranks the findings.
*
* Hardcoded v1 collector list (open-Q4): a static array keeps ordering
* deterministic and the surface auditable. Each collector runs in its OWN
* try/catch so one failure never kills the whole report. Workspace-dependent
* collectors (A1) are dropped over MCP.
*/
import type { AdvisorCollector, AdvisorContext, AdvisorFinding, AdvisorReport, AdvisorSeverity } from './types.ts';
import { collectVersion } from './collect-version.ts';
import { collectMigration } from './collect-migration.ts';
import { collectSchemaPack } from './collect-schema-pack.ts';
import { collectStalledJobs } from './collect-stalled-jobs.ts';
import { collectUsageShape } from './collect-usage-shape.ts';
import { collectSetupSmells } from './collect-setup-smells.ts';
import { collectUninstalledBrainPack } from './collect-uninstalled-brain-pack.ts';
import { collectUninstalledBundled } from './collect-uninstalled-bundled.ts';
import { collectChronicle } from './collect-chronicle.ts';
/** Deterministic v1 collector order (also the secondary sort key for ranking). */
export const COLLECTORS: AdvisorCollector[] = [
collectVersion,
collectMigration,
collectSchemaPack,
collectStalledJobs,
collectUsageShape,
collectSetupSmells,
collectUninstalledBrainPack,
collectUninstalledBundled,
collectChronicle,
];
const SEV_RANK: Record<AdvisorSeverity, number> = { critical: 0, warn: 1, info: 2 };
/**
* Rank: critical > warn > info, ties broken by collector order (stable). All
* criticals are always kept; the info tail is capped so the agent isn't buried.
*/
export function rankFindings(findings: AdvisorFinding[], opts: { infoCap?: number } = {}): AdvisorFinding[] {
const order = new Map(COLLECTORS.map((c, i) => [c.id, i] as const));
const sorted = [...findings].sort((a, b) => {
const s = SEV_RANK[a.severity] - SEV_RANK[b.severity];
if (s !== 0) return s;
return (order.get(a.collector) ?? 99) - (order.get(b.collector) ?? 99);
});
const infoCap = opts.infoCap ?? 10;
const out: AdvisorFinding[] = [];
let infoSeen = 0;
for (const f of sorted) {
if (f.severity === 'info') {
if (infoSeen >= infoCap) continue;
infoSeen++;
}
out.push(f);
}
return out;
}
/**
* Run every collector and return a ranked report. Resilient: a collector that
* throws contributes nothing and never aborts the others.
*/
export async function runAdvisor(ctx: AdvisorContext): Promise<AdvisorReport> {
const all: AdvisorFinding[] = [];
for (const c of COLLECTORS) {
try {
const found = await c.collect(ctx);
for (const f of found) {
// A1: drop workspace-dependent findings over MCP.
if (ctx.remote && f.workspace_dependent) continue;
all.push(f);
}
} catch {
// one collector failing must not kill the report
}
}
const ranked = rankFindings(all);
const worst: AdvisorSeverity | null =
ranked.some((f) => f.severity === 'critical')
? 'critical'
: ranked.some((f) => f.severity === 'warn')
? 'warn'
: ranked.length > 0
? 'info'
: null;
return {
version: ctx.version,
generated_at: ctx.now.toISOString(),
findings: ranked,
worst,
};
}