# Validators

Each validator is a named check with a command. The /validate skill
reads this file and executes each check in sequence, collating pass/fail
summary at the end.

## Format

For each validator:
- **Name** — short label for the summary table
- **Command** — shell command that returns exit 0 on success, non-zero on failure
- **What it catches** — brief description

## Active Validators

### skill-structure

```bash
bash scripts/skill-validator.sh templates/skills/*/SKILL.md .claude/skills/*/SKILL.md
```

Catches SKILL.md files that violate the best practices encoded in
`.claude/cabinet/skill-best-practices.md` — line count over 500, missing
or malformed name/description, broken reference depth, backslash paths,
and skill-type-specific rules (workflow vs cabinet). Fast (<5s for 50
skills). Runs every /validate invocation.

### manifest-drift

```bash
node scripts/cc-drift-check.cjs
```

Catches files listed in `.ccrc.json` that have been locally modified
relative to their upstream hashes. Flags files that should be updated
through the installer rather than edited in place.

### cabinet-structure

```bash
errors=0
for skill_dir in .claude/skills/cabinet-*/; do
  file="$skill_dir/SKILL.md"
  [ -f "$file" ] || continue
  name=$(basename "$skill_dir")

  if ! grep -q '^tools:' "$file" 2>/dev/null; then
    echo "WARN: $name missing 'tools:' frontmatter"
    errors=$((errors + 1))
  fi

  if ! grep -q '## Investigation Protocol' "$file" 2>/dev/null; then
    if ! grep -q '## Research Method' "$file" 2>/dev/null; then
      echo "WARN: $name missing Investigation Protocol or Research Method section"
      errors=$((errors + 1))
    fi
  fi

  if ! grep -q '## Portfolio Boundaries' "$file" 2>/dev/null; then
    echo "WARN: $name missing Portfolio Boundaries section"
    errors=$((errors + 1))
  fi

  if ! grep -q '## Calibration Examples' "$file" 2>/dev/null; then
    echo "WARN: $name missing Calibration Examples section"
    errors=$((errors + 1))
  fi

  if ! grep -q '## Historically Problematic Patterns' "$file" 2>/dev/null; then
    echo "WARN: $name missing Historically Problematic Patterns section"
    errors=$((errors + 1))
  fi
done

if [ "$errors" -gt 0 ]; then
  echo ""
  echo "$errors structural warnings found across cabinet members."
  echo "See .claude/cabinet/_cabinet-member-template.md for required structure."
  exit 1
fi
echo "All cabinet members pass structural validation."
```

Catches cabinet members missing required sections (Investigation
Protocol, Portfolio Boundaries, Calibration Examples, Historically
Problematic Patterns) or frontmatter fields (tools). Complements
skill-structure — that one is mechanical; this one checks sectional
structure specific to cabinet members.

### memory-structure

```bash
node scripts/validate-memory.mjs --quiet
```

Catches MEMORY.md exceeding session-start budget (200 lines / 25KB),
orphan memory files not indexed in MEMORY.md, broken references to
files that don't exist, and oversized topic-style files (>50KB).
Skips silently if the project has no memory directory yet.

### checkpoint-protocol-reference

```bash
proto="templates/cabinet/checkpoint-protocol.md"
[ -f ".claude/cabinet/checkpoint-protocol.md" ] && proto=".claude/cabinet/checkpoint-protocol.md"
[ -f "$proto" ] || { echo "checkpoint-protocol.md not present — skipping"; exit 0; }

errors=0
for name in execute execute-group; do
  f="templates/skills/$name/SKILL.md"
  [ -f "$f" ] || f=".claude/skills/$name/SKILL.md"
  [ -f "$f" ] || continue
  # Require BOTH the filename and the imperative read-phrase. Checking
  # only the filename would let a re-inline that keeps a stray
  # "see checkpoint-protocol.md" comment pass while dropping the actual
  # read-instruction — that is still drift.
  if ! grep -q "checkpoint-protocol.md" "$f" || ! grep -q "follow it, scoped to" "$f"; then
    echo "WARN: $name/SKILL.md no longer reads cabinet/checkpoint-protocol.md (missing filename or 'follow it, scoped to' instruction)"
    errors=$((errors + 1))
  fi
done

if [ "$errors" -gt 0 ]; then
  echo "$errors skill(s) dropped the checkpoint-protocol read-instruction."
  echo "Cabinet checkpoints rely on it — re-add the 'Read checkpoint-protocol.md and follow it, scoped to ...' step."
  exit 1
fi
echo "Checkpoint-protocol references intact."
```

