From 497142f26b84dc0ac2bc02bacb0cddebbb53b2dc Mon Sep 17 00:00:00 2001 From: hermaxtalaris Date: Wed, 20 May 2026 07:07:28 -0400 Subject: [PATCH 1/2] fix: route skillpack harvest intents --- skills/RESOLVER.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/RESOLVER.md b/skills/RESOLVER.md index 1c676bfd5..53f0777fe 100644 --- a/skills/RESOLVER.md +++ b/skills/RESOLVER.md @@ -59,7 +59,7 @@ This is the dispatcher. Skills are the implementation. **Read the skill file bef | "Skillify this", "is this a skill?", "make this proper" | `skills/skillify/SKILL.md` | | "Compress my resolver", "AGENTS.md too large", "RESOLVER.md too big", "functional area dispatcher", "shrink routing table" | `skills/functional-area-resolver/SKILL.md` | | "Is gbrain healthy?", morning health check, skillpack-check | `skills/skillpack-check/SKILL.md` | -| "harvest this skill into gbrain", "publish this skill to gbrain", "lift this skill upstream", "share this skill with other gbrain clients", "promote my skill to gbrain" | `skills/skillpack-harvest/SKILL.md` | +| "harvest this skill into gbrain", "publish this skill to gbrain", "lift this skill upstream", "skill back into gbrain", "fork-only skill", "skill with the gbrain bundle", "share this skill with other gbrain clients", "harvest my skill into gbrain", "promote this skill to gbrain", "skill in the gbrain bundle", "custom skill into the gbrain core" | `skills/skillpack-harvest/SKILL.md` | | Post-restart health + auto-fix, "did the container restart break anything", smoke test | `skills/smoke-test/SKILL.md` | | Cross-modal review, second opinion | `skills/cross-modal-review/SKILL.md` | | "Validate skills", skill health check | `skills/testing/SKILL.md` | From a9270db7a1c4a49129dfaa53c0f3dce56416cbc9 Mon Sep 17 00:00:00 2001 From: hermaxtalaris Date: Wed, 20 May 2026 08:35:27 -0400 Subject: [PATCH 2/2] fix: align remediation plan with active health deficits --- src/commands/autopilot.ts | 16 ++++++++-- src/commands/doctor.ts | 40 +++++++++++------------- src/core/brain-score-recommendations.ts | 18 +++++++++++ test/brain-score-recommendations.test.ts | 32 +++++++++++++++++++ 4 files changed, 82 insertions(+), 24 deletions(-) diff --git a/src/commands/autopilot.ts b/src/commands/autopilot.ts index c341c95cf..41d81be6f 100644 --- a/src/commands/autopilot.ts +++ b/src/commands/autopilot.ts @@ -343,8 +343,8 @@ export async function runAutopilot(engine: BrainEngine, args: string[]) { const score = health.brain_score; const ctx = { repoPath, - hasEmbeddingApiKey: !!(process.env.OPENAI_API_KEY || await engine.getConfig('openai_api_key')), - hasChatApiKey: !!(process.env.ANTHROPIC_API_KEY || await engine.getConfig('anthropic_api_key')), + hasEmbeddingApiKey: await isGatewayTouchpointAvailable('embedding'), + hasChatApiKey: await isGatewayTouchpointAvailable('chat'), }; const plan = computeRecommendations(health, ctx).filter((r) => r.status === 'remediable'); const estTotal = plan.reduce((s, r) => s + r.est_seconds, 0); @@ -909,3 +909,15 @@ function showStatus(json: boolean) { function escapeXml(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } + +async function isGatewayTouchpointAvailable(touchpoint: 'embedding' | 'chat'): Promise { + try { + // The CLI configures the gateway from the merged config plane before + // invoking autopilot. Direct env/DB key checks miss file-local credentials + // and non-OpenAI embedding providers. + const { isAvailable } = await import('../core/ai/gateway.ts'); + return isAvailable(touchpoint); + } catch { + return false; + } +} diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 909f5a184..ce80a105a 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -3724,7 +3724,7 @@ export async function runRemediationPlan( engine: BrainEngine, args: string[], ): Promise { - const { computeRecommendations, classifyChecks, maxReachableScore } = + const { computeRecommendations, classifyChecks, maxReachableScore, checksForRemediationContext } = await import('../core/brain-score-recommendations.ts'); const targetScore = parseIntFlag(args, '--target-score') ?? 90; @@ -3735,16 +3735,7 @@ export async function runRemediationPlan( const health = await engine.getHealth(); const ctx = await loadRecommendationContext(engine); const recs = computeRecommendations(health, ctx); - // Synthetic check list for classification — we don't need full doctor - // output, just the check names the recommendations care about. - const syntheticChecks = [ - { name: 'brain_score', status: 'ok' as const }, - { name: 'sync_freshness', status: 'ok' as const }, - { name: 'missing_embeddings', status: 'ok' as const }, - { name: 'dead_links', status: 'ok' as const }, - { name: 'orphan_pages', status: 'ok' as const }, - ]; - const classifications = classifyChecks(syntheticChecks, ctx); + const classifications = classifyChecks(checksForRemediationContext(health), ctx); const ceiling = maxReachableScore(health, classifications); const filteredRecs = recs.filter((r) => r.status === 'remediable'); @@ -3816,21 +3807,14 @@ export async function runRemediate( const skipConfirm = args.includes('--yes'); const jsonOutput = args.includes('--json'); - const { computeRecommendations, classifyChecks, maxReachableScore } = + const { computeRecommendations, classifyChecks, maxReachableScore, checksForRemediationContext } = await import('../core/brain-score-recommendations.ts'); const ctx = await loadRecommendationContext(engine); // Pre-flight ceiling check (D13) const initialHealth = await engine.getHealth(); - const syntheticChecks = [ - { name: 'brain_score', status: 'ok' as const }, - { name: 'sync_freshness', status: 'ok' as const }, - { name: 'missing_embeddings', status: 'ok' as const }, - { name: 'dead_links', status: 'ok' as const }, - { name: 'orphan_pages', status: 'ok' as const }, - ]; - const classifications = classifyChecks(syntheticChecks, ctx); + const classifications = classifyChecks(checksForRemediationContext(initialHealth), ctx); const ceiling = maxReachableScore(initialHealth, classifications); if (targetScore > ceiling) { console.error( @@ -3973,11 +3957,23 @@ async function loadRecommendationContext(engine: BrainEngine) { repoPath: repoPath ?? undefined, embeddingModel: embeddingModel ?? undefined, embeddingDimensions: embeddingDimensions ? Number(embeddingDimensions) : undefined, - hasEmbeddingApiKey: !!(process.env.OPENAI_API_KEY || await engine.getConfig('openai_api_key')), - hasChatApiKey: !!(process.env.ANTHROPIC_API_KEY || await engine.getConfig('anthropic_api_key')), + hasEmbeddingApiKey: await isGatewayTouchpointAvailable('embedding'), + hasChatApiKey: await isGatewayTouchpointAvailable('chat'), }; } +async function isGatewayTouchpointAvailable(touchpoint: 'embedding' | 'chat'): Promise { + try { + // connectEngine() configures the gateway from the merged config plane + // (env + ~/.gbrain/config.json + DB). Direct env/DB key checks miss + // file-local credentials and non-OpenAI embedding providers. + const { isAvailable } = await import('../core/ai/gateway.ts'); + return isAvailable(touchpoint); + } catch { + return false; + } +} + function parseIntFlag(args: string[], flag: string): number | null { const i = args.indexOf(flag); if (i === -1 || i === args.length - 1) return null; diff --git a/src/core/brain-score-recommendations.ts b/src/core/brain-score-recommendations.ts index fa03a6c13..d8d9094c5 100644 --- a/src/core/brain-score-recommendations.ts +++ b/src/core/brain-score-recommendations.ts @@ -106,6 +106,24 @@ export interface CheckClassification { reason?: string; } +/** + * Build the minimal check list used by doctor remediation surfaces. + * + * Only include deficit-specific checks when the health snapshot actually has + * that deficit. The previous caller-side synthetic list always included + * `missing_embeddings` and `dead_links`, which could surface blocked or + * remediable states for problems that were not present in the current brain + * health snapshot. + */ +export function checksForRemediationContext(health: BrainHealth): Check[] { + const checks: Check[] = [{ name: 'brain_score', status: 'ok' }]; + if (health.stale_pages > 0) checks.push({ name: 'sync_freshness', status: 'warn' }); + if (health.missing_embeddings > 0) checks.push({ name: 'missing_embeddings', status: 'warn' }); + if (health.dead_links > 0) checks.push({ name: 'dead_links', status: 'warn' }); + if (health.orphan_pages > 0) checks.push({ name: 'orphan_pages', status: 'warn' }); + return checks; +} + /** * Generate ordered Remediation list from health snapshot + context. * diff --git a/test/brain-score-recommendations.test.ts b/test/brain-score-recommendations.test.ts index e866eb53e..c0a6e35f4 100644 --- a/test/brain-score-recommendations.test.ts +++ b/test/brain-score-recommendations.test.ts @@ -4,6 +4,7 @@ import { classifyChecks, maxReachableScore, estimateAnthropicCost, + checksForRemediationContext, } from '../src/core/brain-score-recommendations.ts'; import type { BrainHealth } from '../src/core/types.ts'; @@ -160,6 +161,37 @@ describe('computeRecommendations', () => { }); }); +describe('checksForRemediationContext', () => { + test('does not surface missing_embeddings when embedding coverage is complete', () => { + const health = makeHealth({ + brain_score: 45, + missing_embeddings: 0, + dead_links: 0, + stale_pages: 0, + orphan_pages: 0, + }); + const names = checksForRemediationContext(health).map((c) => c.name); + expect(names).toEqual(['brain_score']); + }); + + test('surfaces only active remediation deficits', () => { + const health = makeHealth({ + stale_pages: 3, + missing_embeddings: 2, + dead_links: 1, + orphan_pages: 4, + }); + const names = checksForRemediationContext(health).map((c) => c.name); + expect(names).toEqual([ + 'brain_score', + 'sync_freshness', + 'missing_embeddings', + 'dead_links', + 'orphan_pages', + ]); + }); +}); + describe('classifyChecks (D13)', () => { test('remediable: missing_embeddings with API key', () => { const result = classifyChecks([{ name: 'missing_embeddings', status: 'fail' }], {