Run a diff-scoped code audit for architecture, dead code, and test quality. Uses the change from main to focus feature reviews; request a repository audit for whole-codebase discovery.
يبقى الأمر في سطر واحد. مرّر أفقيًا لمراجعته كاملًا قبل النسخ.
تفضّل نسخة محلية؟ نزّل الملفات المتاحة حاليًا لدى SkillsMP.
عرض SKILL.md
SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
audit
description
Run a diff-scoped code audit for architecture, dead code, and test quality. Uses the change from main to focus feature reviews; request a repository audit for whole-codebase discovery.
allowed-tools
*
Audit
Run a diff-scoped code audit. Execute checks and report results by severity.
Reviewer class:class-2 — independent observation: every check confirms an observable fact, so no cross-model reviewer applies. Judging whether the architecture is sound is not audit's job — that lives in the Architecture Review Gate (ARCHITECTURE.md) and quality-review.
Invocation log
This skill is required before marking a feature ticket done. The line below appends a current-run entry to skill-invocations.log under the project namespace root (.project/, or legacy .safeword-project/ where that exists) so the done-gate hook can verify /audit was actually invoked. Claude Code expands the ! line automatically and passes ${CLAUDE_SESSION_ID} when available. The helper also resolves Claude remote-container ids from the runtime environment, and on Cursor and Codex the pre-shell hook (beforeShellExecution / PreToolUse) bridges the session id to the helper — so on all three runtimes the fallback runs without hand-picking an id. Hand-writing audit results cannot produce this feature-gate proof.
If the automatic line or fallback prints [skill-invocation-log] FAILED, prints no run identity, or still does not print audit ✓: a feature ticket can't be marked done without this proof — don't hand-write audit results as a substitute. Report the failure to the user (most likely cause: inline shell execution was denied, the runtime did not expose a usable run identity, or Bun could not run the installed helper) and ask them to resolve it before re-invoking /audit.
For task, patch, or no-ticket work, this proof isn't required — note it's missing and continue.
Scope
Default to the current working tree's change from origin/main, falling back to
local main. The shared scope helper prints its exact merge-base SHA and changed
files. Treat that printed list as the audit boundary for every review below.
Review changed source, tests, agent configuration, documentation, and learning
files; follow direct references from them when a missing reference could make
the change invalid.
Deleted and type-changed paths are evidence for broken-reference review,
never analyzer inputs. The helper prints them under Reference review scope.
Whole-workspace Knip, repository clone totals, and dependency-freshness
discovery are intentionally skipped in this mode because their pre-existing
findings are noise for a feature diff.
Run a repository audit only when the user explicitly asks for a full,
repository-wide, or baseline audit, or when neither origin/main nor main
exists. In that mode retain the prior whole-project checks and report the mode
prominently. Do not silently widen a Git-aware diff audit.
For an explicit repository audit, set AUDIT_SCOPE_REQUEST=repository in the
environment of every executable audit block below. That is what widens code,
agent configuration, learnings, tests, documentation, and domain docs together.
Leave the variable unset for the default diff audit; do not edit the blocks'
commands.
Instructions
1. Code Quality Checks
Run the block below verbatim, as ONE bash invocation. Do not extract or paraphrase individual commands — the manifest gates, package-manager routing, and tool-absence messages are load-bearing, and a hand-rolled subset silently skips whole check families.
Check the knip output above for "Configuration hints" lines. If knip reports configuration hints (unused entries in ignoreDependencies, ignoreBinaries, ignoreUnresolved, or ignoreWorkspaces), flag each as:
- [W005] Stale config: `knip.json` — `{entry}` can be removed from {list}
These mean the ignore override no longer matches anything knip would flag — the suppression is dead config. Cleaning them up reduces noise for future readers.
If no configuration hints are found, skip this section.
Findings triage — baselines, not re-litigation
Knip:knip.json's ignore lists ARE the accepted-false-positive baseline — persist confirmed FPs there instead of re-triaging them every run (W005 flags any entry that goes stale, so the baseline self-cleans). Report only findings not already covered by the ignore lists.
jscpd: record the clone count in the audit summary with its scope named next to the count — e.g. Clones: 416 (8.9%) [repo minus .safeword,.project] — and compare against the previous audit's recorded count at the SAME scope (last verify.md/audit record, if any). A count whose scope differs from the prior record is a new baseline, not a delta (issue #825: unscoped counts spanning 84→594 proved incomparable). Deltas are the findings; a flat count is the baseline, not a finding. Never report a raw total as if it were new.
2. Agent Config Checks
In a diff audit, find and check changed agent configuration files (excluding
.safeword/) and direct references from them. In a repository audit, check
every matching configuration file.
Files to check:
CLAUDE.md, AGENTS.md (root and subdirectories)
.claude/CLAUDE.md (root and subdirectories)
.cursor/rules/*.mdc or .cursor/rules/*/ (root and subdirectories)
All referenced files/paths exist (skip URLs starting with http)
error
Staleness
Do not report date-only staleness in a diff audit; use a repository audit
n/a
3. Learning Files Check
Changed project learnings in the resolved namespace root's learnings/*.md must have a Covers: line on line 3 — the auto-generated INDEX.md is built from these lines, and files without them don't appear in the index. In a repository audit, check every learning as before.
Review changed test files for quality issues, plus a changed source file's
co-located test when present. Check them against the iron laws and anti-patterns
in .claude/skills/testing/SKILL.md. A repository audit may use the former
project-wide sample.
Find test files:
# Start with test files named in the printed audit scope. For a changed# `src/foo.ts`, also inspect `src/foo.test.ts` / `src/foo.spec.ts` when present.# Repository audit fallback (common patterns):
find . -name "*.test.*" -o -name "*.spec.*" -o -name "*_test.*" | grep -v node_modules | grep -v dist | head -20
For each sampled test file, check:
The criteria are language-neutral; the parenthetical idioms are examples — map them to the project's test framework (Jest/Vitest, pytest, Go testing, Rust #[test], …).
Check
Criteria
Severity
Meaningful assertions
Every test asserts specific values/behavior — not bare existence/truthiness/no-error checks (toBeTruthy, bare assert result, only err == nil, assert!(x.is_ok()))
error
Behavior over implementation
Tests assert observable outcomes, not internal state or mock call args
error
Independence
No test depends on another test's side effects; fresh state per test
error
No arbitrary timeouts
No sleeps or hardcoded delays (sleep, waitForTimeout, time.Sleep, thread::sleep)
error
Edge case coverage
Tests include error paths and boundary cases, not just happy path
error
No duplicate tests
Similar tests use parameterized/table-driven patterns (it.each, pytest.mark.parametrize, Go table-driven subtests, rstest)
error
Test naming
Names describe behavior, not implementation ("returns 401 when..." not "works correctly")
error
Report format:
Test Quality:
- Files reviewed: N
- Issues found: N (E errors)
- [E] file.test.ts:42 — Weak assertion: `expect(result).toBeTruthy()` → assert specific value
- [E] file.test.ts:15 — Shared mutable state: `user` modified across tests
- [E] file.test.ts — Happy-path only: no error case tests for `processOrder()`
5. Project Documentation Checks
Docs source inventory:
Read .safeword/config.json first. If top-level docs.sources exists, treat it as the authoritative documentation inventory:
{ "type": "local", "path": "..." } — inspect that file or directory. Relative paths resolve from the project root.
{ "type": "url", "url": "..." } — fetch the page/site when browsing or network access is available. If unavailable, report it under coverage limitations.
{ "type": "git", "repo": "...", "path": "..." } — inspect the repo/path when it is already available or can be fetched without credentials. If unavailable, report it under coverage limitations.
If docs.sources is absent, prompt the user: "Where should audit look for project documentation? I can add local paths, URLs, git repos, or set docs.sources: [] to keep fallback discovery and stop asking." Wait for the answer before continuing unless the run is explicitly autonomous; in autonomous runs, use fallback discovery and report that no decision was recorded.
If the user chooses not to configure documentation sources, write docs.sources: [] in .safeword/config.json. Treat that explicit empty list as a durable no-prompt decision in future audits.
If docs.sources: [] is configured, do not prompt. Fall back to local discovery: README.md, docs/, documentation/, package docs folders, and known docs-site configs.
Always report docs coverage: configured vs fallback, sources checked, and sources skipped. In a diff audit, inspect sources directly affected by changed code or changed docs. In a repository audit, inspect the entire configured or fallback source inventory; date-only staleness and a last-20-commits sweep belong there too.
ARCHITECTURE.md (the architecture narrative):
Resolve the narrative location first: the paths.architecture target in .safeword/config.json when set — a file is the narrative itself; a directory holds decision records, read them all — else the root ARCHITECTURE.md. A configured location wins outright: do not fall back to a root file the host deliberately moved away from. Every check below applies to the resolved narrative.
If missing → create from .safeword/templates/architecture-template.md (at the configured location when paths.architecture is set, else root ARCHITECTURE.md)
If exists → check for drift and gaps along TWO axes — dependency drift (what tech) and structural drift (what modules/layers):
Dependency drift:
Drift (error): Documented tech contradicts the code's actual dependencies (e.g., doc says "Redux" but package.json has "zustand"; doc says "Flask" but pyproject.toml has "fastapi")
Gap (error): Major dependencies not documented
Structural drift — reconcile ARCHITECTURE.md's STRUCTURAL claims against architecture.generated.md, the deterministic, always-fresh module/package map (kept current by the architecture hooks). Read the generated doc as ground truth — NOT package.json:
Read the namespace-root architecture.generated.md (resolve the namespace root the same way as other audit checks; default .project/). Its ### <name> headings under ## Modules (single-repo) or ## Packages (monorepo) ARE the project's real top-level units. This machine list is the source of structural truth, so the verdict is deterministic-by-reading, not guessed.
Orphaned (error): ARCHITECTURE.md documents a module/layer — including a layer→directory mapping in its "Layers & Boundaries" table — that no longer appears in the generated map (renamed or removed).
Drifted layer→dir (error): A "Layers & Boundaries" directory entry that matches no module path in the generated map.
Inventory omissions are not findings: The generated document owns the structural inventory. Never require the human narrative or decision records to mention every generated module/package; those documents own the architectural "why," not a duplicate package-by-package list.
Report only — never auto-overwrite prose. Cite the generated-doc evidence and propose narrative edits for the user to review; the human "why" is human-owned, and only a person can judge whether a paragraph is still true. The deterministic structural facts come from reading the generated doc; the narrative judgment stays with the human/agent.
README.md:
Check changed claims and impacted references. Check date-only staleness only in a repository audit.
Docs site (if changed or directly impacted):
Detect docs/, documentation/ with Starlight/Docusaurus/etc config
Check staleness of docs content
Documentation impact check:
Review the changed area from the printed scope. For each significantly changed area, check if related docs, readmes, or guides need updating. Flag stale, missing, or contradictory impacted documentation as errors. Documentation drift is never a warning; date-only staleness with no changed-code contradiction is repository-audit context, not a diff finding.
6. Principle Trace Integrity
For the active ticket, when impl-plan.md declares project-principle alignment,
resolve the source using paths.principles (default
<namespace-root>/principles.md) and check the principle trace as observable
facts only:
The named principle exists in the configured source.
The trace contains a non-empty concrete consequence and proof.
The proof reference resolves to recorded test, verification, or manual
evidence; an intentional conflict is named in Known deviations.
Report a missing source entry, incomplete mapping, dead evidence reference, or
unrecorded conflict as [E010] Broken principle trace. Do not judge whether a
principle was applicable, whether the consequence was a wise interpretation,
or whether an experience was genuinely delightful—those are adversarial
quality-review judgments. A plan with no declared applicable principle is not
an audit finding.
Run the factual checker below verbatim. Its sentinel keeps the executable audit
contract testable without turning semantic review into shell heuristics.
When a changed feature/spec or changed domain doc references them, reconcile the
three namespace domain docs — personas.md, surfaces.md, glossary.md —
against those changed references and report empty scaffolds. A repository audit
reconciles the whole corpus. This check is read-only and class-2 (observable
facts only): it reports and offers, it never rewrites a doc. Run the block
below verbatim, as ONE bash invocation.
Content is human-owned — advisory only, never an error. This check judges references (a slug/code that is or isn't defined) and emptiness — observable facts. Whether a glossary term's meaning, or a persona/surface description, is still accurate is a human judgment: raise it as an advisory note at most, never as an error code. Only the three codes above (E008, E009, W008) are emitted here.
Empty-doc offer (W008): report the empty doc and point the user to its template — do not draft entries or write the file during the audit pass (read-only). Filling it is a follow-up the user approves.
Coverage limitation: configured paths.personas, paths.surfaces, and paths.glossary are resolved before reconciliation. safeword doctor separately reports missing configured files and orphaned defaults. If the safeword feature-directory resolver is unavailable, W009 says E008 fell back to root features/ only. Persona drift reads spec **Persona:** lines only — feature lineage tags are not a reliable persona source.
Report Format
Report findings by severity with codes:
Errors (must fix)
[E001] Dead ref: CLAUDE.md references missing file src/foo.ts
[W003] Staleness: README.md last modified 45 days ago (12 commits since)
[W005] Stale config: knip.json — lodash can be removed from ignoreDependencies
[W006] Learning file missing Covers: — <namespace-root>/learnings/foo.md (absent from INDEX.md)
[W007] Stale .safeword/depcruise-config.cjs — run safeword project sync-config to refresh and commit
[W008] Empty domain doc: surfaces.md has no uncommented entries — fill from its template (BDD intake references degrade until filled)
[W009] Feature-directory resolver unavailable — E008 scanned root features/ only
Code Quality
Architecture:
Circular dependencies: [None / show cycle path]
Layer violations: [None / show invalid import]
Dead Code:
Knip findings: [list unused items to review — verify before removing, knip cannot see packages consumed via Astro/Vite/Wrangler config]
Duplication:
Clone count: X (Y% of codebase; delta vs previous audit: +N/-N/flat)
Outdated Packages: the table + per-tier verdict from "Outdated Package Triage" above (or ✅ All packages up to date).
Test Quality:
Files reviewed: N
Issues: [None / list by severity]
Summary
Errors: N | Warnings: N | Passed: N
[Audit passed | Audit passed with warnings | Audit failed]
**Next:** [imperative — which fix to start, which package to upgrade, which file to update].
Close with the **Next:** line even on a clean pass — name the immediate move (commit, mark ticket done, open a follow-up for warnings) so the reader isn't left guessing which finding to start with (the stop hook reads it for the re-entry brief).
Voice: plainspoken and concise — write to be scanned.
$AUDIT_HAS_PYTHON_CHANGE
true
"$AUDIT_HAS_GO_CHANGE"
true
"$AUDIT_HAS_RUST_CHANGE"
true
then
true
fi
# Stack-specific checks are gated by project manifests. A package.json may be a
# safeword lane host in Python, Rust, or Go installs, so JavaScript checks run
# from package.json evidence while native stack checks run independently.
# JavaScript-specific checks still run only when package.json exists; skip
# JavaScript checks for projects without package.json evidence.
# Detect package manager from lockfiles/packageManager for JavaScript package commands.
"Manual evidence required: yarn.lock found but yarn is unavailable; cannot check outdated JavaScript dependencies."
echo
"Yarn modern detected. Manual evidence required: modern Yarn does not provide the Yarn Classic noninteractive 'yarn outdated' command; review dependency freshness with 'yarn upgrade-interactive' or project CI evidence."
esac
run_python_outdated_check
"$1"
cd
"$project_dir"
exit
if
then
true
elif
'^\[tool\.poetry\]'
then
true
elif
then
true
else
true
fi
if
"$AUDIT_HAS_CODE_OR_MANIFEST_CHANGE"
true
then
echo
"Code quality scope: no changed source or manifest files"
else
"$PYTHON_PROJECT_DIRS"
echo
"No Python projects found — Python architecture, dead-code, and outdated checks not applicable"
"$GO_MODULE_DIRS"
echo
"No Go modules found — Go architecture, dead-code, and outdated checks not applicable"
"$RUST_CRATE_DIRS"
echo
"No Rust crates found — Rust architecture, dead-code, and outdated checks not applicable"
# 1b. Architecture - Python (import-linter). Python does NOT reliably catch cycles
# at runtime — an ImportError fires only when the import order happens to touch a
# not-yet-defined name, so a passing test run is NOT proof of an acyclic import
# graph. import-linter is the static gate, but it is config-driven (it enforces only
# declared contracts, nothing by default), so gate on its config and never force it.
if
"$PYTHON_PROJECT_DIRS"
then
while
read
do
"$project_dir"
continue
cd
"$project_dir"
exit
if
'^\[importlinter\]'
'^\[tool\.importlinter\]'
then
if
command
then
true
else
echo
"Manual evidence required: import-linter contracts found in $project_dir but 'lint-imports' not installed — Python architecture check skipped"
fi
else
echo
"Manual evidence required: no import-linter contracts for $project_dir (.importlinter / [tool.importlinter] / setup.cfg [importlinter]) — Python import cycles are NOT statically checked (runtime does not reliably catch them). Add import-linter, or run 'pylint --disable=all --enable=cyclic-import <pkg>' for a config-free heuristic."
fi
done
EOF
$PYTHON_PROJECT_DIRS
EOF
fi
# 1c. Architecture - Go. The compiler REJECTS import cycles at build, so a green
# `go build ./...` / `go test ./...` already guarantees an acyclic package graph —
# no separate cycle check exists or is needed. Layer/boundary rules are enforced by
# depguard, which runs INSIDE the golangci-lint pass below when `.golangci.yml`
# configures it — do NOT force-enable it (an unconfigured depguard flags every
# non-stdlib import as a false positive).
if
"$GO_MODULE_DIRS"
then
while
read
do
"$module_dir"
echo
"Go architecture — $module_dir: import cycles are compiler-guaranteed absent (a passing build proves it); boundary contracts run via depguard in the golangci-lint pass when .golangci.yml configures them."
# mutually-recursive modules, so a compiling project cannot contain cycles — no
# check needed. No mature standard tool enforces directional layer boundaries in
# Rust (cargo-modules only visualizes); teams enforce boundaries structurally via
# separate crates + visibility. (cargo-deny covers dependency supply-chain —
# advisories/licenses/bans — a different axis, not architecture.)
if
"$RUST_CRATE_DIRS"
then
while
read
do
"$crate_dir"
echo
"Rust architecture — $crate_dir: crate/module cycles are compiler-guaranteed absent (a passing build proves it); no standard layer-boundary tool exists — enforce structurally via crates."
"Dependency freshness: skipped in diff scope — run a repository audit for upgrade discovery"
fi
fi
"[W006] Missing Covers: line on line 3 — $f"
esac
done
fi
A monorepo ## Coverage gaps advisory in the generated doc (a present-but-unparseable workspace manager, #558) is itself a coverage limitation — note it so the structural reconciliation isn't mistaken for complete.
");
const validCode = code => /^[A-Z][A-Z0-9]{1,5}$/.test(code);
const canonicalCode = code => /^[A-Z][A-Z0-9]{2,3}$/.test(code);
const skip = [];
let isInsideCodeFence = false;
let isInsideComment = false;
for (const line of lines) {
if (line.trimStart().startsWith("
")) {
skip.push(true);
isInsideCodeFence = !isInsideCodeFence;
continue;
}
if (isInsideCodeFence) {
skip.push(true);
continue;
}
if (!isInsideComment && line.trimStart().startsWith("
")) isInsideComment = true;
if (isInsideComment) {
skip.push(true);
if (line.includes("
")) isInsideComment = false;
continue;
}
skip.push(false);
}
function stripInlineComments(line) {
let result = "
";
let position = 0;
while (position < line.length) {
const open = line.indexOf("
", position);
if (open === -1) return result + line.slice(position);
result += line.slice(position, open);
const close = line.indexOf("
", open + 4);
if (close === -1) return result + line.slice(open);
position = close + 3;
}
return result;
}
function parseHeading(line) {
const body = stripInlineComments(line.slice(3)).trimEnd();
if (body.endsWith("
")).map(parseHeading);
const claimed = new Set(parsed.filter(persona => persona.explicit && persona.rawCode.length > 0).map(persona => persona.rawCode));
const resolved = parsed.map(persona => {
if (persona.explicit) return { ...persona, code: persona.rawCode, codeError: false };
const allocation = allocateCanonical(deriveCanonical(persona.name), claimed);
if (!allocation.exhausted) claimed.add(allocation.code);
return { ...persona, code: allocation.code, codeError: allocation.exhausted || !canonicalCode(allocation.code) };
});
const aliases = [];
for (const persona of resolved) {
if (persona.codeError) continue;
const base = deriveLegacy(persona.name);
if (base === "
" || base === persona.code) continue;
let candidate = base;
for (let suffix = 2; claimed.has(candidate); suffix += 1) candidate = `${base}${suffix}`;
if (!validCode(candidate)) continue;
claimed.add(candidate);
aliases.push(candidate);
}
for (const persona of resolved) if (!persona.codeError) console.log(persona.code);
for (const alias of aliases) console.log(alias);
')"
# Referenced codes: (CODE) from spec **Persona:** lines, comments stripped.