fix(doctor): derive host skill manifests (SUP-3488) (#2961)

This commit is contained in:
levineam
2026-07-20 22:38:25 -07:00
committed by GitHub
parent 4df7796061
commit c21d7b253a
2 changed files with 31 additions and 11 deletions
+10 -11
View File
@@ -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' };
}
}
+21
View File
@@ -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