mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat(doctor): doctor-categories foundation — BRAIN/SKILL/OPS/META sets + drift guard
Categorizes every doctor check name into exactly one of four categories. Exported
constants + categorizeCheck(name) helper are the single source of truth for the
v0.41.20.0 brain_checks_score + category_scores + --scope=brain wave. Drift guard
test parses doctor.ts source for both inline {name: 'foo'} and helper
const name = 'foo' patterns; CI fires if any check name lacks a category.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(doctor): brain_checks_score + category_scores + --scope=brain skip-computation
Extends Check with optional category and DoctorReport with brain_checks_score +
category_scores (additive — schema_version stays at 2; back-compat health_score
math byte-identical). buildChecks gains --scope=brain with explicit early-skip
gates around the SKILL check group (resolver_health + skill_conformance +
skill_brain_first + whoknows_health). Sub-second doctor on a brain with thousands
of skills. computeDoctorReport tags every check via categorizeCheck() at compute
time. Human output leads with the brain figure and renders the weighted
BrainHealth.brain_score alongside.
Test seam fix in test/doctor-home-dir-in-worktree.test.ts: the pre-existing
fragile JSON parser walked back from "checks" to find the envelope's outer
brace; v0.41.20.0's new nested category_scores object broke that heuristic.
Anchored on the canonical {"schema_version" envelope prefix instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(status): gbrain status — single-screen brain health dashboard + get_status_snapshot MCP op
NEW gbrain status command (src/commands/status.ts) composes 6 sections:
sync (per-source last_sync_at + staleness via buildSyncStatusReport),
cycle (TWO rows: last autopilot-cycle + last autopilot-* of any kind —
reflects v0.36.4.0 health-aware autopilot's targeted handler routing;
totals read from result.report.totals per the canonical handler shape),
locks (gbrain_cycle_locks active rows), workers (readSupervisorEvents +
summarizeCrashes), queue (LIVE counts NO time-window — old stuck jobs
are exactly what status surfaces), autopilot (PID liveness via kill -0).
Stable --json envelope (schema_version: 1). Exit codes 0=ok / 1=snapshot
failed / 2=usage. --section filter.
Thin-client mode routes Sync + Cycle through NEW get_status_snapshot MCP
op (admin scope, NOT localOnly; payload deliberately omits Locks /
Workers / Queue / Autopilot so feature creep can't quietly widen the
admin-scoped data exposure). Local-only sections render "local-only —
N/A on remote brain" honestly instead of pretending the local install's
empty state is the remote brain's.
CLI dispatch: pre-engine-bind branch for thin-client (no PGLite needed)
+ engine-connected dispatch case for local mode. CLI-only architecture
per codex MAJOR-4 (status owns its own thin-client branch inside
runStatus, not routed through op dispatch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump version to v0.41.20.0 + CHANGELOG + TODOS + llms regen
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(doctor-categories): categorize batch_retry_health from v0.41.19.0 Supavisor wave
The drift guard correctly caught a new check introduced by master's v0.41.19.0
Supavisor Retry Cathedral (PR #1537). batch_retry_health surfaces batch-write
retry events from the new src/core/audit/batch-retry-audit.ts module — OPS
category (infrastructure liveness).
This is exactly why the drift guard exists: any future check added to doctor.ts
without a category entry fails CI immediately instead of silently degrading
to 'meta'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
4.3 KiB
TypeScript
95 lines
4.3 KiB
TypeScript
/**
|
|
* v0.41.19.0 `--scope=brain` skip-computation contract.
|
|
*
|
|
* The plan-eng-review D4/D9 lock: `--scope=brain` must SKIP computation of
|
|
* the SKILL check group (resolver_health, skill_conformance,
|
|
* skill_brain_first, whoknows_health), not just filter the output. The
|
|
* observable contract: with `--scope=brain`, the returned checks list
|
|
* contains zero entries with `category: 'skill'`, AND the resolver walk is
|
|
* NOT performed.
|
|
*
|
|
* This is the "sub-second on a brain with thousands of skills" win. Hermetic
|
|
* test: passes `engine=null` so no DB or PGLite is needed; uses `--fast` to
|
|
* skip the DB check path; sets $GBRAIN_SKILLS_DIR to a tmpdir to control the
|
|
* resolver walk's input cheaply.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { withEnv } from './helpers/with-env.ts';
|
|
import { buildChecks } from '../src/commands/doctor.ts';
|
|
|
|
function makeSkillsTree(): string {
|
|
const root = mkdtempSync(join(tmpdir(), 'gbrain-scope-test-'));
|
|
mkdirSync(join(root, 'foo'), { recursive: true });
|
|
// A minimal SKILL.md so checkResolvable / skill_conformance / skill_brain_first
|
|
// all have content to scan when scope=all. The body doesn't need to be
|
|
// valid in detail; we only care about whether the function INVOKES the
|
|
// resolver walk.
|
|
writeFileSync(
|
|
join(root, 'foo', 'SKILL.md'),
|
|
'---\nname: foo\ntriggers:\n - "test trigger"\nbrain_first: exempt\n---\n\n# foo\n\nA test skill.\n',
|
|
);
|
|
writeFileSync(
|
|
join(root, 'RESOLVER.md'),
|
|
'| Skill | Triggers |\n|---|---|\n| **foo**: test trigger |\n',
|
|
);
|
|
return root;
|
|
}
|
|
|
|
describe('buildChecks --scope=brain skip-computation contract', () => {
|
|
test('SKILL group is absent from the checks list under --scope=brain', async () => {
|
|
const skillsDir = makeSkillsTree();
|
|
await withEnv({ GBRAIN_SKILLS_DIR: skillsDir, GBRAIN_NO_BANNER: '1' }, async () => {
|
|
const checks = await buildChecks(null, ['--scope=brain', '--fast']);
|
|
const skillChecks = checks.filter(
|
|
(c) =>
|
|
c.name === 'resolver_health' ||
|
|
c.name === 'skill_conformance' ||
|
|
c.name === 'skill_brain_first' ||
|
|
c.name === 'whoknows_health',
|
|
);
|
|
expect(skillChecks).toEqual([]);
|
|
});
|
|
});
|
|
|
|
test('SKILL group IS present under default scope (--fast alone)', async () => {
|
|
const skillsDir = makeSkillsTree();
|
|
await withEnv({ GBRAIN_SKILLS_DIR: skillsDir, GBRAIN_NO_BANNER: '1' }, async () => {
|
|
const checks = await buildChecks(null, ['--fast']);
|
|
const names = new Set(checks.map((c) => c.name));
|
|
expect(names.has('resolver_health')).toBe(true);
|
|
// skill_conformance + skill_brain_first only run if skillsDir is detected
|
|
// AND has SKILL.md files — the makeSkillsTree fixture provides one.
|
|
expect(names.has('skill_conformance')).toBe(true);
|
|
expect(names.has('skill_brain_first')).toBe(true);
|
|
});
|
|
});
|
|
|
|
test('--scope=brain still emits non-skill checks (the brain figure is meaningful)', async () => {
|
|
const skillsDir = makeSkillsTree();
|
|
await withEnv({ GBRAIN_SKILLS_DIR: skillsDir, GBRAIN_NO_BANNER: '1' }, async () => {
|
|
const checks = await buildChecks(null, ['--scope=brain', '--fast']);
|
|
// At least one non-skill check must be present (e.g. the migration
|
|
// health/meta checks that always run in the FS phase, or schema_version
|
|
// which is the canonical META check).
|
|
expect(checks.length).toBeGreaterThan(0);
|
|
const cats = new Set(checks.map((c) => c.category));
|
|
// No skill-category checks at all.
|
|
expect(cats.has('skill')).toBe(false);
|
|
});
|
|
});
|
|
|
|
test('--scope=brain does NOT emit a "Could not find skills directory" warn (we deliberately skipped, not failed)', async () => {
|
|
await withEnv({ GBRAIN_SKILLS_DIR: '/nonexistent/path/should/not/exist', GBRAIN_NO_BANNER: '1' }, async () => {
|
|
const checks = await buildChecks(null, ['--scope=brain', '--fast']);
|
|
const resolverHealth = checks.find((c) => c.name === 'resolver_health');
|
|
// Under scope=brain, we don't even attempt to find the skills dir, so
|
|
// the "Could not find skills directory" branch should NOT fire.
|
|
expect(resolverHealth).toBeUndefined();
|
|
});
|
|
});
|
|
});
|