Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
87 lines
3.0 KiB
JavaScript
Executable File
87 lines
3.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// Verify that every agent-role referenced by a settings.json SubagentStop matcher
|
|
// is a declared role in harness.config.json (config.roles). Claude Code requires
|
|
// settings.json matchers to be literal strings (they cannot be computed from the
|
|
// config), so this check is the bridge that keeps the two in sync: rename a role
|
|
// in config AND settings -> the matcher token still resolves, pass; rename it in
|
|
// config only -> the stale settings token is not a config role, fail.
|
|
//
|
|
// Usage: node .claude/scripts/check-config-sync.mjs [--app <repo>]
|
|
// Exit 0 = in sync; exit 1 = drift (prints the offending tokens).
|
|
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { loadConfig, roleNames } from "../hooks/lib/config.mjs";
|
|
|
|
function resolveRepo(argv) {
|
|
const flag = argv.indexOf("--app");
|
|
if (flag !== -1 && argv[flag + 1]) return argv[flag + 1];
|
|
return process.env.APP_DIR || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
}
|
|
|
|
// SubagentStop matchers are role tokens (possibly `a|b`); PreToolUse/PostToolUse
|
|
// matchers are tool names (Bash, Agent, Write|Edit) and are NOT roles, so only
|
|
// SubagentStop is checked here.
|
|
function settingsRoleTokens(settings) {
|
|
const entries = settings.hooks?.SubagentStop ?? [];
|
|
const tokens = new Set();
|
|
for (const entry of entries) {
|
|
const matcher = entry.matcher;
|
|
if (!matcher || typeof matcher !== "string") continue;
|
|
for (const t of matcher
|
|
.split("|")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)) {
|
|
tokens.add(t);
|
|
}
|
|
}
|
|
return [...tokens];
|
|
}
|
|
|
|
export function checkConfigSync(repo) {
|
|
const settingsPath = join(repo, ".claude", "settings.json");
|
|
if (!existsSync(settingsPath)) {
|
|
return { ok: false, error: `settings.json not found at ${settingsPath}` };
|
|
}
|
|
let settings;
|
|
try {
|
|
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
} catch (e) {
|
|
return {
|
|
ok: false,
|
|
error: `settings.json is not valid JSON: ${e.message}`,
|
|
};
|
|
}
|
|
let roles;
|
|
try {
|
|
roles = new Set(roleNames(loadConfig(repo)));
|
|
} catch (e) {
|
|
return { ok: false, error: e.message };
|
|
}
|
|
const tokens = settingsRoleTokens(settings);
|
|
const missing = tokens.filter((t) => !roles.has(t));
|
|
return { ok: missing.length === 0, missing, tokens, roles: [...roles] };
|
|
}
|
|
|
|
// Run as CLI when invoked directly (not when imported by a test).
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
const repo = resolveRepo(process.argv.slice(2));
|
|
const r = checkConfigSync(repo);
|
|
if (r.error) {
|
|
console.error(`check-config-sync: ${r.error}`);
|
|
process.exit(1);
|
|
}
|
|
if (!r.ok) {
|
|
console.error(
|
|
`check-config-sync: settings.json SubagentStop matcher(s) reference roles not in harness.config.json: ${r.missing.join(", ")}.\n` +
|
|
`Declared roles: ${r.roles.join(", ")}.\n` +
|
|
`Rename the role in BOTH files, or add it to config.roles.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
process.stdout.write(
|
|
`check-config-sync: OK (${r.tokens.length} role matcher(s) all declared)\n`,
|
|
);
|
|
process.exit(0);
|
|
}
|