Catches drift-by-deletion: `/execute` and `/execute-group` must each
read `cabinet/checkpoint-protocol.md` rather than inline (and silently
diverge from) the checkpoint mechanism. If a skill stops referencing the
protocol, its checkpoints have either been re-inlined or dropped — both
are drift. Skips silently if the protocol file isn't present (e.g., a
project that hasn't adopted the split yet).

### qa-dimensions

```bash
node scripts/qa-dimensions-validator.cjs
```

Catches malformed qa-dimensions.yaml: missing `dimensions:` key, empty
map, dimensions without paths or severity or checks, invalid severity
values (must be high|moderate|info), invalid tags (must be run|review).
Exits 0 silently if qa-dimensions.yaml doesn't exist (the checklist
engine is opt-in).

### artifacts-of-thought

```bash
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "not a git work tree — skipping"; exit 0; }
untracked=0
for dir in .claude/methodology .claude/plans; do
  [ -d "$dir" ] || continue
  for f in "$dir"/*.md; do
    [ -e "$f" ] || continue
    if ! git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
      echo "UNTRACKED: $f"
      untracked=$((untracked + 1))
    fi
  done
done
if [ "$untracked" -gt 0 ]; then
  echo ""
  echo "$untracked design record(s)/plan(s) are not tracked by git."
  echo "These are project thought-record, not ephemera. Commit them, or if"
  echo "one is genuinely disposable, move it out of these dirs. Never resolve"
  echo "this by gitignoring the directory — see .claude/rules/artifacts-of-thought.md."
  exit 1
fi
echo "All methodology records and plans are tracked."
```

Catches the silent-loss failure mode: a design record in
`.claude/methodology/` or a plan in `.claude/plans/` that git isn't
tracking — because it was just written and not committed, or because the
directory was gitignored (which un-tracks every *new* doc while older
committed ones survive, masking the problem). `README.md` indexes are
tracked, so they don't flag. Skips silently outside a git work tree or
when neither directory exists.

### qa-handoff-verdict

```bash
node -e '
const fs=require("fs"),os=require("os"),path=require("path");
const dir=path.join(os.homedir(),".claude-cabinet","watchtower","queue","items");
if(!fs.existsSync(dir)){console.log("no watchtower queue — skipping");process.exit(0)}
let bad=0;
for(const f of fs.readdirSync(dir)){
  if(!f.endsWith(".json"))continue;
  let j;try{j=JSON.parse(fs.readFileSync(path.join(dir,f),"utf8"))}catch{continue}
  if(j.category==="qa-handoff"&&j.status==="resolved"){
    const v=j.qa_verdict;
    if(!v||typeof v!=="object"||Array.isArray(v)){
      console.log("WARN bypass: "+j.id+" — resolved qa-handoff with no validated qa_verdict ("+(j.resolved_at||"").slice(0,10)+", "+(j.project||"?")+")");
      bad++;
    }
  }
}
if(bad>0){
  console.log("");
  console.log(bad+" resolved qa-handoff item(s) lack a validated qa_verdict — resolved by a");
  console.log("direct file write that bypassed the recipient gate (validateQaVerdict fires only");
  console.log("through resolveItem). Pre-gate items are legacy; post-gate ones mean QA was");
  console.log("recorded without a verdict. Review, and re-resolve through the gate if live.");
}else{
  console.log("All resolved qa-handoff items carry a validated qa_verdict.");
}
'
```

Surfaces the one hole in the staff-QA recipient gate: the gate guards
`resolveItem`, not the file. A session that writes queue-item JSON
directly can stamp a resolved `qa-handoff` with no (or an unvalidated)
`qa_verdict` — the bypass demonstrated 4× within hours of the gate
shipping. This is a **warning, not a gate** (exit 0): pre-gate handoffs
legitimately lack a verdict and can't be dated per-consumer, so failing
`/validate` on them would cry wolf. It still names every offender so a
post-gate bypass is visible. Skips silently when no watchtower queue
exists.

