mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
b16c63f346
Compress prose across 18 preamble resolvers — Voice, Writing Style, AskUserQuestion Format, Completeness Principle, Confusion Protocol, Context Health, Context Recovery, Continuous Checkpoint, Lake Intro, Proactive Prompt, Routing Injection, Telemetry Prompt, Upgrade Check, Vendoring Deprecation, Writing Style Migration, Brain Sync Block, Completion Status, and Question Tuning. Same semantic contract, ~half the bytes. Restored "Treat the skill file as executable instructions" phrase in the plan-mode info section after diagnosing it as load-bearing. Restored "Effort both-scales" rule in AskUserQuestion format. Bonus: scripts/skill-check.ts gains isRepoRootSymlink() so dev installs that mount the repo root at host/skills/gstack as a runtime sidecar (e.g., codex's .agents/skills/gstack) get skipped instead of double-counted. opus-4-7 model overlay gets a Fan-Out directive — explicit instruction to launch parallel reads/checks before synthesis. Net token impact across all generated SKILL.md files: ~140K tokens removed across 47 outputs. Plan-* skills retain full preamble surface (Brain Sync, Context Recovery, Routing Injection) — load-bearing functionality that early slim attempts incorrectly cut. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
154 lines
5.5 KiB
TypeScript
154 lines
5.5 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* skill:check — Health summary for all SKILL.md files.
|
|
*
|
|
* Reports:
|
|
* - Command validation (valid/invalid/snapshot errors)
|
|
* - Template coverage (which SKILL.md files have .tmpl sources)
|
|
* - Freshness check (generated files match committed files)
|
|
*/
|
|
|
|
import { validateSkill } from '../test/helpers/skill-parser';
|
|
import { discoverTemplates, discoverSkillFiles } from './discover-skills';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { execSync } from 'child_process';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const ROOT_REALPATH = fs.realpathSync(ROOT);
|
|
|
|
function isRepoRootSymlink(candidateDir: string): boolean {
|
|
try {
|
|
return fs.realpathSync(candidateDir) === ROOT_REALPATH;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Find all SKILL.md files (dynamic discovery — no hardcoded list)
|
|
const SKILL_FILES = discoverSkillFiles(ROOT);
|
|
|
|
let hasErrors = false;
|
|
|
|
// ─── Skills ─────────────────────────────────────────────────
|
|
|
|
console.log(' Skills:');
|
|
for (const file of SKILL_FILES) {
|
|
const fullPath = path.join(ROOT, file);
|
|
const result = validateSkill(fullPath);
|
|
|
|
if (result.warnings.length > 0) {
|
|
console.log(` \u26a0\ufe0f ${file.padEnd(30)} — ${result.warnings.join(', ')}`);
|
|
continue;
|
|
}
|
|
|
|
const totalValid = result.valid.length;
|
|
const totalInvalid = result.invalid.length;
|
|
const totalSnapErrors = result.snapshotFlagErrors.length;
|
|
|
|
if (totalInvalid > 0 || totalSnapErrors > 0) {
|
|
hasErrors = true;
|
|
console.log(` \u274c ${file.padEnd(30)} — ${totalValid} valid, ${totalInvalid} invalid, ${totalSnapErrors} snapshot errors`);
|
|
for (const inv of result.invalid) {
|
|
console.log(` line ${inv.line}: unknown command '${inv.command}'`);
|
|
}
|
|
for (const se of result.snapshotFlagErrors) {
|
|
console.log(` line ${se.command.line}: ${se.error}`);
|
|
}
|
|
} else {
|
|
console.log(` \u2705 ${file.padEnd(30)} — ${totalValid} commands, all valid`);
|
|
}
|
|
}
|
|
|
|
// ─── Templates ──────────────────────────────────────────────
|
|
|
|
console.log('\n Templates:');
|
|
const TEMPLATES = discoverTemplates(ROOT);
|
|
|
|
for (const { tmpl, output } of TEMPLATES) {
|
|
const tmplPath = path.join(ROOT, tmpl);
|
|
const outPath = path.join(ROOT, output);
|
|
if (!fs.existsSync(tmplPath)) {
|
|
console.log(` \u26a0\ufe0f ${output.padEnd(30)} — no template`);
|
|
continue;
|
|
}
|
|
if (!fs.existsSync(outPath)) {
|
|
hasErrors = true;
|
|
console.log(` \u274c ${output.padEnd(30)} — generated file missing! Run: bun run gen:skill-docs`);
|
|
continue;
|
|
}
|
|
console.log(` \u2705 ${tmpl.padEnd(30)} \u2192 ${output}`);
|
|
}
|
|
|
|
// Skills without templates
|
|
for (const file of SKILL_FILES) {
|
|
const tmplPath = path.join(ROOT, file + '.tmpl');
|
|
if (!fs.existsSync(tmplPath) && !TEMPLATES.some(t => t.output === file)) {
|
|
console.log(` \u26a0\ufe0f ${file.padEnd(30)} — no template (OK if no $B commands)`);
|
|
}
|
|
}
|
|
|
|
// ─── External Host Skills (config-driven) ───────────────────
|
|
|
|
import { getExternalHosts } from '../hosts/index';
|
|
|
|
for (const hostConfig of getExternalHosts()) {
|
|
const hostDir = path.join(ROOT, hostConfig.hostSubdir, 'skills');
|
|
if (fs.existsSync(hostDir)) {
|
|
console.log(`\n ${hostConfig.displayName} Skills (${hostConfig.hostSubdir}/skills/):`);
|
|
const dirs = fs.readdirSync(hostDir).sort();
|
|
let count = 0;
|
|
let missing = 0;
|
|
for (const dir of dirs) {
|
|
const skillDir = path.join(hostDir, dir);
|
|
if (isRepoRootSymlink(skillDir)) {
|
|
console.log(` - ${dir.padEnd(30)} — sidecar symlink, skipped`);
|
|
continue;
|
|
}
|
|
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
if (fs.existsSync(skillMd)) {
|
|
count++;
|
|
const content = fs.readFileSync(skillMd, 'utf-8');
|
|
const hasClaude = content.includes('.claude/skills');
|
|
if (hasClaude) {
|
|
hasErrors = true;
|
|
console.log(` \u274c ${dir.padEnd(30)} — contains .claude/skills reference`);
|
|
} else {
|
|
console.log(` \u2705 ${dir.padEnd(30)} — OK`);
|
|
}
|
|
} else {
|
|
missing++;
|
|
hasErrors = true;
|
|
console.log(` \u274c ${dir.padEnd(30)} — SKILL.md missing`);
|
|
}
|
|
}
|
|
console.log(` Total: ${count} skills, ${missing} missing`);
|
|
} else {
|
|
console.log(`\n ${hostConfig.displayName} Skills: ${hostConfig.hostSubdir}/skills/ not found (run: bun run gen:skill-docs --host ${hostConfig.name})`);
|
|
}
|
|
}
|
|
|
|
// ─── Freshness (config-driven) ──────────────────────────────
|
|
|
|
import { ALL_HOST_CONFIGS } from '../hosts/index';
|
|
|
|
for (const hostConfig of ALL_HOST_CONFIGS) {
|
|
const hostFlag = hostConfig.name === 'claude' ? '' : ` --host ${hostConfig.name}`;
|
|
console.log(`\n Freshness (${hostConfig.displayName}):`);
|
|
try {
|
|
execSync(`bun run scripts/gen-skill-docs.ts${hostFlag} --dry-run`, { cwd: ROOT, stdio: 'pipe' });
|
|
console.log(` \u2705 All ${hostConfig.displayName} generated files are fresh`);
|
|
} catch (err: any) {
|
|
hasErrors = true;
|
|
const output = err.stdout?.toString() || '';
|
|
console.log(` \u274c ${hostConfig.displayName} generated files are stale:`);
|
|
for (const line of output.split('\n').filter((l: string) => l.startsWith('STALE'))) {
|
|
console.log(` ${line}`);
|
|
}
|
|
console.log(` Run: bun run gen:skill-docs${hostFlag}`);
|
|
}
|
|
}
|
|
|
|
console.log('');
|
|
process.exit(hasErrors ? 1 : 0);
|