plugins/pronto/skills/audit/SKILL.md
Run pronto's composite readiness audit — walks the rubric, delegates per-dimension to installed sibling plugins, falls back to kernel presence checks, emits a composite scorecard
npx skillsauth add acostanzo/quickstop auditInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are the Pronto audit orchestrator. When the user runs /pronto:audit or /pronto:audit --json, walk the readiness rubric, delegate depth scoring to installed sibling plugins, fall back to kernel presence checks when siblings are absent, and emit a composite scorecard.
This skill is pure orchestration: it owns none of the depth analysis. Kernel presence is delegated to /pronto:kernel-check; sibling-specific scoring is delegated to each sibling's native audit or a per-sibling parser agent.
Parse $ARGUMENTS:
--json → OUTPUT_MODE = "json".git rev-parse --show-toplevel 2>/dev/null. If this fails, tell the user the audit must run inside a git repo and stop.date -u +"%Y-%m-%dT%H:%M:%SZ" — ISO 8601 UTC.${CLAUDE_PLUGIN_ROOT} — pronto's own root, for reading references.jq -r '.version' "${PLUGIN_ROOT}/.claude-plugin/plugin.json" — pronto's running version, used for the ADR-004 §2 sibling handshake in Phase 4.${PLUGIN_ROOT}/references/rubric.md — used for weights, presence-check definitions, letter-grade bands.${PLUGIN_ROOT}/references/recommendations.json — used to map each dimension to its recommended sibling plugin and install command.${PLUGIN_ROOT}/references/report-format.md — used for the output shape.Discovery is a single deterministic shell call — there is no LLM-controlled enumeration in this phase. Invoke:
"${PLUGIN_ROOT}/skills/audit/discover-siblings.sh" "${PLUGIN_ROOT}"
The helper walks the parent directory of pronto's plugin root and emits a JSON array of every co-located plugin (one record per plugin), each carrying { name, plugin_root, version, compatible_pronto, native_declarations }. native_declarations is the plugin's pronto.audits array (empty [] when absent — siblings without that block are still emitted so parser-agent dispatch via Sub-path B in Phase 4.1 can reach them through recommendations.json).
Capture the helper's stdout and parse it as the INSTALLED_SIBLINGS map (keyed by .name). The bash variable holds the raw JSON; the orchestrator looks plugins up by name (e.g., INSTALLED_SIBLINGS[claudit].compatible_pronto).
Why parent-walk, not installed_plugins.json or the audit-target's marketplace.json. When pronto runs in a Claude Code session, ${CLAUDE_PLUGIN_ROOT} resolves to the directory it was loaded from — ~/.claude/plugins/pronto@<source>/ for /plugin install, or <repo>/plugins/pronto/ for --plugin-dir. In both layouts, sibling plugins sit one directory up. Walking the parent captures both cases and is the only source that reflects what is actually loaded into this session — installed_plugins.json misses --plugin-dir loads, and the audit-target's marketplace.json describes what plugins the target expects to be available, not what is loaded into pronto's session. The historical Phase 2 sources confused "what the audit target's marketplace says exists" with "what pronto can dispatch to in this session"; they diverge whenever the audit runs against a fixture or any tree whose own plugins/pronto/ predates the current sibling set (see project/tickets/closed/phase-2-followup-eval-harness-staleness.md).
The audit does not dispatch /claudit:knowledge. Expert ecosystem context is not part of the audit path.
Pronto's audit scope is deterministic scoring — kernel presence, sibling wire-contract scores, and per-dimension rubric evaluation. /claudit:knowledge emits narrative research output, not structured scores; folding it into the audit contributed to non-deterministic composites. It was removed from the audit flow deliberately.
Users who want expert ecosystem context invoke /claudit:knowledge ecosystem directly. That skill is first-class user-facing surface, not a pronto dependency. Its absence is not a degradation — do not surface it in sibling_integration_notes, and do not treat it as a missing capability anywhere in the report.
Parsers receive only the dispatch brief defined in Phase 4.1. There is no EXPERT_CONTEXT payload.
Invoke the kernel-check skill for the repo's baseline:
Invoke `/pronto:kernel-check --json` via Bash or skill invocation.
Capture stdout as KERNEL_JSON. Parse as JSON.
If parsing fails, note the failure in sibling_integration_notes and treat all kernel categories as fail (score 0) for this run. Do not traceback.
Extract the kernel's categories[] and build a map from category name to score:
KERNEL_CATEGORY_SCORES = {
"AGENTS.md scaffold": <0 or 100>,
"Project record container": <0 or 100>,
"Tool-state (.pronto/)": <0 or 100>,
".claude/ presence": <0 or 100>,
"README": <0 or 100>,
"LICENSE": <0 or 100>,
".gitignore": <0 or 100>
}
Walk every dimension in the rubric (8 rows in Phase 1). For each, resolve the score via the following decision tree:
kernel-owned.KERNEL_CATEGORY_SCORES["AGENTS.md scaffold"] (0 or 100, no cap — this dimension is always kernel-driven).If avanti is in INSTALLED_SIBLINGS and declares project-record natively, the dispatch is a sibling dispatch and must go through the same version handshake as the other sibling-owned dimensions below. Run compatible-pronto-check.sh against INSTALLED_SIBLINGS[avanti].compatible_pronto first; only proceed to sibling dispatch on in_range or unset (apply the same notes per branch as in "Other dimensions" step 2). On out_of_range or malformed, skip the sibling dispatch and fall back to the kernel-presence-cap path below. Source on successful sibling dispatch: sibling.
Sub-path selection (handshake passed). Consult recommendations.json for the project-record dimension's parser_agent field:
parser_agent is set (e.g. parsers/avanti) → prefer this path: invoke the deterministic scorer script directly via Bash per Phase 4.1 below. Direct-shell dispatch removes the LLM-controlled instruction-following step from the score path entirely — the script owns every scoring decision and runs byte-deterministically. Score: scorer's composite_score./avanti:audit --json) via the SlashCommand tool, bind its stdout into a per-dimension local variable, parse as JSON, validate against the contract. Score: composite_score.Isolation invariant. Whichever sub-path the handshake selects, avanti's sub-audit JSON is a bound value used in Phase 5 aggregation — it is never pronto's stdout. The sibling sub-audit JSON has shape {plugin, dimension, categories[], letter_grade, ...} (per references/sibling-audit-contract.md); pronto's composite envelope has shape {schema_version, repo, composite_score, dimensions[], ...} (per references/report-format.md). If you ever find yourself about to write the captured sibling JSON to pronto's stdout, you are emitting the wrong object — see the Phase 6 sentinel for the runtime backstop.
Else (avanti absent, or handshake forced a skip) → use KERNEL_CATEGORY_SCORES["Project record container"]; cap at 50 if 100 (present), 0 if 0 (absent). Source: kernel-presence-cap or presence-fail. The per-dimension notes field must reflect which fallback path was taken — apply the same templates as "Other dimensions" step 4, substituting avanti for <plugin>:
"avanti not installed; presence check passed; capped at 50" (or "...presence check failed; score 0").out_of_range: "avanti <version> installed but compatible_pronto excludes pronto <PRONTO_VERSION>; sibling audit skipped; presence-only."malformed: "avanti <version> installed but compatible_pronto '<range>' is unparseable; sibling audit skipped; presence-only."Otherwise the row's notes will contradict the hard entry in sibling_integration_notes.
For each of claude-code-config, skills-quality, commit-hygiene, code-documentation, lint-posture, event-emission:
Look up recommendation: from recommendations.json, get recommended_plugin, audit_command, parser_agent.
Version handshake (per ADR-004 §2): if the recommended plugin is in INSTALLED_SIBLINGS, gate dispatch on its compatible_pronto declaration. Invoke:
"${PLUGIN_ROOT}/skills/audit/compatible-pronto-check.sh" \
"${PRONTO_VERSION}" \
"${INSTALLED_SIBLINGS[<plugin>].compatible_pronto}"
Parse the helper's JSON output and branch on .branch:
in_range → continue to step 3 (dispatch normally), no note.unset → continue to step 3 (dispatch normally) AND append a soft note to sibling_integration_notes: "<plugin> does not declare compatible_pronto; dispatching at sibling's risk per ADR-004 §2."out_of_range → skip dispatch entirely, fall through to step 4 (presence fallback) for this dimension, AND append a hard note to sibling_integration_notes of the form: "<plugin> <version> declares compatible_pronto '<range>' but this pronto is <PRONTO_VERSION>. Sibling audit skipped; upgrade <plugin> to re-enable depth scoring." Take <version> and <range> from INSTALLED_SIBLINGS[<plugin>]. The fallback's source stays kernel-presence-cap / presence-fail per the existing report-format contract; consumers correlate the skipped sibling via this entry plus the per-dimension notes template in step 4.malformed → skip dispatch entirely (same handling as out_of_range), fall through to step 4, AND append a hard note to sibling_integration_notes: "<plugin> <version> declares compatible_pronto '<range>' which is not parseable per ADR-004 §2 (must be space-separated <op>MAJOR.MINOR.PATCH clauses; ops: >= <= > < =). Sibling audit skipped; fix the sibling's plugin.json to re-enable depth scoring."If the helper itself exits non-zero (rc != 0), that is a pronto-side bug — pronto's own version is malformed, the call site forgot an argument, or there's an internal desync. Capture stderr, append it to sibling_integration_notes prefixed pronto bug: compatible-pronto-check.sh exited <rc>: <stderr>, treat the dimension as out_of_range for the rest of dispatch (skip sibling, fall through to step 4). The pronto bug is not a sibling problem — but masking it would be worse than a noisy log.
If the recommended plugin is NOT in INSTALLED_SIBLINGS, skip the handshake (no sibling means no declaration to check) and fall through to step 4 directly.
Sibling dispatch (only when handshake says in_range or unset):
If the plugin declares the dimension natively in its plugin.json pronto.audits block → invoke the declared command (a slash command, e.g. /skillet:audit --json) via the SlashCommand tool, bind its stdout into a per-dimension local variable, parse as JSON, validate against the contract. Source: sibling. Score: composite_score.
Isolation invariant. The captured JSON is a bound value used in Phase 5 aggregation. It is not pronto's stdout. Pronto's stdout is reserved for the composite envelope emitted in Phase 6, and only Phase 6 writes to it. A sibling sub-audit's JSON has shape {plugin, dimension, categories[], letter_grade, ...} (per references/sibling-audit-contract.md); pronto's composite envelope has shape {schema_version, repo, composite_score, dimensions[], ...} (per references/report-format.md). If you ever find yourself about to write the captured sibling JSON to pronto's stdout, you are emitting the wrong object — see the Phase 6 sentinel. The same isolation rule applies whether Sub-path A (this sub-bullet) or Sub-path B (parser dispatch, Phase 4.1 below) sourced the JSON.
Else if a parser_agent is registered for this dimension (e.g., parsers/claudit, parsers/skillet, parsers/commventional) → invoke the deterministic scorer script directly via Bash (see Phase 4.1 below). Source: sibling (the scorer is the sibling's audit in Phase 1). Score: scorer's composite_score.
Else → fall through to presence check.
Presence fallback (sibling not installed, handshake forced skip, OR no parser registered):
For claude-code-config and code-documentation, use the existing
kernel-check category score directly: KERNEL_CATEGORY_SCORES[".claude/ presence"]
and KERNEL_CATEGORY_SCORES["README"] respectively. No additional Bash needed.
For skills-quality, commit-hygiene, lint-posture, and
event-emission, invoke the deterministic presence check via Bash:
"${CLAUDE_PLUGIN_ROOT}/skills/audit/presence-check.sh" <dimension> "${REPO_ROOT}"
Where <dimension> is the slug literally (e.g. event-emission).
The script prints 100 on pass, 0 on fail — and nothing else.
Do not compose your own grep, find, or git invocation here;
the script exists precisely so the orchestrator never reinvents
the check (Phase 1.5 PR 3b: composing the grep with different
--exclude-dir sets across runs was the largest pre-mechanization
variance source).
If presence passes (100) → score 50, source kernel-presence-cap.
If presence fails (0) → score 0, source presence-fail.
The per-dimension notes field must reflect why the fallback ran, not a generic stub:
"<plugin> not installed; presence check passed; capped at 50" (or "...presence check failed; score 0").out_of_range): "<plugin> <version> installed but compatible_pronto excludes pronto <PRONTO_VERSION>; sibling audit skipped; presence-only."malformed): "<plugin> <version> installed but compatible_pronto '<range>' is unparseable; sibling audit skipped; presence-only."sibling_integration_notes.When the parser_agent field is set for a dimension, invoke the deterministic scorer script directly via the Bash tool. The scorer owns every scoring decision and runs byte-deterministically — there is no LLM step in the score path.
Invoke (substituting <sibling> from the parser_agent filename, e.g. parser_agent: parsers/claudit → <sibling> = claudit):
"${CLAUDE_PLUGIN_ROOT}/agents/parsers/scorers/score-<sibling>.sh" "${REPO_ROOT}"
Capture stdout into a per-dimension local variable.
Pipe the captured scorer JSON through the observations-aware translator before the score is folded into Phase 5. Invoke "${CLAUDE_PLUGIN_ROOT}/agents/parsers/scorers/observations-to-score.sh" <dimension-slug> <captured-json-path> — write the scorer's stdout to a temp file first, since the helper takes a path. The translator emits an envelope of shape {composite_score, observations_applied, passthrough_used, dropped}: take its composite_score as the dimension's score (overriding any composite_score the scorer itself emitted) and append every entry in its dropped[] to sibling_integration_notes as "<plugin>:<dimension>: dropped observation '<id>' (<reason>)". Translator exit 3 (stanza-loader rejects the rubric) is a pronto-side configuration bug, not a sibling problem; surface stderr verbatim in sibling_integration_notes prefixed pronto bug: observations-to-score.sh exited 3: <stderr> and degrade to presence-cap for the dimension. Translator exit 4 (sibling emitted a v1-only envelope — $schema_version absent or != 2) is a sibling-side regression: append "<plugin>:<dimension>: sibling emitted deprecated v1 envelope (missing \$schema_version: 2)" to sibling_integration_notes and degrade to presence-cap. The v1 composite_score passthrough was deprecated 2026-04-28 (post-M3) — the sibling must ship a v2 envelope to score via this path.
Validate the translator's envelope: must be valid JSON, must have a numeric composite_score (or null when the sibling emitted observations: [] with no legacy composite_score field — the v2-native "no scope" signal degrades to presence-cap). On invalid return, non-zero exit (other than the configuration-bug case above), or composite_score: null, degrade to the presence fallback and append a note to sibling_integration_notes.
The orchestrator does not summarize, restructure, re-derive, or "sanity-check" the script's output — its only job here is to capture the byte stream, run it through the translator, and validate the resulting envelope.
The parser-agent files at agents/parsers/<sibling>.md are legacy scaffolding from when parser dispatch routed through the Task tool (pre-H2d). They remain checked in as documentation of the contract but are not in the hot path; pronto invokes the scorer scripts directly.
For each dimension in the rubric:
weight from the rubric row.score from Phase 4.weighted_contribution = round(weight * score / 100, 1).Compute composite_score = round(sum(weighted_contribution)). Clamp to 0–100.
Compute the math via Bash + jq, not in your head. Even simple weighted sums are an LLM determinism hazard — a half-up vs half-even rounding choice in one run vs another contributes 1pt of composite stddev for free. Write the per-dimension {weight, score} pairs to a temp file and aggregate with one jq expression, e.g.:
jq -n --argjson dims "$DIMS_JSON" '
($dims | map(.weight * .score / 100)) as $contribs
| { weighted_contributions: ($contribs | map(. * 10 | round / 10)),
composite_score: ($contribs | add | round) }'
Use the returned composite_score directly in Phase 6's emission. Do not re-add the rounded weighted_contributions in your head — round(sum(x)) and sum(round(x)) differ.
Audit-level passthrough summary removed. The migration-progress line "<passthrough_count>/<passthrough_total> siblings scored via legacy passthrough — observations[] migration pending" was retired on 2026-04-28 when the v1 passthrough rule itself was deprecated (post-M1/M2/M3). Every sibling that reaches Phase 4.1 successfully is by construction on $schema_version: 2; the translator's exit 4 path surfaces any sibling-side regression to v1 directly via sibling_integration_notes (see Phase 4.1's exit-4 handling above), so the per-audit summary line is no longer informative.
Derive composite_grade and composite_label per the bands in rubric.md:
| Grade | Range | Label | |---|---|---| | A+ | 95-100 | Exceptional | | A | 90-94 | Excellent | | B | 75-89 | Good | | C | 60-74 | Fair | | D | 40-59 | Needs Work | | F | 0-39 | Critical |
Phase 6 produces the composite envelope via a deterministic shell composer. The orchestrator's job here is two steps: (1) write the envelope JSON to a known file path, and (2) emit the composer's stdout verbatim. There is no LLM-controlled construction step inside the emit boundary — the composer reads the prepared file, validates the schema, and prints a byte-clean envelope; the orchestrator transmits its bytes unchanged.
The split exists because LLM-controlled construction-and-emit at this boundary is a known instruction-following ceiling (see project/tickets/closed/phase-2-h2c-orchestrator-preamble-emission.md). Building the envelope into a file first, then emitting a script's verbatim stdout, replaces "construct and emit a multi-KB JSON object" with "transcribe these bytes" — a strictly simpler instruction.
Sentinel — verify before building. The envelope's top-level fields must include schema_version and dimensions[] (per references/report-format.md) — these two fields are the structural discriminators (the sibling sub-audit shape lacks both; composite_score alone does not discriminate because it appears at the top level of both the sub-audit and the composite). If your in-flight per-dimension state looks like {plugin, dimension, categories[], letter_grade, ...}, you have a sibling sub-audit in your hand — Phase 4 captured it as a value, Phase 5 should have aggregated from it, and Phase 6 must not echo it. Re-enter Phase 5 and compose the composite envelope from your per-dimension state. See the Phase 4 isolation invariant.
Compose the full composite envelope as a JSON object and write it to ${REPO_ROOT}/.pronto/composite-out.json. Use a single jq -n invocation — do not assemble the JSON by string concatenation. The shape is defined in references/report-format.md. Required top-level fields:
schema_version: 1repo: absolute REPO_ROOT pathtimestamp: TIMESTAMP (ISO 8601 UTC)composite_score: integer 0–100composite_grade: letter per the bands abovecomposite_label: label per the bands abovedimensions: array, one entry per rubric dimension (shape in references/report-format.md)kernel: the full KERNEL_JSON object captured in Phase 3sibling_integration_notes: array of strings (empty array [] if no notes)Construct via Bash + jq -n, e.g.:
mkdir -p "${REPO_ROOT}/.pronto"
jq -n \
--arg repo "${REPO_ROOT}" \
--arg ts "${TIMESTAMP}" \
--argjson cs "${COMPOSITE_SCORE}" \
--arg cg "${COMPOSITE_GRADE}" \
--arg cl "${COMPOSITE_LABEL}" \
--argjson dims "${DIMENSIONS_JSON}" \
--argjson kernel "${KERNEL_JSON}" \
--argjson notes "${SIBLING_INTEGRATION_NOTES_JSON}" \
'{
schema_version: 1,
repo: $repo,
timestamp: $ts,
composite_score: $cs,
composite_grade: $cg,
composite_label: $cl,
dimensions: $dims,
kernel: $kernel,
sibling_integration_notes: $notes
}' > "${REPO_ROOT}/.pronto/composite-out.json"
The bash variable names above are illustrative — substitute your in-memory state. The point is that the entire envelope is constructed inside one jq -n call and lands in the file in one write.
Run:
"${CLAUDE_PLUGIN_ROOT}/skills/audit/compose-composite.sh" "${REPO_ROOT}"
The composer reads ${REPO_ROOT}/.pronto/composite-out.json, validates the schema (must have schema_version=1, dimensions[8], all required top-level fields, and structurally valid types), and prints the envelope to stdout byte-clean — compact JSON, no leading or trailing whitespace.
Your final response is byte-identical to the composer's stdout. Transcribe it exactly. Do not add a preamble. Do not add a trailer. Do not summarise the result. Do not narrate "audit complete" or "envelope emitted." The composer is the emit; your job is to transmit its output unchanged to pronto's stdout.
If the composer exits non-zero, the envelope file failed validation. Read its stderr, fix the underlying issue (re-run Step 1 with corrected fields), and re-run the composer. Do not emit anything until the composer succeeds with exit 0. A non-zero composer exit is not a failure to recover from by hand-emitting the envelope — it is a signal that Step 1 produced an invalid file, and emitting that invalid file would be worse than failing cleanly.
The composer enforces every byte-cleanliness rule the previous emit prose tried to enumerate (no fences, no preamble, no trailer, no blank lines, single compact JSON object). Diagnostics from any phase still go to stderr (echo "..." >&2) or are suppressed; the composer's stdout is the only thing that reaches pronto's stdout.
Present the scorecard per the template in references/report-format.md:
╔══════════════════════════════════════════════════════════╗
║ PRONTO READINESS SCORECARD ║
╠══════════════════════════════════════════════════════════╣
║ Composite: XX/100 Grade: X (Label) ║
║ Repo: <REPO_ROOT> ║
║ Ran: <TIMESTAMP> ║
╚══════════════════════════════════════════════════════════╝
Weakest first:
<dimension rows ordered ascending by score, ties broken by descending weight>
What's next:
Run /pronto:improve to walk the weakest dimensions in order.
Kernel health:
<✓/✗ per kernel category>
Sibling integration notes:
<bullets or omitted if empty>
Rendering rules for dimension rows:
█ for filled, ░ for empty; filled = round(score * 25 / 100).✓ sibling-scored, ⊘ presence-cap, × presence-fail, ◉ kernel-owned.✓ rows: ✓ <sibling-plugin> (weight W)⊘ rows: ⊘ presence-cap (weight W) — recommended: <sibling-plugin>[ (Phase N)]× rows: × not configured (weight W) — recommended: <sibling-plugin>◉ rows: ◉ kernel-owned (weight W)Annotations showing (Phase N) surface for siblings whose plugin_status is phase-1b or phase-2-plus in recommendations.json.
Write the JSON composite to ${REPO_ROOT}/.pronto/state.json. Create .pronto/ if missing. Overwrite any prior state.
In JSON mode, any confirmation or diagnostic about state persistence (e.g., "State persisted to .pronto/state.json") goes to stderr only — never to stdout. Stdout already received its sole payload in Phase 6 (the JSON composite), and adding trailing prose breaks machine consumers piping through jq. In markdown mode, the state-persistence confirmation is optional and may appear after the scorecard.
Schema:
{
"schema_version": 1,
"last_audit": "<TIMESTAMP>",
"composite_score": <int>,
"composite_grade": "<letter>",
"dimensions": {
"<slug>": { "score": <int>, "weight": <int>, "source": "<enum>", "source_plugin": "<name or null>" }
}
}
This is the state /pronto:status reads and /pronto:improve consumes.
sibling_integration_notes, degrade that dimension to its presence check, continue..pronto/ directory not writable → note in sibling_integration_notes, still emit the scorecard./pronto:init if the plugin install is damaged.sibling_integration_notes, proceed.Target: full audit completes in under 5 seconds on a repo with claudit, skillet, commventional installed and the kernel populated. This is achievable because:
If a parser exceeds 10 seconds, degrade to presence and log a note.
/claudit is a multi-phase interactive skill; running it inside /pronto:audit would disrupt the user. Parsers read repo state directly and emit contract JSON. This is the Phase 1 reality — Phase 2+ siblings will ship --json flags and this skill will prefer those..pronto/state.json. The orchestrator is read-mostly. Any fix-applying behavior belongs to /pronto:improve.sibling_integration_notes is a warnings array, not a success log.
[] is the steady state when every sibling dispatched and every parser returned valid JSON..pronto/ wasn't writable, validation warnings./claudit:knowledge is out of audit scope by design (Phase 2.5); do not emit a note about it. Its absence is not a sibling integration issue."avanti: dispatched via Skill tool, composite <N> (<grade>)". Never write "invoked inline", "executed scoring logic inline", or "skipped to avoid nested skill invocation" — those phrasings describe failure modes that no longer exist in this spec.documentation
Surface (and optionally fix) doc-tree drift — duplicates, dead links, stale docs, template non-compliance, missing `## Related` blocks. Read-only by default; `--apply` does mechanical fixes; `--apply-semantic` emits diffs for human review.
documentation
Full-text search over the repo's `docs/` tree (FTS5-backed). Returns ranked hits with file paths, tags, and matching snippets.
testing
Retrieval-augmented Q&A over the repo's `docs/` tree. Returns a one-paragraph synthesis plus citations (doc path + heading anchor) and per-citation corroboration verdicts. Field shape and ordering are locked at M3; M5 populates the verdicts.
documentation
Scaffold a new doc under `docs/` from a Diátaxis template, or update an existing one and bump its `updated:` date. Suggests `## Related` candidates and refreshes the FTS5 index on write.