### qa-debt-payer-wired

Checks that the debt payer's parts are all actually present and connected.
This exists because a session once recorded the payer as BUILT while the
drain skill contained no Step 4 at all, and the plan documenting it read
as success for two days. A feature whose core is skill PROSE has no
natural gate — this is that gate.

```bash
node -e '
const fs=require("fs");
const need=[
  ["templates/scripts/qa-debt-ledger.mjs", null],
  ["templates/scripts/qa-debt-payer.mjs", null],
  ["templates/skills/qa-drain/SKILL.md", /^## Step 4: Pay down filed debt/m],
  ["templates/skills/qa-handoff/SKILL.md", /TOMBSTONE/],
];
let bad=0;
for(const [f,re] of need){
  if(!fs.existsSync(f)){console.log("FAIL missing: "+f);bad++;continue}
  if(re){
    const t=fs.readFileSync(f,"utf8");
    if(!re.test(t)){console.log("FAIL "+f+" does not match "+re);bad++}
  }
}
// The payer must not be re-derived in prose: the skill CALLS the module.
const drain=fs.existsSync("templates/skills/qa-drain/SKILL.md")?fs.readFileSync("templates/skills/qa-drain/SKILL.md","utf8"):"";
if(drain.includes("Step 4")&&!drain.includes("qa-debt-payer.mjs")){
  console.log("FAIL Step 4 exists but never references qa-debt-payer.mjs — the predicates are prose again");bad++;
}
// The retired wording must not come back.
if(/QA debt: zero/.test(drain.replace(/never call that "QA debt: zero"/g,""))){
  console.log("FAIL the retired \"QA debt: zero\" wording is live again");bad++;
}
if(bad===0)console.log("qa-debt payer wired: ledger + predicates + Step 4 + tombstone all present");
process.exit(bad>0?1:0);
'
```

**Registered because:** the payer is the one deliverable here whose core
is prose, and prose is what silently reverted last time.

### audit-coherence

```bash
[ -f scripts/audit-coherence-check.mjs ] || { echo "audit-coherence-check not present — skipping"; exit 0; }
node scripts/audit-coherence-check.mjs
```

Catches the audit-persistence failure mode (act:faf23e4c): a session runs
`/audit`, creates a `reviews/<date>/<time>/` run directory, but the
orchestrator dies (or skips phase 5) before ingesting findings into pib-db —
so the run directory exists on disk with no matching `audit_runs` row.
Suppression then stays permanently empty and the promotion signal is blind.
The check is a **warning, not a gate** (exit 0): a reviews dir newer than the
latest run may be an audit still in progress, so failing `/validate` would cry
wolf. Skips silently when the script, a `reviews/` directory, or pib-db is
absent. This is the single home for the reviews-vs-db coherence check — the
audit skill does not re-run it.

### patterns-scope

```bash
[ -f scripts/patterns-scope-check.mjs ] || { echo "patterns-scope-check not present — skipping"; exit 0; }
node scripts/patterns-scope-check.mjs
```

