name: audit-project
description: Audit the current project's workflow infrastructure (git/branch hygiene, beads init, bd hooks, workflow.json, MemPalace wing, CLAUDE.md, .claude/rules/, .claude/agents/+commands/, bd memories) and — for projects that already have a Diataxis docs substrate — the docs/system/beads/MemPalace alignment of the project's documentation. Drives the project-onboarder subagent, presents the structured checklist to the user, and offers interactive template-based fixes per gap. Manual-only — never auto-suggested by session-startup or any activity recipe; only fires when the user invokes /audit-project.
Audit-Project — Project Onboarding + Drift-Detection Skill
This skill is the driver behind the /audit-project slash command.
It coordinates two responsibilities, run sequentially in one session:
Onboarding scan — dispatch the
project-onboardersubagent to scan workflow-infrastructure setup (git hygiene, beads, hooks,workflow.json, MemPalace wing,CLAUDE.md, rules dir,bd memories) and return aPASS/WARN/MISSchecklist. This is the v1 behavior shipped 2026-05-03.Docs drift detection — for projects with a Diataxis docs substrate, compare
docs/against the system (filesystem primitives), beads (bd show), and MemPalace (mempalace_*) and report doc-vs-reality drift. This is the v2 behavior gated by--check=docs(default-on when the substrate is present).Note: "Diataxis substrate" (the docs-check gate) is a different condition from "loom-managed" (the docs-scaffold and onboarding gate). Loom-managed =
.claude/workflow.jsonpresent. Diataxis substrate =.beads/present ANDdocs/already has at least one Diataxis quadrant. A project is typically loom-managed first, then gains a Diataxis substrate after running/docs-scaffold. Don't conflate the two terms.
The two phases produce a single combined report. The user approves fixes per item — nothing is auto-applied unless the user asks.
The discipline this skill codifies, restated for the docs check:
when docs disagree with system / beads / MemPalace, docs lose.
The check reports doc fixes — never the other way around. A doc
that says "all six commands have X" when only five do is the doc's
problem; a doc that cites a bead-ID that no longer resolves is the
doc's problem. The system / beads / palace are the sources of
truth; docs/ is the surface.
Invocation: explicit only. /audit-project (with optional flags)
fires this skill. The slash command and this skill both carry
disable-model-invocation: true in spirit — the user has to ask;
session-startup and the activity recipes never auto-suggest the
audit. This is a deliberately user-pulled workflow.
When to use
- The user types
/audit-project(with or without flags). - The user asks to "audit this project" / "check docs drift" / "see what's missing for the workflow."
- A new project just got
bd init-ed and you want a sanity check on what's wired up. - You suspect docs have drifted from reality (cardinality claims, dead bead-IDs, primitives that changed shape) and want a systematic sweep instead of ad-hoc grepping.
Skip when
- Mid-task in another bead. The audit is a session-spanning ceremony; don't interleave it with claimed work.
- The project is not a beads workspace and not a loom-managed
project. The skill produces empty output for non-loom projects
with no
.beads/. - The user wants to verify a single fact ("does X exist?"). Just check it directly; don't run the full audit.
Flags
--check=onboarding— run only the project-onboarder dispatch. Equivalent to v1 behavior.--check=docs— run only the docs drift detection. Useful when the project is already onboarded and you only want the doc-vs-reality sweep.--check=all(default when the project has a Diataxis substrate; see Step 1) — run both.--check=tree-sitter— run ONLY the tree-sitter grammar check (Step 8 below): scan the project tree for any directory containing agrammar.js, and for each WARN when no siblingtree-sitter.jsonexists (tree-sitter 0.25+ ABI-15 compatibility). Runs neither the onboarding scan nor the docs check. This check is ALSO folded into the onboarding scan (project-onboarder item 22) so it surfaces on a default--check=onboarding|allrun too; the dedicated flag exists for grammar-heavy projects that want the check in isolation. (loom-qvs.)--check=constitution— run ONLY the project-constitution capture flow (Step 7 below): detect the project's tooling fingerprint, render draft front-matter, confirm each field with the user one field at a time (never lump-sum, per loom-xcw), write.claude/project-constitution.mdUNSTAGED, mirror to the<wing>/decisionsMemPalace drawer, and emit KG triples for the tooling. The prose body is emitted as a[HUMAN AUTHOR]TODO stub — never agent-authored (loom-d50). On re-run, detection is diffed against the captured file and per-field drift is surfaced without overwriting the prose body. This mode runs neither the onboarding scan nor the docs check — it is the loom-6f8 Constitution epic's capture half (loom-1iz). The schema, dogfooded sample, and field reference were shipped by loom-vin (references/project-constitution.schema.json,templates/project-constitution.md,docs/reference/project-constitution.md).--apply-trivial— auto-apply doc-drift items the skill has tagged[DOC FIX][TRIVIAL]: cardinality count corrections (the loom-469 class — single-numeral substitution at a known file:line) and dead bead-IDs whosebd showreturns a uniquesuperseded-byID. Ambiguous items (factual claims, behavior descriptions, fuzzy drawer-citation matches) are NEVER tagged TRIVIAL and remain in the per-item approval queue. See "Step 3.5 — apply tagged items" below for the full apply procedure. (loom-8hg.)--apply-onboarding— auto-apply onboarding-checklist items theproject-onboardersubagent has tagged[AUTOFIX:<recipe-id>]on the suggested-fix line. Recognised recipes (loom-a29):[AUTOFIX:bd-hooks](item 3 MISS) — runsbd hooks installthengit add .beads/issues.jsonl && git commit -m "bd: post-install export sync"(the loom-cka two-step absorbing commit).[AUTOFIX:workflow-json](item 4 MISS) — writes{"v":1,"mode":"full"}to<root>/.claude/workflow.json. Mode is a real choice;fullis the documented default. Override with--workflow-mode=light|offto change the value the flag writes; users who later want a different mode can edit the file directly.[AUTOFIX:gitignore-worktrees](item 11 INFO) — appends BOTH.claude/worktrees/and.claude/workflow-state.jsonto<root>/.gitignoreif not already present. Idempotent per line. Both are per-session loom ephemera that show up at the root of every loom-managed project; folded into one recipe by loom-tat after both customer trials (loom-b6o, loom-wxo) handled the workflow-state.json line manually.[AUTOFIX:loom-env-block](item 16 WARN/MISS) — deep-merges the canonical loom env block (CLAUDE_CODE_ENABLE_TASKS=false,CLAUDE_CODE_DISABLE_AUTO_MEMORY=1) into<root>/.claude/settings.json, overwriting only those two keys and preserving every other key. Writes.claude/settings.json.pre-loom.bakon first overwrite. Idempotent — re-running against a canonical file is a no-op that does not touch the backup. Counters the harness's competing TaskCreate / MEMORY.md defaults on the per-project layer (loom-7ro).[AUTOFIX:loom-upstream-gc-handoff](item 17 INFO) — handoff recipe for orphan clones under~/.loom/upstream/<owner>/<repo>/with no matching openupstream:watchbead. The recipe does NOT prune directly — actual removal is per-clone y/N gated inside/loom-upstream-gc. The recipe prints the handoff messageorphan clones detected; run /loom-upstream-gc to review and prune interactivelyand marks the row as queued- for-user. The handoff tag exists so--apply-onboardingvisibly resolves the row rather than silently leaving it in the per-item queue (loom-k2g).[AUTOFIX:gh-auth-prompt](item 18 WARN) — handoff recipe for unauthenticatedgh(gh auth statusnon-zero exit). The recipe does NOT attempt the OAuth flow —gh auth loginis interactive and cannot run inside the audit. The recipe printsgh is not authenticated; run \gh auth login` interactively to fix` and marks the row as queued-for-user. Same handoff-tag rationale as item 17 (loom-k2g).[AUTOFIX:dedup-hook-skip-worktree](item 12 WARN — the DEFAULT offer for a duplicate hook) — when item 12 reports a project-tracked SessionStart/PreToolUse hook that is ALSO registered by the plugin or user-global layer, this recipe resolves the duplicate per-user and reversibly:git update-index --skip-worktree <root>/.claude/settings.json(so the local strip is untracked — git stops watching the file for changes, and a future upstream pull no longer errors with "would be overwritten by checkout"),- strip the duplicate
(event, matcher, command)stanza from the local copy of.claude/settings.json, - log the recovery snippet (below) to
<root>/.claude/loom-audit-state.jsonunder thededup-hook-skip-worktreekey, for the inevitable next upstream change to the tracked file. Recovery snippet (baked into the AUTOFIX log so the next pull is not a surprise):
git update-index --no-skip-worktree .claude/settings.json git stash git pull git stash pop # then re-apply skip-worktree + strip via /audit-project --apply-onboardingThis is the default because it is per-user and reversible: it never changes shared content, so it cannot break a non-loom dev's setup. The detection mechanism (
find-hook-dups.sh) is unchanged — this recipe only consumes its WARN output (loom-jnn).[AUTOFIX:dedup-hook-commit](item 12 WARN — gated behind an explicit y/N confirmation) — the same detection, the opposite resolution: remove the duplicate hook stanza from the tracked.claude/settings.jsonand commit. Because this changes shared content, the binaryapplyshape does NOT fit — the recipe plumbs a confirmation prompt through and is NOT auto-applied on--apply-onboarding. The prompt names the consequence verbatim:This commits a change to .claude/settings.json that assumes loom adoption for all devs on this repo. Non-loom devs lose <hook-name> registration. Proceed? (y/N). Only on a typedydoes it commit with subjectaudit: dedup <hook-name> SessionStart hook (loom-managed; plugin + user-global handle registration). The empirical reason a resolution path is needed at all: hook layering is additive across all four layers (plugin + user-global + project-tracked + project-local) — empty arrays insettings.local.jsondo NOT cancel an inherited registration, they only add zero entries to the union. Verified inert in e2e-api-tests 2026-05-27 (bd prime still fired 3 times in a fresh SessionStart after the override). Seedocs/reference/claude-code-hook-layering.mdfor the full finding (loom-jnn). Items NOT tagged AUTOFIX (item 2bd init, item 5 MemPalace wing creation, item 6 CLAUDE.md authoring, item 7.claude/rules/content) remain in the per-item approval queue..claude/rules/CONTENT is HARD-EXCLUDED from every auto-apply path (loom-d50). The audit may SCAFFOLD an empty[HUMAN AUTHOR]stub rules file or SUGGEST one, but it NEVER auto-drafts or auto-applies authored rule content — rule text encodes project conventions, which a human authors (same class as the constitution prose body, Step 7d). See the.claude/rules/content-exclusion bullet in Step 3.5 for the scaffold-stub shape. The flag never touches WARN items (those imply real conflict — dirty tree, malformed workflow.json, etc. — and need human triage). Items 17 and 18 are exceptions to the WARN-untouched rule because they resolve by handoff (not by in-process write); the handoff message itself IS the resolution.
--workflow-mode=full|light|off— only meaningful with--apply-onboarding. Sets themodevalue the[AUTOFIX:workflow-json]recipe writes. Defaultfull.--root <path>— project root to audit (default: current working directory's git root, or cwd if not in a git repo). All filesystem globs,bdlookups, andgitcommands resolve against this root. Lets the skill run against any loom-managed project, not just loom itself.--wing <name>— MemPalace wing to use for drawer-slug resolution in Check 5 (and any other palace-citation checks). Default: the basename of--rootused verbatim (no case-folding, no_↔-substitution — the palace's de-facto convention follows filesystem naming, soliza_basefilesystem → wingliza_base,hundred_acre_woods→ winghundred_acre_woods). Fallback:loomonly if the auto-detect basename is itselfloom(preserves the pre-portability behavior for loom's own audit). The wing-name flag exists for projects whose directory basename doesn't match their MemPalace wing slug (e.g., a checkout namedliza_livewhose wing isliza). Step 1b's wing-variant WARN catches the remaining divergence cases (capitalization that doesn't match, separator flip relative to a larger sibling wing).--mine-history— after the audit report is presented, delegate to the/loom-mine-historyskill to mine the project's git/PR history for unmined decisions (drawers + KG triples), behind its own mandatory two-pass cost gate. Runs against the resolved--root/--wing. WITHOUT this flag the audit only flags the gap informationally — theproject-onboarderdecision-history line reports the unmined-unit count, and the audit never auto-mines (mining is an explicit, billable action the user opts into). See "Step 6 — optional history mining" below.
If no flag is given, the default is --check=all for projects with
a Diataxis substrate (heuristic: .beads/ exists AND docs/
contains at least one Diataxis quadrant directory — tutorials/,
how-to/, reference/, or explanation/). For other projects
the default is --check=onboarding to preserve v1 behavior.
This gate condition is named has-diataxis-substrate throughout the
sequence below; it is not the same as "loom-managed" (which is
.claude/workflow.json present, the gate /docs-scaffold and the
project-onboarder use).
The Sequence
Step 1 — resolve project root + flags + wing
Run the resolution helper first. Invoke
~/.claude/scripts/loom-audit-resolve [--root <path>] [--wing <name>] (passing
through whatever --root/--wing the user gave) and read its
key=value stdout:
root=<abs path> # resolved per the precedence below
wing=<name> # basename verbatim, or explicit --wing
primitives=<csv> # which of skills,commands,agents,hooks exist
diataxis_optout=<0|1> # <root>/docs/.no-diataxis present
loom_managed=<0|1> # .beads/ AND a docs Diataxis quadrant
This helper computes the deterministic resolution prelude
(unit-tested at lib/tests/loom-audit-resolve.test.sh), so the rules
below are documentation of what it does — do not re-derive them by
hand; consume the helper's output. In particular the wing default is
the basename verbatim (no _↔- substitution, no case-folding),
the only rule correct for both underscore wings (liza_base) and dash
wings (golden-path); Step 1b's variant-WARN backs this up for the
divergence cases.
For reference, the precedence the helper implements:
Resolve the project root in this precedence order:
- Explicit
--root <path>flag (absolute or relative; resolved to absolute). - Current working directory's git root (
git -C $PWD rev-parse --show-toplevel). - Current working directory itself (fallback when not in a git repo).
Parse the rest of the flags. Decide whether the docs check runs
(has-diataxis-substrate heuristic below, or explicit
--check=docs|all).
Resolve the project's MemPalace wing in this precedence order:
- Explicit
--wing <name>flag. - Basename of the resolved root, used verbatim — no case-folding,
no
_↔-substitution (e.g., a root at/home/frank/repos/loom→ wingloom; a root at/home/frank/repos/hundred_acre_woods→ winghundred_acre_woods; a root at/home/frank/repos/liza_base→ wingliza_base). The palace's de-facto wing convention follows filesystem naming, so the verbatim basename is the right default. Step 1b's variant WARN handles the cases where the filesystem name genuinely diverges from the canonical wing slug. - The literal
loomonly when step 2 already producesloom— this is the no-flag, loom-itself path and preserves v1 behavior.
Detect the project's primitive directories from the filesystem (used by Checks 3 and 4). Probe each of these under the resolved root; record which exist:
skills/(eachskills/*/SKILL.mdis a primitive)commands/(eachcommands/*.mdis a primitive)agents/(eachagents/*.mdis a primitive)hooks/(eachhooks/*.shis a primitive)
Do NOT hardcode the loom set. Projects that follow loom's primitive shape will have all four; projects that adopted only a subset (or that use additional primitive types) drive what the checks compare against. Checks 3 and 4 silently skip a primitive class whose directory doesn't exist.
Detect has-diataxis-substrate by checking <root> for: .beads/
present AND docs/ containing at least one of tutorials/,
how-to/, reference/, explanation/. If both conditions hold,
the docs check defaults on; otherwise it defaults off. (This is
distinct from "loom-managed", which is .claude/workflow.json
present — the gate used by /docs-scaffold and the
project-onboarder. The two gates can be true independently.)
Detect the Diataxis opt-out: if <root>/docs/.no-diataxis exists,
record opt_out_diataxis = true. Check 4 (inclusion-glob coverage,
which assumes Diataxis-shaped docs/reference/<thing>/ catalog
pages) is skipped under opt-out. Checks 1, 2, 3, and 5 still run
if <root>/docs/ exists at all — those checks are about
docs-vs-reality drift in whatever shape the docs take, not about
Diataxis layout.
Detect whether <root>/docs/ is generated by running the shared
detector at lib/docs-generated.sh (loom repo, sourced from this
loom checkout):
bash <loom-checkout>/lib/docs-generated.sh "<root>"
The detector exits 0 when docs/ is generated (gitignored, or
written by a build script — see the helper for the full signal
list) and prints a one-line reason on stdout. Record the result as
docs_generated = true|false along with the reason.
When docs_generated = true:
All sub-checks in Step 3 that would scan files under
<root>/docs/(Checks 1, 2, 3, 5) are skipped for paths under<root>/docs/but still run against root-level docs files in scope (per loom-ojn:README.md,README.rst,README.txt).Check 4 (inclusion-glob coverage) is skipped entirely — the generated catalog page is not the source-of-truth.
Step 3 emits one line per skipped class:
[DOC SKIP][GENERATED] docs/ is generated — skipping <check-name> reason: <verbatim detector reason> pointer: edit the source named in the build script, not docs/
This is loom-qp0's required behavior — when docs/ is the artifact, audit-project must not point the user at it. The README-in-scope expansion below (loom-ojn) naturally handles the cardinality drift case (reports drift in README, the source, even when docs/ is generated from README).
Step 1b — wing-variant warning (auto-detect only)
If the wing was auto-detected (steps 2 or 3 of the wing precedence
chain — i.e., --wing was NOT explicitly passed), surface a WARN when
the resolved wing has basename-variant siblings in the palace. This
catches the case where the canonical project wing uses a different
separator or capitalization than the directory basename suggests
(e.g., directory hundred-acre-woods auto-resolves to wing
hundred-acre-woods with 3 drawers, but the canonical wing is
hundred_acre_woods with 13 drawers).
Procedure:
- Call
mempalace_list_wings. - Compare the auto-detected wing slug
Wagainst every other wing slugSin the result.Sis a basename-variant ofWwhen any of these holds:SequalsWafter substituting_↔-and lowercasingSequalsWafter stripping/adding a trailings(singular ↔ plural)SequalsWafter collapsing_and-to a common neutral (snake_case ↔ kebab-case both reduce to the same compact form)
- For each variant
S, record its drawer count (usemempalace_list_drawers(wing=S, limit=1)and the returned total count, or whichever palace API surfaces the count cheaply). - Emit the WARN line only when at least one variant
Shasdrawer_count(S) > drawer_count(W). Skip the WARN when the auto-detected wing is the largest sibling (it's plausibly canonical; no escalation needed).
WARN line shape:
[WING WARN] auto-detected wing may not be canonical
resolved: <W> (<M> drawers, basename auto-detect)
variants: <S1> (<N1> drawers), <S2> (<N2> drawers)
suggested: re-run with --wing <S_largest> if you want drift checks
scoped to the larger wing
Surface this WARN before Step 2, so the user can interrupt and
re-run with the correct --wing. Do not block — emit, then continue.
The user owns the call; the skill just makes the silent-wrong case
loud.
If --wing was explicitly passed, skip Step 1b entirely (the user
made the call deliberately).
Step 2 — dispatch project-onboarder (unless --check=docs)
Call the project-onboarder subagent with the absolute project
root (the resolved --root value) and the project's short name
(the resolved --wing value, which doubles as the bd-memories
search keyword and the wing slug the subagent reports against).
Wait for its structured PASS/WARN/MISS checklist. Display
the report verbatim before moving to step 3.
The onboarder enumerates 20 items including git hygiene, bd init,
bd hooks, workflow.json, MemPalace wing, CLAUDE.md, .claude/rules/,
docs scaffold, .claude/agents/+commands/, bd memories tribal
facts, .gitignore loom-ephemera entries, the .deploy wrap-up
hint (item 21, loom-1tq), and — added by loom-ann —
Claude Code hook command duplicates: the same (event, matcher, command) tuple registered in both the project's
.claude/settings.json (or ~/.claude/settings.json) and a plugin's
plugin.json. Duplicates fire the command twice per event, billing
wasted tokens (observed in liza_base 2026-05-09; fixed via loom-sd5
by removing the project-layer entry — the plugin's registration is
canonical). Project-level dups surface as WARN; user-level dups as
INFO (machine-specific config, advisory only).
The duplicate JSON-stanza removal is content-aware (multiple hook
entries may share a stanza), so the raw removal was excluded from
the Wave 2 deterministic-apply contract. loom-jnn closes the
resolution gap with two purpose-built AUTOFIX paths for the WARN
case (detection via find-hook-dups.sh is unchanged): the DEFAULT
[AUTOFIX:dedup-hook-skip-worktree] (per-user, reversible —
git update-index --skip-worktree the tracked file + strip the dup
locally + log the recovery snippet), and the opt-in
[AUTOFIX:dedup-hook-commit] behind an explicit y/N confirmation
that names the shared-content consequence. The empirical reason a
resolution is needed at all — empty-array overrides in
settings.local.json do NOT cancel inherited hook registrations
because Claude Code hook layering is additive across all four
layers — lives in
docs/reference/claude-code-hook-layering.md.
Items 13–15, 21: interactive-resolution checks (loom-r6g, loom-z3m, loom-1tq)
The onboarder also runs several checks that surface defaults wrong
for the project's shape (or fields the user has never been asked
about) but require interactive resolution. The skill (this file)
owns the prompt loop and the write half; the onboarder only reports
the verdict. Items 13–14 (language + solo-workspace) are loom-r6g,
item 15 (upstream:loom label) is loom-z3m.11, and item 21 (.deploy
wrap-up hint) is loom-1tq.
Item 13 — preflight-language-match
The onboarder describes detect_project_language() (canonical
markers: pyproject.toml/setup.py/setup.cfg/requirements*.txt →
python; go.mod → go; Cargo.toml → rust; package.json → node;
scripts/+*.sh fallback → shell; otherwise / polyglot → unknown).
Tie-break rule: never guess on polyglot. The onboarder reads
<root>/.beads/preflight.template (or config.yaml's
preflight.template field) for the bd preflight shape.
Verdicts the onboarder emits, and the skill's response:
PROMPT (language=unknown AND preflight.template unset / bd-default). The skill prompts the user interactively:
Item 13: project language is unknown and preflight.template is unset / bd-default Go-shaped. Pick a language for the preflight template: (python / go / rust / node / shell / skip)On a non-skip answer the skill writes the matching template into
.beads/preflight.template(or the equivalent field inconfig.yaml). Onskip, the skill writes a per-check memo into<root>/.claude/loom-audit-state.jsonso future runs render this row as a silent PASS.WARN (language ∈ {python, rust, node, shell} AND preflight.template starts with
goor is the bd-default Go-shaped template). The skill offers a y/N/skip diff preview showing the proposed template replacement. Onyit writes; onNit leaves the row in the queue; onskipit writes the state-file memo. The skill does NOT add a new AUTOFIX recipe — the choice of replacement template is content-aware and stays in the per-item conversational gate.PASS otherwise.
Test mocking surface: the env var LOOM_AUDIT_PROMPT_ANSWER lets
test fixtures inject the PROMPT/WARN answer non-interactively (e.g.
LOOM_AUDIT_PROMPT_ANSWER=python or LOOM_AUDIT_PROMPT_ANSWER=skip).
The skill checks this env var first when running under tests; in
real interactive sessions it stays unset and the conversational
gate fires normally.
Item 14 — claude-md-solo-aware
The onboarder describes is_solo_workspace(): run
bd dolt remote list --json. [] → TRUE; non-empty (a "name"
field present) → FALSE; error → degrade-safe TRUE. When solo, the
onboarder scans <root>/CLAUDE.md for bd dolt push lines that
are NOT wrapped in the canonical loom-hsb guard.
The canonical loom-hsb guard shape (copy verbatim from loom's own CLAUDE.md — do NOT paraphrase):
if bd dolt remote list --json 2>/dev/null | grep -q '"name"'; then
bd dolt push
else
echo "(solo bd workspace; no Dolt remote — skipping bd dolt push)"
fi
Verdicts:
WARN (solo workspace AND unguarded
bd dolt pushpresent in CLAUDE.md's BEADS INTEGRATION block). The skill offers a y/N/skip diff preview that rewrites the canonical block to the loom-hsb guard shape. If the surrounding block has been hand-edited beyond pattern recognition (e.g., the surroundingbd dolt pushis part of a larger custom workflow, or the lines around it don't match the canonicalbd init-generated template), the fix refuses with a one-line pointer to loom's own CLAUDE.md ("Reference shape lives in loom/CLAUDE.md — copy by hand").skipwrites the state-file memo forclaude-md-solo-aware.PASS otherwise (no CLAUDE.md; no BEADS INTEGRATION block; block already uses the guard; skip memo exists; is_solo_workspace returned FALSE).
Item 15 — upstream-loom-label-suggest
Cross-tracker dependency hygiene. Project beads sometimes exist only
because of an open loom-side bug — the bead's life ends when the
loom fix lands, but there is no auto-clearing signal back to the
project's tracker. The upstream:loom label is the cross-tracker
handshake (see
docs/reference/upstream-loom-label.md),
and this check surfaces candidate beads that should carry it.
The onboarder enumerates open project beads whose description matches the canonical loom-keyword regex:
(^|[^a-zA-Z0-9_])(loom-hook|loom-script|loom-[a-z0-9]+)|hooks/|scripts/loom-
The word-boundary anchor on loom- prefix avoids matching substrings
inside other words (heirloom-data, etc.). The five canonical signals:
loom-hook— bare token reference to a loom hook classhooks/— path prefix referring to loom'shooks/directoryloom-script— bare token reference to a loom script classscripts/loom-— path prefix referring to loom's installed scriptsloom-<id>— direct bead-ID reference (loom-x4m,loom-z3m, etc.)
Verdicts:
- INFO = at least one matching bead lacks the
upstream:loomlabel. The skill renders the matching beads in a y/N/skip gate per bead — the user decides whether to apply the label. Informational only — never auto-applies. Onythe skill runsbd label add <id> upstream:loom; onNit leaves the row in the queue; onskipit writes aupstream-loom-label-suggestmemo to.claude/loom-audit-state.jsonso the same row does not re-prompt. - PASS otherwise (no matching beads; all matching beads already carry the label; skip memo exists).
No AUTOFIX tag — applying the label per-bead is a real human choice (the regex catches structural workaround beads, but also catches beads that mention loom in passing without being a workaround). The gate stays interactive.
The companion /check-loom-upstream slash command runs the same
sweep on-demand outside of an audit and additionally pairs labeled
beads against recently-closed loom beads — its output is a
suggestion-stream, never a write.
Lineage: loom-z3m.11 (2026-05-23). Surfaced by lingering HAW bead
7iz that mirrored what loom-x4m fixed; cleared by inspection only
because someone happened to remember the pairing. The label +
sweep + suggest-on-audit triad addresses the next sibling case
structurally.
Item 21 — workflow-deploy-hint
The .deploy field in <root>/.claude/workflow.json (loom-0k0) is
the shell command /wrap-up section 6 surfaces as Next step (project deploy): <cmd> after a bead closes. It is optional and
silent-skip by default, which makes it undiscoverable until a user
reads the wrap-up source — this check surfaces it so the user can
set it (or explicitly opt out) at onboarding time. Mirrors the
.guest-block discovery + onboarding pattern (loom-4re).
The onboarder reads the field's three-state lifecycle
(workflow_config_deploy_state in lib/workflow-config.sh):
set (non-empty string), empty (""/null — explicit opt-out),
absent (key not present — never decided). The skill (this file)
owns the prompt loop and the write half; the onboarder only reports
the verdict.
Verdicts the onboarder emits, and the skill's response:
MISS (
workflow.jsonexists AND.deployisabsent). The skill prompts the user interactively with the loom-1tq prompt verbatim:Item 21: .deploy is unset in <root>/.claude/workflow.json. What command should /wrap-up surface as the project deploy hint? (e.g. ./install.sh, make deploy, ./scripts/build. Leave blank to explicitly opt out — sets .deploy: "".)On a non-blank answer the skill writes the command verbatim via
workflow_config_deploy_set "<command>" <root>— no validation, no auto-detection (both out of scope, loom-1tq). On a blank answer (the explicit opt-out) the skill writes.deploy: ""viaworkflow_config_deploy_set "" <root>; the empty string flips the state fromabsenttoemptyso future audits report PASS and do NOT re-prompt — empty means "explicitly chose nothing", distinct from absent's "never decided". Either write preserves.mode,.v, and any.guestblock. On a literalskipanswer the skill writes aworkflow-deploy-hintskip memo into<root>/.claude/loom-audit-state.jsonso the row renders as a silent PASS on future runs.N/A (
workflow.jsondoesn't exist). Item 4 already covers the missing-config case; the skill renders the row asN/Aand takes no action. Do not write aworkflow.jsonfrom this item — that is item 4's[AUTOFIX:workflow-json]job.PASS otherwise (state is
setorempty, or a skip memo exists).
No AUTOFIX tag — the value is user-supplied (a command or an
explicit blank opt-out); there is no deterministic command to write,
so the fix stays in the per-item conversational gate. The
LOOM_AUDIT_PROMPT_ANSWER env var injects the answer
non-interactively under tests (same mocking surface as items
13/14): LOOM_AUDIT_PROMPT_ANSWER='./install.sh' simulates a typed
command, LOOM_AUDIT_PROMPT_ANSWER='' simulates the blank opt-out,
LOOM_AUDIT_PROMPT_ANSWER=skip writes the skip memo.
Lineage: loom-1tq (2026-06-08), parent finding
drawer_loom_decisions_9fb2868e288751d22c6dd7ec (loom-0k0). The
schema-write path is unit-tested at
lib/tests/workflow-config-deploy.test.sh (parallel to
workflow-config-guest.test.sh).
State file: <root>/.claude/loom-audit-state.json
Per-project, gitignored. Stores per-check skip memos so re-runs respect "user said no". Schema:
{
"<check-name>": {
"skipped_at": "<ISO-8601 timestamp>",
"reason": "user-skipped"
}
}
Recognised check-names: preflight-language-match,
claude-md-solo-aware, upstream-loom-label-suggest,
workflow-deploy-hint (item 21 — skip memo when the user declines to
set or opt out of .deploy), script-convention (item 23 — skip memo
when the user declines the script/-skeleton scaffold offer and/or the
.deploy → canonical_commands.deploy migration offer),
dedup-hook-skip-worktree (item 12 —
stores the recovery snippet applied by the default AUTOFIX, not a
skip memo). The skill
reads the file at the start of Step 2; for any check with a skip
memo, the onboarder's verdict is silently downgraded to PASS in the
rendered report. The dedup-hook-skip-worktree entry is a record of
the applied recovery snippet rather than a user-skipped memo — its
presence does not suppress the row, since a later upstream change to
settings.json may re-introduce the duplicate. The skill writes the memo on skip answers from
the per-item gate. The file is NOT a config file; it is never read
outside /audit-project, and the <root>/.gitignore adds
.claude/loom-audit-state.json on first audit so it stays out of
the project's history.
Lineage: loom-r6g (2026-05-21) for items 13-14. Surfaced by
/audit-project on fresh ~/repos/mforth: a Python solo project
passed every existing check while inheriting a Go preflight template
and an unguarded CLAUDE.md bd dolt push. The two checks are
conceptually "workflow-infrastructure language fit" plus "workflow-
infrastructure topology fit" — orthogonal to the existing 12 checks,
hence two new rows rather than expanding one. Item 15
(upstream-loom-label-suggest) added by loom-z3m.11 (2026-05-23)
to address the orthogonal "cross-tracker dependency awareness" gap.
Step 3 — docs drift detection (unless --check=onboarding)
Run the six sub-checks below in order. Each produces zero or more
report lines tagged [DOC FIX], with three fields:
- what doc says — the verbatim claim (or path) the doc makes
- what reality says — what the system / beads / palace shows
- suggested fix — the minimal edit that resolves the drift
Lines accumulate into one report section labeled ## Docs drift detection. Empty section = clean.
Default doc scope
All sub-checks scan a single flat set of doc files (loom-ojn):
<root>/README.md,<root>/README.rst,<root>/README.txt— the root README, in whichever extension the project uses.<root>/docs/**/*.md— the existing scope.
Out of scope (excluded by default): AGENTS.md, CLAUDE.md,
GEMINI.md at the root (these are agent-instruction files, not
user-docs); .github/* and package-metadata files. The exclusion is
hardcoded for v1; a future bead may add a --scope-extra <glob>
flag if a project needs to bring more files into scope.
There is no MIRROR qualifier — root README is a sibling doc to
docs/, not a mirror. Drift in README reports as a plain [DOC FIX]
on the same footing as drift inside docs/. (Historical note: the
[DOC FIX][MIRROR] tag was an emergent runtime behavior during the
loom-b6o trial; it is not part of the skill's specified output.)
When docs_generated = true (per Step 1), the <root>/docs/**/*.md
half of the scope is skipped per check (one [DOC SKIP][GENERATED]
line emitted per class) but root README files remain in scope —
which is usually the source-of-truth in generated-docs projects
(e.g. cp README.md docs/index.md in tla-puzzles).
Sub-check execution
All filesystem globs and paths in the sub-checks below are relative
to the resolved --root. All bd show calls run in the project's
.beads/ workspace by cd-ing to <root> first (or by setting
bd's --workspace flag if available — cd is the portable
default). All mempalace_search calls filter by the resolved
--wing value.
If neither <root>/docs/ nor any of <root>/README.{md,rst,txt}
exists, emit ## Docs drift detection with a single line no in-scope doc files at <root> — skipping docs drift detection and
proceed to Step 4. If <root>/docs/.no-diataxis is present, emit a
[DOC FIX][INFO] diataxis-opt-out note explaining that Checks 4 and
6 are skipped (both assume the Diataxis/mkdocs setup the opt-out
marker disclaims), then run Checks 1, 2, 3, and 5 normally.
Check 1 — Cardinality
Find numeric claims in <root>/docs/ that count primitives. For
v1 the patterns are naive grep:
All (one|two|three|four|five|six|seven|eight|nine|ten|N+) <noun><digit>+ (skills|commands|subagents|hooks|recipes|drawers|wings)(only|just|exactly) <digit>+ <noun>
For each match, identify the noun and source-of-truth glob (all
paths relative to the resolved --root; wing-scoped MCP calls use
the resolved --wing value):
| Noun | Source-of-truth |
|---|---|
skills / recipes |
<root>/skills/*/SKILL.md |
commands / slash commands |
<root>/commands/*.md |
subagents / agents |
<root>/agents/*.md |
hooks |
<root>/hooks/*.sh |
wings / rooms |
mempalace_list_wings / mempalace_list_rooms (filtered to --wing for room counts) |
If a primitive directory doesn't exist under <root> (e.g., a
project that has no agents/), skip cardinality claims about that
noun rather than reporting "0 found".
Compare the doc's count to the actual count. Mismatch → emit:
[DOC FIX][TRIVIAL] cardinality
doc: <file>:<line> "All <N> <noun> have <claim>"
reality: <M> <noun> match <glob>
suggested: s/<N>/<M>/
Emit the [TRIVIAL] qualifier ONLY when:
- The doc's text differs from reality by exactly one numeral (or one
word-number like
four→five), AND - The substitution is unambiguous at the given file:line (the numeral appears once on that line, so a literal s/old/new/ is safe).
When the mismatch is "N matches but K satisfy [DOC FIX] without the
[TRIVIAL] tag and let the user resolve it manually.
This catches the loom-469 class. The --apply-trivial flag (Step 3.5)
applies every [DOC FIX][TRIVIAL] item.
Check 2 — Citation resolution
Every citation in <root>/docs/ must resolve. Scan for:
Bead IDs — delegate the scan + resolve to
lib/bd-id-extract.sh(loom-6m8). The helper takes doc text on stdin and emits one dead bead-ID per line on stdout. It detects the project's bd prefix as a LITERAL string from<root>/.beads/issues.jsonl(orbd list --limit 1 --jsonas fallback), so snake_case prefixes (liza_base-) and hyphenated prefixes (tla-puzzles-) both work without regex-shape guessing. Invocation:find <root>/docs -type f -name '*.md' -print0 \ | xargs -0 cat \ | bash <loom>/lib/bd-id-extract.sh --root=<root>For each emitted ID, also run
cd <root> && bd show <id> 2>&1to recover close-reason / supersession metadata for the[DOC FIX] dead-bead-idline. Do NOT write ad-hoc regexes inline — the helper exists precisely so every/audit-projectrun produces the same answer (loom-6m8 surfaced an "every ID shows as dead" false-positive caused by per-run regex drift). Failure → emit[DOC FIX] dead-bead-id.Commit SHAs — pattern
\b[0-9a-f]{7,40}\badjacent to "commit" / "sha" / git context. For each match, rungit -C <root> cat-file -e <sha> 2>&1. Failure → emit[DOC FIX] dead-commit.File paths — pattern that looks like a path inside the repo (starts with one of the project's detected primitive directories —
skills/,commands/,agents/,hooks/if present — or withdocs/,lib/,scripts/, etc., and ends in a known extension or directory marker). For each match, check the filesystem under<root>. Missing → emit[DOC FIX] missing-path.Drawer slugs — any reference to a MemPalace drawer by slug or title. Pattern: text inside backticks adjacent to "drawer" / "MemPalace" / "wing/" / "decisions" — admittedly fuzzy in v1. For each candidate, call
mempalace_searchwithwing=<--wing>to scope the lookup to the project's own wing, plus an unfiltered fallback search if nothing hits (the doc may legitimately cite a cross-wing drawer reachable via tunnel). Emit[DOC FIX] missing-draweronly if both searches return no strong match.Slash command names — pattern
/[a-z0-9-]+\b. For each match, check<root>/commands/<name>.mdexists. Missing → emit[DOC FIX] missing-slash-command. Skip this check if the project has nocommands/directory at all (the slash-command convention doesn't apply).
Output line shape (per failed citation):
[DOC FIX] <citation-class>
doc: <file>:<line> cites `<token>`
reality: <one-line failure from the resolution attempt>
suggested: <replacement|removal hint>
For dead bead-IDs specifically, attempt to follow the
supersedes-chain: bd show <dead-id> may return supersession
metadata in the close reason or via a superseded-by label;
if so, suggest the replacement ID. When the supersedes-chain
yields exactly one replacement ID, emit the line with the
[TRIVIAL] qualifier:
[DOC FIX][TRIVIAL] dead-bead-id
doc: <file>:<line> cites `<dead-id>`
reality: bd show <dead-id> → superseded-by <new-id>
suggested: s/<dead-id>/<new-id>/
When the chain yields zero or multiple candidates, emit
[DOC FIX] dead-bead-id without the [TRIVIAL] tag — the
replacement is a real choice and stays in the per-item approval
queue.
This catches the loom-qj3 lying-doc class for the citation sub-class.
Check 3 — Behavior claims
Doc says X exists / X does Y. Verify against system reality. v1 naive: scan for sentences of shape:
\`<token>\` (exists|is shipped|ships|is installed|carries)\`<token>\` (does|fires|invokes|dispatches) <claim>
For tokens that name a primitive (/foo → command,
<name>-a-bead → skill, <name>-researcher → subagent,
<name>.sh → hook), check the corresponding source file under
<root> (and skip the class entirely if the project doesn't
have that primitive directory):
/<name>→<root>/commands/<name>.md<name>-a-bead(without slash) →<root>/skills/<name>-a-bead/SKILL.md- bare hook name
<x>.sh→<root>/hooks/<x>.sh - bare subagent name (matches an
<root>/agents/*.mdbasename) → that file
Missing source file → emit:
[DOC FIX] missing-primitive
doc: <file>:<line> "<verbatim claim>"
reality: <expected source file> does not exist
suggested: remove the claim, or file a bead to add the primitive
For "does Y" claims, v1 cannot semantically verify the action
description (that's v2). It can only confirm the primitive itself
exists. If the primitive exists but the claim's verb is suspect
(e.g., "fires on X" claims with a hook that doesn't bind to X in
settings.json), surface as a [DOC FIX][SOFT] for human review
rather than [DOC FIX].
This catches the loom-qj3 lying-doc class for the "X exists" / "X does Y" sub-class.
Check 4 — Inclusion-glob coverage (symmetric)
Skipped entirely under docs/.no-diataxis opt-out — this
check assumes the Diataxis-shaped docs/reference/<thing>/ catalog
layout, which the opt-out marker disclaims. The other four checks
still run.
For each catalog page in <root>/docs/reference/ whose source
primitive directory exists under <root>:
| Catalog page | Source glob |
|---|---|
<root>/docs/reference/skills/index.md |
<root>/skills/*/SKILL.md |
<root>/docs/reference/slash-commands/index.md |
<root>/commands/*.md |
<root>/docs/reference/subagents/index.md |
<root>/agents/*.md |
<root>/docs/reference/hooks/index.md |
<root>/hooks/*.sh |
Pairs are skipped when either side is absent: a project with no
agents/ directory has nothing to glob; a project that hasn't
shipped docs/reference/hooks/index.md has no catalog to check.
The check fires only on (catalog-page-exists AND source-dir-exists)
pairs.
Two checks per pair:
- Source → doc. Every primitive on disk must appear in the
catalog page (by name). Missing → emit
[DOC FIX] missing-from-catalog. - Doc → source. Every primitive named in the catalog page
must correspond to a real file under the source glob. Missing
→ emit
[DOC FIX] catalog-ghost.
The symmetric check is what catches the case where a new primitive shipped without doc backfill (source → doc miss) AND the case where docs claim a primitive that doesn't exist (doc → source miss; the loom-qj3 / installed-files-claims-audit-project class).
Delegate to the mechanized engines when the project ships them
(loom-wj26.3). The grep-derived comparison above is the generic
fallback. When the project being audited carries
scripts/loom-docs-catalogue and/or scripts/loom-docs-gen — loom's
own repo, or any project that has adopted those engines — do NOT
re-derive the symmetric coverage by grep. Instead delegate to the
mechanized gates and surface their findings verbatim:
scripts/loom-docs-catalogue --check(loom-wjuo) is the deterministic name-set drift gate for thedocs/reference/<category>/index tables. It exits non-zero when a shipped primitive is absent from — or a ghost is present in — the inventory tables, which is exactly the source→doc / doc→source symmetric coverage Check 4 re-derives by hand. Run it and report its drift lines.scripts/loom-docs-gen --check(loom-itph) is the companion gate for the per-item nav pages + the nav block. It recomputes the expected per-primitive wrapper pages and the nav and diffs them against what's committed, catching the page-level half of inclusion drift the catalogue index check does not.
So: in a repo that ships the engines, Check 4 runs
scripts/loom-docs-catalogue --check (index tables) +
scripts/loom-docs-gen --check (per-item nav pages + nav block) and
surfaces their non-zero findings as the [DOC FIX] lines, instead
of the re-derived grep. The engines are deterministic and gateable,
so their result supersedes the naive grep wherever both could run.
For projects without those scripts (no scripts/loom-docs-catalogue
and no scripts/loom-docs-gen — most non-loom-managed projects until
the deferred generic engine lands), keep the existing generic grep
fallback described above.
For projects that use mkdocs-include-markdown to auto-glob the
catalog, the auto-generated all-<thing>.md page should be
trusted as the authoritative inclusion result. Compare the
human-edited Inventory / Invocation tables in index.md against
the auto-globbed all-<thing>.md content; any name that appears
in all-<thing>.md but not in index.md is a missing-from- inventory drift. Names that appear in index.md but not in
all-<thing>.md are a inventory-ghost drift.
Check 5 — Explanation-doc consistency
Every page under <root>/docs/explanation/ cites at least one
MemPalace drawer (the design-source-of-truth claim from
docs/explanation/provenance.md and the recipe-family doc).
For each citation:
- Exact slug →
mempalace_get_drawer(slug). Hit → PASS. - Title-shaped citation →
mempalace_search(title, wing=<--wing>). Scope the search to the project's own wing first; on no strong hit, retry without wing filter (cross-wing tunnel case). Top result with high similarity → PASS. No strong match either way → emit[DOC FIX] missing-drawer-citation.
This is a v1 best-effort check — drawer citation in prose is
unstructured, so the patterns are fuzzy. If the project uses a
convention like > Drawer: <wing>/<slug> or footnote-style
citations, prefer those structured patterns and skip free-text
matching.
Check 6 — mkdocs markdown_extensions drift
GATE: fire ONLY when <root>/mkdocs.yml exists AND
<root>/docs/.no-diataxis is absent. A project that doesn't use
mkdocs has no mkdocs.yml and nothing to check — when
<root>/mkdocs.yml is absent, this check is silently skipped (no
line emitted). Under the .no-diataxis opt-out it is skipped too
(Check 6 assumes the Diataxis/mkdocs setup, same as Check 4).
This check detects a project's own site mkdocs.yml silently
lagging the diataxis template's markdown_extensions — the
loom-be3t / loom-p4tf "instance silently lags the diataxis
template" class. It is invisible to mkdocs build --strict (a
stray icon shortcode like :material-school: is valid markdown —
it just renders as literal text, exit 0) AND to the serving check
(the page still returns HTTP 200). A dropped extension surfaces only
as a human eyeballing the rendered page; this check catches it at
the source-text level before any build.
Locate loom's template via the loom-install root — the SAME
<loom> root mechanism Check 2 uses for <loom>/lib/bd-id-extract.sh
— at <loom>/templates/diataxis/mkdocs.yml.template.
Compare extension NAME sets (name-altitude, NOT full per-extension
config blocks): the NAME is the token in the markdown_extensions
list after - , up to a trailing : or EOL, trimmed (- admonition
bare, - toc: with sub-config, - pymdownx.emoji: with sub-config).
Comparing names — not config — means a project legitimately tuning a
per-extension sub-config block never false-positives, while a WHOLE
extension going missing (the be3t bug) is still caught.
For every template extension NAME NOT present in <root>/mkdocs.yml's
markdown_extensions (template name-set ⊄ instance name-set), emit
one line:
[DOC FIX] mkdocs-extension-drift
doc: <root>/mkdocs.yml markdown_extensions
reality: diataxis template declares `<ext>` but the instance does not
suggested: add `<ext>` to <root>/mkdocs.yml markdown_extensions (sync from templates/diataxis/mkdocs.yml.template)
Do NOT tag these [TRIVIAL]: adding an extension block — especially
one with sub-config like pymdownx.emoji's emoji_index /
emoji_generator lines — is not a safe one-numeral substitution, so
it stays in the per-item human-approval queue (Step 4).
Step 3.5 — apply tagged items (only when --apply-trivial / --apply-onboarding set)
If neither flag is set, skip this step entirely; every item flows to the per-item approval queue in Step 4.
When at least one apply flag is set, walk the report top-to-bottom and
process items as follows. Order: onboarding items first (they may
create .beads/, .claude/, etc. that downstream items reference),
then doc-drift items.
--apply-onboarding: walk the project-onboarder report
For each line whose Suggested fix text contains a literal
[AUTOFIX:<recipe-id>] token (substring match — exact bracketed
form), apply the recipe:
Guest-mode gate (loom-3r8). The read-only scan that produced this report ALWAYS runs (Diataxis-shape detection, drawer-citation probes, etc. don't touch the tree). But every AUTOFIX recipe below writes into the project tree, so each one MUST source
lib/refuse-on-guest.shand callrefuse_if_guest AUTOFIX:<recipe-id>before doing any in-tree work. If the call returns 1, skip that item with a one-line note (AUTOFIX:<id>: skipped — guest mode active) and continue to the next item. The check is per-item, not per-run, so a future AUTOFIX recipe that's safe under guest mode (e.g. external-only) can opt out simply by omitting the call.
[AUTOFIX:bd-hooks]— gate first, then execute:. "$LOOM_ROOT/lib/refuse-on-guest.sh" refuse_if_guest AUTOFIX:bd-hooks || exit $? cd <root> && bd hooks install cd <root> && git add .beads/issues.jsonl 2>/dev/null cd <root> && git -c core.hooksPath=/dev/null commit -m "bd: post-install export sync" 2>/dev/null \ || echo "(nothing to absorb — fresh .beads/ already clean)"The
core.hooksPath=/dev/nulloverride on the absorbing commit prevents the just-installed pre-commit hook from re-firing on its own export — it's the chicken-and-egg break the loom-cka two-step is meant to dodge. If the absorbing commit has no staged content, emit a one-line "(nothing to absorb)" note and continue.[AUTOFIX:workflow-json]— gate first (refuse_if_guest AUTOFIX:workflow-json), then write{"v":1,"mode":"<mode>"}(where<mode>defaults tofull, or the value passed via--workflow-mode) to<root>/.claude/workflow.json. Create<root>/.claude/if it doesn't exist. Do NOT overwrite an existing file — re-check the presence first; if the file appeared between the scan and apply step (race), skip with a note.[AUTOFIX:gitignore-worktrees]— gate first (refuse_if_guest AUTOFIX:gitignore-worktrees), then append BOTH of the per-session loom ephemera lines to<root>/.gitignore(creating the file if absent):.claude/worktrees/— the dispatch-isolation path (Agent+isolation: "worktree"); never meant to be tracked..claude/workflow-state.json— per-session ephemeral state written at every session start by the loom statusline /workflow-statehelper. Both customer trials (tla-puzzles loom-b6o, liza_base loom-wxo) hit this manually; loom-tat folded the line into the same recipe. Each line is appended INDEPENDENTLY and IDEMPOTENTLY — re-read the file first; for each candidate line, skip the append if it is already present (line-exact match against.claude/worktrees/or.claude/workflow-state.jsonrespectively). A partial pre- existing state (only one of the two lines present) results in exactly the missing line being appended; the already-present line is never duplicated.
[AUTOFIX:loom-env-block]— gate first (refuse_if_guest AUTOFIX:loom-env-block), then deep-merge the canonical loom env block into<root>/.claude/settings.json. The block:{ "env": { "CLAUDE_CODE_ENABLE_TASKS": "false", "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1" } }Loom owns these two keys — conflicts on them OVERWRITE; every other
env.*key (and every non-env top-level key) is preserved verbatim. The merge uses the same python-shape as loom's owninstall.shenv-merge step (loom-7ro):mkdir -p "<root>/.claude" if [ ! -f "<root>/.claude/settings.json" ]; then cat >"<root>/.claude/settings.json" <<'JSON' { "env": { "CLAUDE_CODE_ENABLE_TASKS": "false", "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1" } } JSON else python3 - "<root>/.claude/settings.json" <<'PYEOF' import json, os, shutil, sys path = sys.argv[1] canonical = { "CLAUDE_CODE_ENABLE_TASKS": "false", "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1", } with open(path) as f: cur = json.load(f) cur_env = cur.get("env", {}) if isinstance(cur.get("env"), dict) else {} conflicts = [(k, cur_env[k], v) for k, v in canonical.items() if k in cur_env and cur_env[k] != v] additions = [k for k in canonical if k not in cur_env] if not conflicts and not additions: sys.exit(0) backup = path + ".pre-loom.bak" if not os.path.exists(backup): shutil.copy2(path, backup) merged = dict(cur_env) for k, v in canonical.items(): merged[k] = v cur["env"] = merged with open(path, "w") as f: json.dump(cur, f, indent=2); f.write("\n") PYEOF fiWrites
.claude/settings.json.pre-loom.bakon first overwrite (the python script handles the idempotency check internally — when both keys are already canonical the script exits without writing and without creating a backup). The lineage and motivation match loom's install.sh env-merge step: counters the harness's competing defaults (TaskCreate / MEMORY.md) on a per-project basis.[AUTOFIX:loom-upstream-gc-handoff]— handoff recipe; no in-tree write and no guest-mode gate needed (the recipe only prints text). For each orphan clone the onboarder reported under~/.loom/upstream/<owner>/<repo>/, the recipe emits:orphan clone: ~/.loom/upstream/<owner>/<repo>/ no open upstream:watch bead references this clone run /loom-upstream-gc to review and prune interactivelyThen mark the item resolved-by-handoff so it does not re-surface in the per-item Step 4 queue. The actual prune lives in
/loom-upstream-gc(loom-k2g.4) which gates per-clone with y/N and refuses removal when uncommitted changes are present.[AUTOFIX:gh-auth-prompt]— handoff recipe; no in-tree write and no guest-mode gate needed. Emit:gh is not authenticated (`gh auth status` exited non-zero): <verbatim gh auth status stderr from the onboarder report> run `gh auth login` interactively to authenticateThen mark the item resolved-by-handoff.
gh auth loginis an interactive OAuth/token flow that cannot run inside the audit; the user runs it in their own terminal, then re-runs/audit-projectto confirm the WARN cleared.[AUTOFIX:dedup-hook-skip-worktree]— the default (DEFAULT) duplicate-hook resolution (item 12 WARN). Gate first (refuse_if_guest AUTOFIX:dedup-hook-skip-worktree), then resolve the duplicate per-user and reversibly:. "$LOOM_ROOT/lib/refuse-on-guest.sh" refuse_if_guest AUTOFIX:dedup-hook-skip-worktree || exit $? cd <root> # 1. Stop tracking local edits to the shared settings file. This # ALSO defuses the "would be overwritten by checkout" error a # future upstream pull would otherwise raise on the local strip. git update-index --skip-worktree .claude/settings.json # 2. Strip the duplicate (event, matcher, command) stanza from the # LOCAL copy — content-aware JSON edit (the dup hook the item-12 # WARN line named; preserve every other stanza). Use the Edit # tool / a python json rewrite, not a blind sed. # 3. Log the recovery snippet under the dedup-hook-skip-worktree key # in .claude/loom-audit-state.json (see below).The recovery snippet baked into
.claude/loom-audit-state.json(so the next upstream change tosettings.jsonis not a surprise):git update-index --no-skip-worktree .claude/settings.json git stash git pull git stash pop # then re-apply skip-worktree + strip via /audit-project --apply-onboardingThis recipe is safe to auto-apply on
--apply-onboardingbecause it is per-user and reversible — it never touches shared content, so it cannot break a non-loom dev's checkout. It is the default offer for item 12. The state-file entry shape:{ "dedup-hook-skip-worktree": { "applied_at": "<ISO-8601 timestamp>", "hook": "<event> <command>", "recovery": "git update-index --no-skip-worktree .claude/settings.json; git stash; git pull; git stash pop" } }[AUTOFIX:dedup-hook-commit]— the OPT-IN duplicate-hook resolution (item 12 WARN) that never auto-applies without an explicit y/N confirmation. This recipe changes shared content (it removes the duplicate stanza from the tracked.claude/settings.jsonand commits), so the binaryapplyshape does NOT fit — even with--apply-onboardingset, the recipe MUST NOT auto-apply. It plumbs a confirmation prompt through and obeys the same conversational-pause invariant as Step 4 (loom-xcw): after printing the prompt, STOP and wait for a user-typed reply.Gate first (
refuse_if_guest AUTOFIX:dedup-hook-commit), then print the confirmation prompt verbatim (substitute the offending hook name from the item-12 WARN line):This commits a change to .claude/settings.json that assumes loom adoption for all devs on this repo. Non-loom devs lose <hook-name> registration. Proceed? (y/N)On a typed
y(and only then): strip the duplicate stanza from the tracked file and commit with the scoped subject —cd <root> # strip the duplicate (event, matcher, command) stanza (content-aware) git add .claude/settings.json git commit -m "audit: dedup <hook-name> SessionStart hook (loom-managed; plugin + user-global handle registration)"On
N(or any non-yreply): leave the row in the per-item queue and emitAUTOFIX:dedup-hook-commit: declined — left for manual handling. TheLOOM_AUDIT_PROMPT_ANSWERenv var injects the y/N answer non-interactively under tests (same mocking surface as items 13/14).The reason a resolution path is needed at all: Claude Code hook layering is additive across all four layers (plugin + user-global
~/.claude/settings.json+ project-tracked.claude/settings.json+ project-local.claude/settings.local.json). Empty arrays insettings.local.jsondo NOT cancel an inherited registration — layering is union, not override. Verified inert in e2e-api-tests on 2026-05-27: bd prime still fired 3 times in a fresh SessionStart after the empty-array override was applied. The only resolutions that actually work are the two recipes above; seedocs/reference/claude-code-hook-layering.mdfor the full finding. (loom-jnn.)
For each item NOT carrying an [AUTOFIX:<id>] tag, leave it in the
queue for Step 4. Emit one summary line per skipped item: --apply- onboarding: skipping item N (no AUTOFIX tag — requires human review).
--apply-trivial: walk the docs-drift section
For each line tagged [DOC FIX][TRIVIAL], apply the suggested
substitution:
- The
suggested:field for TRIVIAL items is always shapes/<old>/<new>/. Use theEdittool against the file at thedoc:field's<file>:<line>location; pass the verbatim doc-line text (read fresh — file may have shifted) asold_stringand the substituted text asnew_string. - Re-read the file before each Edit to defend against line-number
drift; if the verbatim text from the report no longer appears in
the file, skip the item with a note
--apply-trivial: skipping <file>:<line> (text drifted between scan and apply).
For each [DOC FIX] line WITHOUT the [TRIVIAL] qualifier, leave
it in the queue for Step 4. Emit one summary line per skipped item:
--apply-trivial: skipping <file>:<line> (no TRIVIAL tag — requires human review).
Apply-step output
Print a ## Auto-applied section listing every change made:
## Auto-applied
[AUTOFIX:bd-hooks] @ <root>
- ran `bd hooks install` → wrote .beads/hooks/pre-commit + post-commit
- absorbed export queue: 1 commit `bd: post-install export sync`
[AUTOFIX:workflow-json] @ <root>/.claude/workflow.json
- wrote {"v":1,"mode":"full"}
[AUTOFIX:gitignore-worktrees] @ <root>/.gitignore
- appended `.claude/worktrees/`
- appended `.claude/workflow-state.json`
[AUTOFIX:loom-env-block] @ <root>/.claude/settings.json
- backed up to settings.json.pre-loom.bak
- merged env block: CLAUDE_CODE_ENABLE_TASKS=false (added),
CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 (added)
[DOC FIX][TRIVIAL] cardinality @ README.md:42
- s/(105 dirs)/(106 dirs)/
[DOC FIX][TRIVIAL] cardinality @ docs/index.md:78
- s/Prelude (4)/Prelude (5)/
(N items skipped — see "--apply-* skipping" notes above)
What this step does NOT do
- Does not commit. Git is left in a dirty state for the user to
review with
git diff/git statusand commit (or revert) themselves. The[AUTOFIX:bd-hooks]recipe is the one exception — it MUST commit the absorbing commit because the bd hook needs to fire once on a clean queue before the user's first logical commit (loom-cka). That commit is intentional, scoped, and message-tagged. - Does not run the project's test suite. The user verifies post- apply.
- Does not retry on failure. A failed Edit / Bash / Write step emits one error line and continues to the next item; the per-item approval queue in Step 4 still has the failed items for manual handling.
- Does not touch WARN items. Onboarding WARNs (item 1 dirty tree, item 4 malformed workflow.json, etc.) imply real conflict — apply flags never auto-resolve them.
- Does not draft or apply
.claude/rules/CONTENT (loom-d50 — HARD EXCLUSION). Item 7's.claude/rules/<x>.mdMISS carries NO[AUTOFIX:...]tag, so this walk never processes it. Even outside the apply walk — in the Step 4 per-item gate — the rules-file fix is scaffold-stub-or-suggest ONLY: the skill may write an EMPTY<root>/.claude/rules/<x>.mdwhose body is a single> [HUMAN AUTHOR] TODO: author the <x> convention here.placeholder (mirroring the constitution prose-body stub, Step 7d), or it may simply SUGGEST the file in the report — but it NEVER auto-drafts and NEVER auto-applies AUTHORED rule content. Rule text encodes project conventions; a human authors those. This is the loom-d50 lesson from the loom-wxo liza_base trial (2026-05-04), where the audit silently drafted+applied.claude/rules/tests.mdcontent — convention- encoding text the human never wrote. The scaffold stub is the only thing the skill writes for a rules gap; the authored content stays a human-authored MISS.
Step 4 — present combined report + drive interactive fixes
Produce one combined report:
# Project audit: <project-short-name>
## Pre-flight warnings
<[WING WARN] line from Step 1b, if any; omitted when wing was
explicit or when no basename-variant has more drawers>
## Onboarding
<verbatim project-onboarder report, if run>
## Docs drift detection
<list of [DOC FIX] lines, if run; "no drift detected" otherwise>
## Auto-applied
<output of Step 3.5, if --apply-trivial and/or --apply-onboarding
fired and any items applied; omitted otherwise>
## Summary
PASS: <N> · WARN: <N> · MISS: <N> · [DOC FIX]: <N>
auto-applied: <K> · skipped (untagged): <S>
Top 3 gaps to fix first: <ordered short list>
For each non-auto-applied gap, ask the user:
Item:
. Apply suggested fix? (yes / skip / edit)
Invariant (loom-xcw): the per-item gate is a conversational pause, not a tool-permission prompt. Two distinct gates can be confused here:
- TOOL permission — Claude Code's built-in prompt before
Write/Edit/Bash.
--dangerously-skip-permissionssilently auto-accepts this gate. It is about which tools the harness is allowed to invoke, not about whether the USER approved the change. - USER approval — the per-item question above. This is a real
conversational pause that requires a user-typed reply ("yes",
"skip", or "edit").
--dangerously-skip-permissionsMUST NOT auto-resolve this gate; running with it does NOT imply blanket user consent.
The two gates are NOT interchangeable. A session with
--dangerously-skip-permissions set still owes the user an explicit
yes/skip/edit reply per item — the flag only removes the
tool-permission friction layer, never the user-approval layer.
Execution rule. After printing the prompt, STOP. Do NOT call
any tool (no Edit, no Write, no Bash, no further analysis) until
the user replies with a message containing one of yes / skip /
edit. Treat the next user message as the answer; if the user's
reply is ambiguous, re-prompt rather than guessing. This is the
fix for the loom-wxo / loom-xcw symptom where three items applied
without an intervening user turn.
On yes: generate the fix (template for onboarding gaps; surgical
edit for docs drift), preview the diff, then write to disk.
On skip: move on. On edit: ask the user for the corrected text
and use that.
Never auto-apply a fix outside --apply-trivial / --apply-onboarding
scope. The skill is a co-pilot for cleanup, not an autonomous editor.
Step 5 — capture findings to <wing>/decisions
When the audit produces non-trivial findings (any WARN / MISS /
[DOC FIX] / [DOC SKIP] lines), file a single drawer summarising
the audit. The drawer always goes to <wing>/decisions.
<wing> is the resolved wing from Step 1 (the --wing value).
This is hardcoded — the skill does not create per-audit rooms
(audit_results, findings, gaps, etc.). Every loom-managed
project's MemPalace wing carries a decisions room by convention;
audit findings ARE decisions about project state, so they live
there alongside design decisions. (loom-lpy.)
Drawer shape:
- Title:
Project audit: <project-short-name> (<YYYY-MM-DD>). The "this is an audit" semantic is carried by the title, not by the room name. - Content: the combined report from Step 4 verbatim, plus a short "what to do next" section listing the top gaps by severity and any beads filed against them.
- Wing/Room:
<wing>/decisions(hardcoded).
If the MemPalace stop-hook auto-files the audit findings as part of
session checkpointing, the auto-file MUST honour the same
destination — do not let the hook create a separate
<wing>/audit_results or <wing>/findings room. If the hook
defaults elsewhere, override with an explicit
mempalace_add_drawer(wing=<wing>, room='decisions', ...) call
before the hook fires.
Migration of pre-loom-lpy drawers (e.g. drawers in
tla_puzzles/audit_results and tla_puzzles/findings from the
loom-b6o trial) is out of scope for this skill — those remain as
historical artifacts. The new convention applies to all future
audits.
Step 6 — optional history mining (--mine-history)
This step runs only when --mine-history was passed. Without the
flag, skip it entirely — the audit has already flagged the
decision-history gap informationally (the project-onboarder
decision-history line, which shells out to ~/.claude/scripts/loom-mine-history --dry-run for the unmined-unit count). Mining is a separate, billable
action the user opts into; the audit never auto-mines.
When the flag IS set, after the report + interactive fixes are done,
delegate to the /loom-mine-history skill against the resolved
--root / --wing:
- Announce: "Mining
decision history into wing <wing>…". - Invoke the
loom-mine-historyskill (it owns the mandatory two-pass cost gate: a zero-spend--dry-runpreview → explicit user go-ahead → the paid LLM salience pass → MCP filing). Pass the resolved--rootand--wingthrough; do NOT re-implement the engine or the cost gate here. - Fold the mine's adoption summary (drawers filed / skipped-dup / triples added) into the audit's closing summary.
Do not bypass loom-mine-history's cost gate — the audit delegating
to it does not change the "preview-before-spend, explicit go-ahead"
contract.
Step 7 — project-constitution capture (--check=constitution)
This step runs only when --check=constitution was passed (it is
NOT part of --check=all). It is the capture half of the loom-6f8
Constitution epic (loom-1iz). The schema, the fillable template, the
dogfooded loom sample, and the field reference are loom-vin artifacts:
- Schema:
references/project-constitution.schema.json - Template:
templates/project-constitution.md - Dogfood: loom's own
.claude/project-constitution.md - Reference:
docs/reference/project-constitution.md
The output is one file per project at
<root>/.claude/project-constitution.md — YAML front-matter (the
machine-read tooling fingerprint) plus a Markdown prose body (human
rationale). This step writes the front-matter from detected signals
and stubs the prose body for a human to author; it never authors the
prose itself (loom-d50).
Step 7a — detect the tooling fingerprint
Dispatch the project-onboarder subagent (or, if it was already
dispatched in Step 2, reuse its fingerprint section) to scan
<root> and return a tooling fingerprint. The onboarder is
read-only — it reports the fingerprint; this skill owns every write,
the per-field confirmation, and the MemPalace mirror.
The detection heuristics (all filesystem-marker based, relative to
<root>), in the order they resolve each field:
shell—<root>/devbox.jsonpresent →shell.enter: "devbox shell",shell.run_prefix: "devbox run". Else<root>/flake.nixpresent →shell.enter: "nix-shell",shell.run_prefix: "nix-shell --run". Else both empty (no shell wrapper).devbox.jsonwins overflake.nixwhen both are present (devbox is the outer envelope).package_manager— first decisive lockfile / manifest wins, in this precedence:<root>/pnpm-lock.yaml→pnpm;<root>/yarn.lock→yarn;<root>/package-lock.json→npm;<root>/uv.lock→uv;<root>/poetry.lock→poetry;<root>/Cargo.toml→cargo;<root>/go.mod→go. None present →none.language.runtime—<root>/Cargo.toml→rust;<root>/go.mod→go; any ofpyproject.toml/setup.py/setup.cfg/requirements*.txt→python;<root>/package.json(with a non-nonepackage_manager) →node; a<root>/scripts/directory containing*.shand no other language marker →bash; otherwiseunknown(polyglot or undetected — never guess).language.versionis left EMPTY (version pins are a human choice, not a filesystem signal).canonical_commands—<root>/Makefilewith abuild:/test:/lint:target →make build/make test/make lintfor those verbs. For any verb the Makefile does not cover (and for all five verbs when there is no Makefile), an executable<root>/scripts/<verb>fills it:scripts/build→ build,scripts/test→ test,scripts/lint→ lint,scripts/gen→ gen,scripts/server→ dev. The Makefile target wins over the script for the same verb. Verbs with neither signal stay EMPTY.forbidden/bypass_patterns— NOT auto-detected. These encode a project-specific lock-in posture (e.g. forbidpip installon a uv project) that is a human judgment, not a filesystem signal. They are rendered as empty lists in the draft for the human to fill.
Empty fields stay empty. The detector never invents a value it
could not read from a marker — an undetected verb, an unpinned
version, an absent shell wrapper all render as "" (or [] for the
lists). This is the same discipline as the loom-vin template: leave
keys present with empty values rather than guessing.
Step 7b — render the draft front-matter
Render the detected fingerprint into the YAML front-matter shape from
templates/project-constitution.md (and validated by
references/project-constitution.schema.json). Every required key is
present; detected fields carry their value; undetected fields carry
"" / []. Do NOT write the file yet — Step 7c confirms each field
first.
Step 7c — per-field interactive confirmation (one field at a time)
Invariant (loom-xcw): confirm ONE field at a time — never
lump-sum. Walk the front-matter fields in schema order
(shell.enter, shell.run_prefix, package_manager,
language.runtime, language.version, each canonical_commands.*
verb, forbidden, bypass_patterns). For EACH field, show the
detected value and ask the user to confirm, edit, or clear it:
Field `<name>`: detected `<value>` (from `<marker>`).
Keep / edit / clear? (keep / <new value> / clear)
After printing each field's prompt, STOP and wait for a user-typed
reply before moving to the next field. Do NOT batch all fields into
one prompt and accept a single lump-sum approval — that is exactly
the loom-xcw / loom-wxo failure mode (multiple items applied without
an intervening user turn). This is a USER-approval gate (a
conversational pause), distinct from the TOOL-permission gate;
--dangerously-skip-permissions does NOT auto-resolve it.
Test mocking surface: the LOOM_AUDIT_PROMPT_ANSWER env var (same
surface as items 13/14) injects per-field answers non-interactively
for fixtures.
Step 7d — write the file UNSTAGED + stub the prose body
After every field is confirmed, write
<root>/.claude/project-constitution.md:
- The confirmed YAML front-matter.
- The Markdown prose body emitted as a
[HUMAN AUTHOR]TODO stub — section headers (## Tooling choices,## Forbidden patterns,## Bypass patterns,## Lineage) each carrying a> [HUMAN AUTHOR] TODO: …placeholder line. The skill NEVER authors the prose body itself — this is the loom-d50 lesson: in the loom-wxo liza_base trial the audit silently drafted+applied.claude/rules/tests.mdcontent (project conventions) without human authorship; the constitution prose is the same class of convention-encoding text and MUST stay a human-authored MISS, not an agent draft. The agent fills the machine-read front-matter; the human fills the prose.
The file is written UNSTAGED — the skill does not git add it.
The user reviews with git diff, authors the prose body, and commits
when ready. (Same posture as the Step 3.5 AUTOFIX recipes: write to
the working tree, leave git dirty, never commit on the user's behalf.)
Step 7e — mirror to MemPalace + emit KG triples
Mirror the captured constitution to a single drawer in the
<wing>/decisions room (the resolved --wing from Step 1 — same
hardcoded destination as the Step 5 audit-findings drawer):
mempalace_add_drawer(wing=<wing>, room='decisions', title='Project constitution: <project-short-name> (<YYYY-MM-DD>)', content=<the confirmed front-matter + the field-by-field detection provenance>).
Then emit KG triples for the tooling so the fingerprint is queryable
(via mempalace_kg_add):
<project> uses_shell <shell.enter>(omit when no shell wrapper)<project> uses_package_manager <package_manager><project> uses_language <language.runtime>
These triples let session-startup, subagent-dispatch briefs, and the debugging recipes (loom-ld4 surfacing) query a project's tooling fingerprint without re-reading the file.
Step 7f — re-run drift detection (idempotent)
When <root>/.claude/project-constitution.md already exists, Step 7
becomes a drift check rather than a fresh capture:
Parse the captured front-matter.
Re-run the Step 7a detection against the current tree.
For each field where the detected value differs from the captured value, surface the drift per field:
[CONSTITUTION DRIFT] <field> captured: <value in the file> detected: <value from the current tree> suggested: confirm / skip (per-field — same one-at-a-time gate as Step 7c)The drift loop reuses the Step 7c one-field-at-a-time confirmation — the user confirms or skips each drifted field. Only the front-matter is rewritten, and only for confirmed fields.
The prose body is NEVER overwritten on re-run. Detection is read-only against the prose; the drift check rewrites front-matter fields the user confirms and leaves the entire Markdown body (including any human-authored rationale) untouched. A re-run that finds no front-matter drift is a no-op that does not modify the file at all.
Step 8 — tree-sitter grammar check (--check=tree-sitter; also folded into the onboarding scan)
This check catches a silent, drift-created gap in projects that ship a tree-sitter grammar. It runs in two places:
- On
--check=tree-sitterit runs in ISOLATION (neither the onboarding scan nor the docs check fires). - On
--check=onboarding|allit runs as part of the project-onboarder scan (item 22) so a default audit surfaces it without a dedicated flag.
Either way the detection logic, verdict, and recipe-only fix are identical. The skill (this file) owns the rendered report line + the fix-recipe text; the onboarder (item 22) owns the read-only detection when the check runs inside the onboarding scan.
The gap
tree-sitter generate against a grammar with no tree-sitter.json
sibling prints:
Warning: No tree-sitter.json file found in your grammar, this file is
required to generate with ABI 15. Using ABI version 14 instead.
tree-sitter 0.25+ (current default ABI 15) wants a tree-sitter.json
sibling to grammar.js. Older grammar repos (pre-0.25) work fine
without it but quietly fall back to ABI 14. A tree-sitter upgrade in
nixpkgs / homebrew silently degrades old grammar repos — exactly the
drift-over-time class /audit-project exists to surface. Same shape
as the docs-scaffold gaps (loom-ad1, loom-tww, umbrella loom-vca) — an
upstream tool's template/init defaults don't pass through to a real
publish — but a DIFFERENT upstream (tree-sitter, not mkdocs-material).
Detection
Find any directory under
<root>containing agrammar.jsfile (typicallytree-sitter-*subdirs, but a few projects use other naming — thegrammar.jsmarker drives detection, not the directory name). Use:find <root> -type f -name 'grammar.js'For each such grammar directory, check whether
tree-sitter.jsonis a sibling (<grammar-dir>/tree-sitter.json).Absent →
WARN. Present →OK.
A project with no grammar.js anywhere has nothing to check — the
check is a silent PASS (no WARN, no OK lines emitted).
Verdict + report line
For each grammar directory missing the sibling, emit:
[TREE-SITTER WARN] <grammar-dir> has grammar.js but no tree-sitter.json
reality: tree-sitter 0.25+ (ABI 15) requires tree-sitter.json;
`tree-sitter generate` here silently falls back to ABI 14
suggested: cd <grammar-dir> && tree-sitter init -p . (scaffolds
tree-sitter.json from the existing package.json
[tree-sitter] block / interactively), OR hand-write
tree-sitter.json mirroring package.json's [tree-sitter]
block. Schema: https://tree-sitter.github.io/tree-sitter/cli#init
Grammar directories that already carry the sibling render as OK and
are not surfaced as a gap.
Recipe-only — tree-sitter init is NEVER auto-run
tree-sitter init requires a TTY (it is interactive — it cannot run
non-interactively inside the audit), so the fix is recipe-only: the
check prints the recipe and the user runs it in their own terminal.
The audit MUST NOT attempt to auto-run tree-sitter init, not even
under --apply-onboarding — there is no AUTOFIX tag for this check.
This mirrors the item-18 gh auth login handoff: an interactive tool
the audit surfaces but never drives.
Out of scope
Validating the tree-sitter.json schema beyond presence — tree-sitter generate itself validates the contents; the audit only checks that the
file exists. Auto-running tree-sitter init — recipe-only, never
auto-run (needs a TTY).
Lineage: loom-qvs (surfaced 2026-05-24 by mforth; downstream fix
mforth commit 216f482, which hand-wrote tree-sitter.json mirroring
the existing package.json [tree-sitter] block).
Step 9 — script/ convention check + scaffold + .deploy migration (folded into the onboarding scan)
This check recognizes the loom script/ convention (GitHub
"scripts to rule them all" lineage, locked in the loom-adm
script/-convention decision drawer; canonical skeleton shipped by
loom-oxs.1 at templates/scripts/), surfaces missing/half-wired
canonical scripts, OFFERS to scaffold the skeleton from
templates/scripts/, and OFFERS the workflow.json .deploy →
canonical_commands.deploy migration. It runs as part of the
project-onboarder scan (item 23) on --check=onboarding|all, so a
default audit surfaces it without a dedicated flag.
The onboarder (item 23) owns the read-only detection; the skill (this
file) owns the rendered report lines AND the interactive offers + the
writes. There is no AUTOFIX tag — both fixes are interactive
(per-file scaffold y/N; per-candidate .deploy migration y/N/skip),
mirroring the items 13/14/21 conversational gates rather than the
deterministic Step 3.5 AUTOFIX recipes.
The 8 canonical scripts
bootstrap setup update server test lint cibuild deploy.
The convention pipeline: bootstrap → setup → (update on later
pulls) → server / test / lint during development → cibuild in
CI → deploy to ship. templates/scripts/ is the source-of-truth
skeleton; each script ships as an unedited exit 2 "not implemented"
stub that the adopter wires up (or marks N/A with exit 0).
Directory recognizer — BOTH script/ and scripts/ accepted
A project "has the script/ convention" iff it carries a script/
(singular — the canonical GitHub spelling and the loom default) OR a
scripts/ (plural — tolerated for projects that already use that
name) directory. The recognizer probes <root>/script/ first, then
<root>/scripts/; the first present dir is the recognized convention
dir. EITHER name is recognized — they are treated equivalently;
new projects should prefer the singular script/. Neither present →
the convention is not recognized (no canonical scripts to compare
against, so the skill emits the whole-skeleton scaffold offer instead
of per-script gaps).
Per-script gap surfacing — PASS / WARN / MISS
For each of the 8 canonical scripts under the recognized dir:
- PASS = exists AND executable AND wired (NOT the unedited
exit 2"not implemented" stub). - WARN = exists but non-executable, OR still the unedited
exit 2stub (present-but-not-ready — a half-wired script must not look healthy). - MISS = absent.
Report line shape:
[SCRIPT] <dir>/ convention recognized (dir: script|scripts)
PASS: setup test lint
WARN: server (still the exit-2 stub), deploy (non-executable)
MISS: bootstrap update cibuild
suggested: scaffold the 3 MISS scripts from templates/scripts/ (per-file y/N)
When NO script/ or scripts/ dir exists, emit a single line instead
of 8 MISS lines:
[SCRIPT] no script/ convention dir — offer to scaffold the canonical
8-script skeleton (bootstrap setup update server test lint cibuild
deploy) from templates/scripts/ into <root>/script/
Scaffold offer (from templates/scripts/)
When ≥1 canonical script is MISS (or the whole dir is absent), the
skill OFFERS to scaffold from templates/scripts/. The offer is a
per-file conversational gate (the same pause-and-wait contract as the
Step 4 per-item gate — present, then STOP and wait for the user's
typed reply; do NOT call any tool until the user replies). On y for a
given script, copy templates/scripts/<s> into <root>/<dir>/<s> and
chmod +x it; on N, leave it. The adopter then wires each scaffolded
stub up (uncomment the per-type comment hint, replace the exit 2
body) or marks it N/A (exit 0) per the templates/scripts/README.md
adoption guide. On a literal skip, write a script-convention skip
memo into <root>/.claude/loom-audit-state.json so the row renders as
a silent PASS on future runs.
The scaffold copies the templates verbatim as exit 2 stubs — the
audit never wires a script to a real command (that is the adopter's
edit, and auto-wiring would re-import the design→build mismatch the
stub-default exists to prevent). LOOM_AUDIT_PROMPT_ANSWER injects the
answer non-interactively under tests (same mocking surface as items
13/14/21).
.deploy → canonical_commands.deploy migration offer
workflow.json's .deploy (loom-0k0) and the constitution's
canonical_commands.deploy (loom-oxs.3) are two homes for the same
fact: the project's deploy command. .deploy is the legacy wrap-up
hint; canonical_commands.deploy is the constitution-schema field that
script/deploy resolves through (lib/loom-script-resolve.sh's
loom_resolve_command deploy). When a project carries the legacy
.deploy but has not set canonical_commands.deploy, the audit OFFERS
to migrate the value forward.
Detection (the onboarder reports candidacy; the skill drives the offer):
- Read
<root>/.claude/workflow.json.deployviaworkflow_resolve_deploy(lib/workflow-config.sh). - Read
<root>/.claude/project-constitution.md'scanonical_commands.deploy. - MIGRATE candidate =
.deployis a non-empty string ANDcanonical_commands.deployis empty/unset. - NOOP = both set (already migrated), or
.deployis empty (nothing to migrate), or no constitution file exists.
When a MIGRATE candidate is found, the skill OFFERS a y/N/skip gate:
[SCRIPT] .deploy migration available
workflow.json .deploy: "<cmd>"
canonical_commands.deploy: (empty)
suggested: migrate the .deploy value into canonical_commands.deploy
so script/deploy and the constitution agree. (y/N/skip)
On y, write <cmd> into the constitution's
canonical_commands.deploy field (preserving the rest of the
front-matter + the prose body untouched). On N, leave the row in the
queue. On skip, write the script-convention skip memo. The offer is
suggest-only — never write without explicit per-item approval, and
never overwrite a non-empty canonical_commands.deploy.
No AUTOFIX tag — interactive only
Neither the scaffold nor the .deploy migration is AUTOFIX-tagged: the
scaffold is a per-file user choice (which scripts to bring in), and the
migration copies a user-authored command into a second home. Both stay
in the per-item conversational gate. The --apply-onboarding flag does
NOT auto-apply this check's offers — they require a typed reply.
Lineage: loom-oxs.4 (2026-06-09), umbrella epic loom-oxs (the script/
convention). Canonical skeleton: loom-oxs.1 (templates/scripts/).
Resolver: loom-oxs.2 (lib/loom-script-resolve.sh,
loom_resolve_command). canonical_commands.deploy schema field:
loom-oxs.3. .deploy lineage: loom-0k0. Design: the loom-adm
script/-convention decision drawer.
Output format (drift items)
One line per drift item, prefix-tagged. Concrete examples:
[DOC FIX] cardinality
doc: docs/reference/manual.md:432 "All three commands have disable-model-invocation"
reality: 6 commands match commands/*.md; 6 of 6 have the flag
suggested: s/All three/All six/
[DOC FIX] dead-bead-id
doc: docs/explanation/provenance.md:117 cites `loom-xyz`
reality: bd show loom-xyz → not found (no superseded-by metadata)
suggested: remove citation, or file a bead to recreate the lineage
[DOC FIX] missing-primitive
doc: docs/reference/installed-files.md:40 lists `skills/audit-project/SKILL.md`
reality: skills/audit-project/SKILL.md does not exist
suggested: ship the SKILL.md (file a bead) OR remove the line
[DOC FIX] catalog-ghost
doc: docs/reference/skills/index.md row "audit-project"
reality: skills/audit-project/SKILL.md does not exist
suggested: remove the row, or ship the SKILL.md
[DOC FIX] missing-drawer-citation
doc: docs/explanation/recipe-family.md:23 cites drawer "RECIPE SHAPES — ACTIVITY MATRIX"
reality: mempalace_search returned no strong match
suggested: fix the slug, or capture the drawer if the design is unrecorded
[DOC SKIP][GENERATED] docs/ is generated — skipping cardinality (docs/ scope)
reason: Signal 1: docs/ matches .gitignore entry 'docs/' in .gitignore
pointer: edit the source named in the build script, not docs/
(root README.md remains in scope and is checked normally)
What this skill does NOT do
- Does not write to disk without user approval (except for
--apply-trivial/--apply-onboardingitems where the user has pre-authorized the AUTOFIX-tagged class by passing the flag). - Does not run
bd init(interactive — requires the user to acknowledge the workspace prompt). Even with--apply-onboarding, item 2 MISS stays in the per-item queue. - Does not write to MemPalace. Even with
--apply-onboarding, item 5 MISS (no project-named wing) stays in the per-item queue — wing creation is a per-user MCP-server operation, not a script recipe. - Does NOT run
bd hooks installor writeworkflow.json/.gitignorewithout--apply-onboarding. When the flag is set, the AUTOFIX recipes in Step 3.5 do these writes; without it, the skill emits the suggested-fix line and waits for per-item approval. - Does not perform semantic claim extraction in v1. "X does Y" claims with verb-level disagreement (the doc says "fires on X" but the hook fires on Y) are out of scope. v2 may add LLM- assisted claim extraction; v1 is grep + filesystem + bd + MCP resolution.
- Does not modify beads or MemPalace state. Read-only against
bd showandmempalace_get_drawer/mempalace_search. - Does not exceed the report cap of 250 onboarding lines + 250
drift lines. If drift output would exceed the cap, emit the top
250 by sort order (file path, then line) and a final line
[DOC FIX] truncated · <K> more · re-run with --check=docs --full-output for everything.
Why this exists
The v1 onboarding scan caught wiring gaps but assumed docs/ was
truthful. Two bug classes proved that assumption wrong:
- loom-469 (cardinality drift) — the manual claimed "All three
commands have
disable-model-invocation" while six commands shipped. The claim was true at write time and silently false three commits later. - loom-qj3 (lying-doc) — docs cited a
/feature-a-beadcommand that didn't exist; cited primitives that had been renamed; cited bead IDs that no longer resolved. Each individual drift was small, but they accumulated faster than humans noticed during review.
The docs check exists because the human review pass at PR time is the wrong layer to catch this kind of drift — it's mechanical and should be mechanized. Running it at audit time, default-on for projects with a Diataxis substrate, surfaces drift at the moment when there's budget to fix it.
The precedence rule (docs lose) is what makes the check tractable.
If docs and reality disagreed in either direction the check would
need a tie-breaker for every item; with the rule fixed in advance,
every drift item is a doc fix and the check can run unattended for
trivial cases.
Related infrastructure
- Slash command:
commands/audit-project.md— manual-only entry point withdisable-model-invocation: true. The slash command forwards--root,--wing,--check,--apply-trivial,--apply-onboarding, and--workflow-modeto this skill. - Subagent:
agents/project-onboarder.md— read-only scanner the skill dispatches in step 2. The subagent already takes the project root + short name as inputs, so portability flows through unchanged. - Companion how-to:
docs/how-to/where-to-update-what.md— when a human is the one fixing a drift item, this is the page that tells them which surface to update. - Locked design lives in MemPalace:
loom/decisionswing — drawer for the loom-9z1 epic (Diataxis docs restructure + drift defenses) plus drawerdrawer_loom_decisions_63aadc6e849779a509678d90(loom-9z1.10 D1 plan §A.4 — the portability spec implemented by loom-km8.4).