mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat(skillpack): brain_resident manifest fields + init-brain-pack scaffolder + tools version-skew lint Add optional brain_resident/schema_pack to the v1 manifest (additive, forward-compatible). New runInitBrainPack scaffolds a brain-resident pack (brain_resident:true, exact gbrain_min_version, 5-section machine-parseable README) beside brain content; applyWritePlan factored out of init-scaffold. brain-pack-lint validates each skill's declared tools: against the serving op set (E6 version-skew). Wires gbrain skillpack init-brain-pack. * feat(skillpack): Topology A brain-pack discovery on sources add + bounded nag After 'gbrain sources add', if the source ships a brain_resident pack, print an agent-readable advisory (ask the user before scaffolding). nag-state.ts tracks declines per (source-repo brain_id, source, pack) with escalate-then-suppress; declines count only on CLI-interactive displays, never cron/MCP. Fail-open: a malformed/absent pack never breaks sources add. * feat(advisor,skillpack): list_brain_skillpack MCP tool + gbrain advisor Topology B: dedicated source-scoped list_brain_skillpack op + get_skill source_id disambiguation (brain-resident-locate.ts); git scaffold-spec never a server FS path; source-aware schema match. LEARN_INSTRUCTION + serve-http banner. gbrain advisor: read-only ranked actions from brain state (8 resilient collectors, shared renderer, JSONL history, --json severity exit codes, local-only argv --apply dispatcher). Exposed over MCP behind mcp.publish_advisor (default off, read-only on remote; workspace collectors no-op remotely). Generalizes post-install-advisory to a single current-state recommended set (install→scaffold). * feat(skills): bundle gbrain-advisor skill + weekly cron recipe + ranking eval skills/gbrain-advisor teaches a harness to run gbrain advisor on a cadence and ping the user (read-only; ask before fixing). Registered in manifest.json, RESOLVER.md, openclaw.plugin.json. E4 ranking-precision eval on seeded-defect fixtures (100%). * chore: bump version and changelog (v0.42.47.0) Brain-resident skillpacks + gbrain advisor (#2180). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: sync CLAUDE.md + KEY_FILES for brain-resident skillpacks + advisor (#2180) Skill count 29->30, Skills section gains the brain-resident skillpacks + advisor capability, KEY_FILES gets current-state entries for the new modules. Regenerate llms bundles. * test,fix: align stale assertions with generalized advisory + advisor resolver triggers (#2180) - post-install-advisory.test.ts: install→scaffold wording (the install verb was removed); restore two-column in book-mirror copy; drop the removed skillpack-list line. - RESOLVER.md: gbrain-advisor trigger now fuzzy-matches a declared frontmatter trigger (resolver round-trip D5/C). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: regenerate llms bundle for updated advisor resolver row (#2180) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
5.0 KiB
TypeScript
128 lines
5.0 KiB
TypeScript
/**
|
|
* Tests for src/core/skillpack/brain-resident-locate.ts — the source-scoped
|
|
* discovery behind the list_brain_skillpack MCP tool and get_skill source_id.
|
|
*/
|
|
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
import { mkdtempSync, rmSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
|
|
import { runInitBrainPack } from '../src/core/skillpack/init-brain-pack.ts';
|
|
import {
|
|
loadResidentPacksForServer,
|
|
getResidentSkillDetail,
|
|
deriveBrainId,
|
|
} from '../src/core/skillpack/brain-resident-locate.ts';
|
|
import type { OperationContext } from '../src/core/operations.ts';
|
|
|
|
let root: string;
|
|
let packDir: string;
|
|
let plainDir: string;
|
|
|
|
beforeEach(() => {
|
|
root = mkdtempSync(join(tmpdir(), 'gbrain-brl-'));
|
|
packDir = join(root, 'pack-source');
|
|
plainDir = join(root, 'plain-source');
|
|
runInitBrainPack({ targetDir: packDir, name: 'deal-brain', firstSkillSlug: 'diligence', schemaPack: 'gbrain-base' });
|
|
// plainDir: a source with no skillpack at all
|
|
mkdtempSync(join(tmpdir(), 'noop-')); // touch tmp to avoid lints; plainDir stays empty
|
|
});
|
|
afterEach(() => {
|
|
rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
/** Minimal fake engine: only executeRaw (sources SELECT) + getConfig are used. */
|
|
function fakeEngine(sources: Array<{ id: string; local_path: string | null; config?: Record<string, unknown> }>, cfg: Record<string, string> = {}) {
|
|
return {
|
|
executeRaw: async () =>
|
|
sources.map((s) => ({
|
|
id: s.id,
|
|
name: s.id,
|
|
local_path: s.local_path,
|
|
last_commit: null,
|
|
last_sync_at: null,
|
|
config: s.config ?? {},
|
|
created_at: new Date(),
|
|
archived: false,
|
|
})),
|
|
getConfig: async (k: string) => cfg[k] ?? null,
|
|
} as unknown as OperationContext['engine'];
|
|
}
|
|
|
|
function ctxFor(engine: OperationContext['engine'], over: Partial<OperationContext> = {}): OperationContext {
|
|
return {
|
|
engine,
|
|
config: {} as OperationContext['config'],
|
|
logger: { info() {}, warn() {}, error() {}, debug() {} } as unknown as OperationContext['logger'],
|
|
dryRun: false,
|
|
remote: true,
|
|
...over,
|
|
} as OperationContext;
|
|
}
|
|
|
|
describe('loadResidentPacksForServer', () => {
|
|
test('finds brain_resident packs, skips plain sources', async () => {
|
|
const engine = fakeEngine([
|
|
{ id: 'default', local_path: packDir, config: { remote_url: 'https://github.com/u/deal-brain.git' } },
|
|
{ id: 'plain', local_path: plainDir },
|
|
]);
|
|
const result = await loadResidentPacksForServer(ctxFor(engine, { remote: false }));
|
|
expect(result.packs).toHaveLength(1);
|
|
const p = result.packs[0]!;
|
|
expect(p.name).toBe('deal-brain');
|
|
expect(p.source_id).toBe('default');
|
|
expect(p.skills.map((s) => s.slug)).toEqual(['diligence']);
|
|
// git remote → scaffold_spec is the git spec, NEVER a server FS path (#6)
|
|
expect(p.scaffold_spec).toBe('https://github.com/u/deal-brain.git');
|
|
expect(p.scaffold_spec).not.toContain(packDir);
|
|
});
|
|
|
|
test('computes schema_pack_match server-side against per-source config (#7)', async () => {
|
|
const engine = fakeEngine(
|
|
[{ id: 'default', local_path: packDir }],
|
|
{ 'schema_pack.source.default': 'gbrain-other' },
|
|
);
|
|
const result = await loadResidentPacksForServer(ctxFor(engine, { remote: false }));
|
|
expect(result.packs[0]!.active_schema_pack).toBe('gbrain-other');
|
|
expect(result.packs[0]!.schema_pack_match).toBe(false);
|
|
});
|
|
|
|
test('local-only source (no git remote) → scaffold_spec null', async () => {
|
|
const engine = fakeEngine([{ id: 'default', local_path: packDir }]);
|
|
const result = await loadResidentPacksForServer(ctxFor(engine, { remote: false }));
|
|
expect(result.packs[0]!.scaffold_spec).toBeNull();
|
|
});
|
|
|
|
test('source scoping: out-of-scope source is excluded', async () => {
|
|
const engine = fakeEngine([{ id: 'default', local_path: packDir }]);
|
|
// caller scoped to a different source id → no packs
|
|
const ctx = ctxFor(engine, { auth: { allowedSources: ['other'] } as OperationContext['auth'] });
|
|
const result = await loadResidentPacksForServer(ctx);
|
|
expect(result.packs).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('getResidentSkillDetail', () => {
|
|
test('returns the SKILL.md body for an in-pack slug', async () => {
|
|
const engine = fakeEngine([{ id: 'default', local_path: packDir }]);
|
|
const detail = await getResidentSkillDetail(ctxFor(engine, { remote: false }), 'default', 'diligence');
|
|
expect(detail.pack_name).toBe('deal-brain');
|
|
expect(detail.slug).toBe('diligence');
|
|
expect(detail.body).toContain('# diligence');
|
|
});
|
|
|
|
test('throws not_found for an unknown slug', async () => {
|
|
const engine = fakeEngine([{ id: 'default', local_path: packDir }]);
|
|
await expect(
|
|
getResidentSkillDetail(ctxFor(engine, { remote: false }), 'default', 'nope'),
|
|
).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('deriveBrainId', () => {
|
|
test('prefers git remote; falls back to path hash', () => {
|
|
expect(deriveBrainId('https://x/y.git', '/p')).toBe('git:https://x/y.git');
|
|
expect(deriveBrainId(null, '/p')).toMatch(/^path:[0-9a-f]{16}$/);
|
|
});
|
|
});
|