From c21d7b253afbc78ea0926ffe8e1fffa3c7826e5f Mon Sep 17 00:00:00 2001 From: levineam Date: Tue, 21 Jul 2026 01:38:25 -0400 Subject: [PATCH] fix(doctor): derive host skill manifests (SUP-3488) (#2961) --- src/commands/doctor.ts | 21 ++++++++++----------- test/doctor.test.ts | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 8914cc707..c9457d318 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -4345,7 +4345,7 @@ export async function buildChecks( // 2. Skill conformance (SKILL group — gated) if (scope === 'all' && skillsDir) { - const conformanceResult = checkSkillConformance(skillsDir); + const conformanceResult = skillConformanceCheck(skillsDir); checks.push(conformanceResult); } @@ -7432,15 +7432,13 @@ function printAutoFixReport(report: AutoFixReport, dryRun: boolean, jsonOutput: /** Quick skill conformance check — frontmatter + required sections */ -function checkSkillConformance(skillsDir: string): Check { - const manifestPath = join(skillsDir, 'manifest.json'); - if (!existsSync(manifestPath)) { - return { name: 'skill_conformance', status: 'warn', message: 'manifest.json not found' }; - } - +export function skillConformanceCheck(skillsDir: string): Check { try { - const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')); - const skills = manifest.skills || []; + // Host workspaces are allowed to omit a gbrain-specific manifest. Keep + // conformance aligned with resolver_health and skill_brain_first by using + // the canonical fallback that derives entries from direct SKILL.md files. + const manifest = loadOrDeriveManifest(skillsDir); + const skills = manifest.skills; let passing = 0; const failing: string[] = []; @@ -7460,7 +7458,8 @@ function checkSkillConformance(skillsDir: string): Check { } if (failing.length === 0) { - return { name: 'skill_conformance', status: 'ok', message: `${passing}/${skills.length} skills pass` }; + const derivedNote = manifest.derived ? ' (derived from SKILL.md files)' : ''; + return { name: 'skill_conformance', status: 'ok', message: `${passing}/${skills.length} skills pass${derivedNote}` }; } return { name: 'skill_conformance', @@ -7468,7 +7467,7 @@ function checkSkillConformance(skillsDir: string): Check { message: `${passing}/${skills.length} pass. Failing: ${failing.join(', ')}`, }; } catch { - return { name: 'skill_conformance', status: 'warn', message: 'Could not parse manifest.json' }; + return { name: 'skill_conformance', status: 'warn', message: 'Could not load or derive skills manifest' }; } } diff --git a/test/doctor.test.ts b/test/doctor.test.ts index 63b627c14..c6df4d857 100644 --- a/test/doctor.test.ts +++ b/test/doctor.test.ts @@ -1,4 +1,7 @@ import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test'; +import { mkdirSync, rmSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; describe('doctor command', () => { test('doctor module exports runDoctor', async () => { @@ -120,6 +123,24 @@ describe('doctor command', () => { } }); + test('skill conformance derives a valid host manifest when manifest.json is absent', async () => { + const { skillConformanceCheck } = await import('../src/commands/doctor.ts'); + const skillsDir = join(tmpdir(), `gbrain-doctor-skills-${crypto.randomUUID()}`); + mkdirSync(join(skillsDir, 'host-only'), { recursive: true }); + writeFileSync( + join(skillsDir, 'host-only', 'SKILL.md'), + '---\nname: host-only\ndescription: host-owned skill\n---\n\n# Host-only\n', + ); + try { + const check = skillConformanceCheck(skillsDir); + expect(check).toMatchObject({ name: 'skill_conformance', status: 'ok' }); + expect(check.message).toContain('1/1 skills pass'); + expect(check.message).toContain('derived from SKILL.md files'); + } finally { + rmSync(skillsDir, { recursive: true, force: true }); + } + }); + // v0.31.2 — facts_extraction_health check added in PR1 commit 12. // Reads ingest_log rows with source_type='facts:absorb' (written by // writeFactsAbsorbLog from src/core/facts/absorb-log.ts), groups by