Catches cross-project pattern contamination (act:62608a5d): a
`.claude/skills/cabinet-*/patterns-project.md` entry stamped `**Project:**`
for a project other than this repo. Stage-1 audit members read+apply these
patterns, so a foreign one (observed: a "51 Notion-migrated meetings" entry in
this repo's process-therapist file) poisons every future audit. **Warning, not
a gate** (exit 0): a mis-stamped entry is a data error to fix, not a reason to
fail `/validate`. Unstamped legacy entries are not flagged (they predate the
stamp). Skips silently when the script or `.claude/skills` is absent.

### feedback-dwell

```bash
node -e '
const fs=require("fs"),path=require("path"),cp=require("child_process");
const root="feedback";
if(!fs.existsSync(root)){console.log("no feedback/ dir — skipping");process.exit(0)}
const files=fs.readdirSync(root).filter(f=>f.endsWith(".md"));
if(files.length===0){console.log("feedback/ root is clean — no untriaged files.");process.exit(0)}
const now=Date.now(),DAY=86400000;
let completed=new Set(),pibOk=false;
try{
  const out=cp.execSync("node scripts/pib-db.mjs query \"SELECT fid FROM actions WHERE completed=1 AND deleted_at IS NULL\"",{stdio:["ignore","pipe","ignore"]}).toString();
  for(const r of JSON.parse(out))completed.add(r.fid);
  pibOk=true;
}catch{}
let warn=0;
for(const f of files){
  const p=path.join(root,f);
  const m=f.match(/^(\d{4})-(\d{2})-(\d{2})/);
  const filed=m?Date.parse(m[1]+"-"+m[2]+"-"+m[3]+"T00:00:00Z"):fs.statSync(p).mtimeMs;
  const ageDays=Math.floor((now-filed)/DAY);
  if(ageDays>7){console.log("DWELL: "+p+" — untriaged "+ageDays+"d (>7d)");warn++;}
  if(pibOk){
    const fid=(fs.readFileSync(p,"utf8").match(/act:[0-9a-f]{8}/)||[])[0];
    if(fid&&completed.has(fid)){console.log("UNSWEPT: "+p+" — references completed "+fid+" but still in feedback/ root");warn++;}
  }
}
if(warn>0){
  console.log("");
  console.log(warn+" feedback/ root breach(es) of triage-at-arrival (dwell >7d or absorbed-but-unswept).");
  console.log("feedback/ root = NOT YET TRIAGED. File-or-decline, stamp the fid/decline, move to feedback/resolved/.");
  console.log("Rule: orient SKILL.md feedback-pipeline step 3 (adopted 2026-06-11).");
}else{
  console.log("feedback/ root within triage-at-arrival bounds ("+files.length+" file(s), all <=7d, none unswept).");
}
'
```

Catches the manual-triage-decay failure mode: `feedback/` root means "not
yet triaged" (orient SKILL.md feedback-pipeline step 3, adopted 2026-06-11),
but resolution was manual and lazy — 76 files once accumulated over two
months and needed a three-agent sweep to reconcile. Two signals: a root
`.md` older than 7 days (dwell breach — filing-date from the `YYYY-MM-DD`
name prefix, else mtime), and a root file referencing a **completed** action
fid (absorbed-but-unswept — triaged but never moved to `resolved/`). A
**warning, not a gate** (exit 0): feedback legitimately sits a few days
mid-triage, so failing `/validate` would cry wolf; it names every offender
instead. Condition 2 skips gracefully when pib-db is unavailable; the whole
check skips silently when there is no `feedback/` directory (consumer
projects file upstream via `/cc-feedback`, not a local root).

### output-register

```bash
node -e '
const fs=require("fs"),os=require("os"),path=require("path"),cp=require("child_process");
const dir=path.join(os.homedir(),".claude","output-styles");
if(!fs.existsSync(dir)){console.log("no ~/.claude/output-styles/ — output-register not installed, skipping");process.exit(0)}
const files=fs.readdirSync(dir).filter(f=>f.endsWith(".md"));
if(files.length===0){console.log("~/.claude/output-styles/ is empty — nothing to check.");process.exit(0)}
const MIN="2.0.37";
const cmp=(a,b)=>{const pa=String(a).split(".").map(Number),pb=String(b).split(".").map(Number);
  for(let i=0;i<Math.max(pa.length,pb.length);i++){const x=pa[i]||0,y=pb[i]||0;if(x>y)return 1;if(x<y)return -1}return 0};
let warn=0;
// 1. THE VERSION GATE. Below v2.0.37 `keep-coding-instructions` does not exist:
//    it is silently ignored, the false default applies, and Claude Code strips
//    its own software-engineering instructions with NO error. No amount of
//    file inspection can see this — only the version can.
let ver=null;
try{ver=(cp.execSync("claude --version",{stdio:["ignore","pipe","ignore"]}).toString().match(/(\d+\.\d+\.\d+)/)||[])[1]||null}catch{}
if(ver===null){
  console.log("NOTE: could not run `claude --version` — cannot confirm this Claude Code honors keep-coding-instructions (needs >= "+MIN+").");
}else if(cmp(ver,MIN)<0){
  console.log("BROKEN: Claude Code v"+ver+" is below v"+MIN+" — `keep-coding-instructions` is IGNORED here.");
  console.log("        Every installed output style is silently stripping Claude Code'"'"'s engineering instructions. Upgrade Claude Code.");
  warn++;
}
// 2. THE INSTALLED COPY — the file Claude Code actually reads. Checking only
//    templates/ would check the one file that cannot cause the failure.
for(const f of files){
  const src=fs.readFileSync(path.join(dir,f),"utf8");
  const m=/^---\r?\n([\s\S]*?)\n---/.exec(src);
  if(!m){console.log("BROKEN: "+f+" has no frontmatter — Claude Code cannot load it.");warn++;continue}
  const fm={};
  for(const line of m[1].split("\n")){const i=line.indexOf(":");if(i>0)fm[line.slice(0,i).trim()]=line.slice(i+1).trim()}
  // Mirror Claude Code TEt(): only literal true/"true" counts. `yes`/`on`/`1`
  // are YAML-truthy and read as undefined here, which STRIPS the instructions.
  const kc=fm["keep-coding-instructions"];
  if(kc!=="true"){console.log("BROKEN: "+f+" — keep-coding-instructions is "+(kc===undefined?"MISSING":JSON.stringify(kc))+", not literally true. Claude Code strips its engineering instructions when this style is active.");warn++}
  if(!fm.name){console.log("BROKEN: "+f+" — no frontmatter `name`; outputStyle cannot select it reliably.");warn++}
  if(!fm.description){console.log("WARN: "+f+" — no `description`; it renders blank in the /config picker.");warn++}
}
if(warn===0){
  // Positive confirmation: silence is ambiguous — it could mean healthy or
  // never-ran (maintainability.md, "no silent failures").
  console.log("output-register healthy: "+files.length+" style(s) in ~/.claude/output-styles/ carry keep-coding-instructions: true"+(ver?", on Claude Code v"+ver+" (>= "+MIN+")":""));
}else{
  console.log("");
  console.log(warn+" output-style problem(s). A style missing keep-coding-instructions: true removes Claude Code'"'"'s");
  console.log("software-engineering instructions from the system prompt with no error of any kind.");
  console.log("Fix the file by hand, or delete it and re-run the installer to reseed (it never overwrites your copy).");
}
'
```

Catches the register's one silent-failure mode (`act:9a0af01a`). Two checks
that no file-content test can make: (1) the running Claude Code is at least
**v2.0.37**, the version that added `keep-coding-instructions` — below it the
field is ignored, its `false` default applies, and CC's software-engineering
instructions are stripped with **no error**; and (2) the **installed** copy in
`~/.claude/output-styles/` is valid, since that is the file Claude Code reads
(the template can be perfect while the installed copy is broken). Mirrors CC's
own strict `=== true` coercion, so YAML-truthy spellings (`yes`, `on`, `1`)
are caught rather than trusted. A **warning, not a gate** (exit 0): this is
user-level machine state, not repo structure, so failing `/validate` would
gate a worktree merge on a condition unrelated to its diff. Reports positive
confirmation when healthy. Skips silently when the module isn't installed.

## Example Validators (commented — enable for your project)

<!--
### Type Check
```bash
cd your-app-dir && npx tsc --noEmit
```
Catches type errors before they reach production.

### Lint
```bash
cd your-app-dir && npx eslint src/
```
Catches style violations and common code quality issues.

### Production Build
```bash
cd your-app-dir && npx vite build
```
Catches what the type checker misses: bare catch blocks, runtime-only
errors, bundle resolution failures. A type check pass + build fail =
broken deploy.

### Structural Validation
```bash
./scripts/validate-structure.sh
```
Project-specific structural checks (e.g., required files exist,
cross-references are valid, configuration is consistent).

### Memory/Docs Validation
```bash
./scripts/validate-docs.sh
```
Checks that documentation references (memory index, CLAUDE.md links)
point to files that actually exist.
-->
