Files
upstream-atomic-crm/.claude/scripts/check-config-sync.mjs
upstream-mirror 93ecc1a6f8
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
snapshot of https://github.com/marmelab/atomic-crm.git (main, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:48:50 +05:00

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);
}