From d13d6fc616ceafb2fccbe06617227a105daa436e Mon Sep 17 00:00:00 2001 From: Justin McLean Date: Tue, 30 Jun 2026 10:38:00 +1000 Subject: [PATCH] feat(validator): add docs/modes.md consistency check (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add validate_modes_doc_consistency (SOFT, category modes-doc-consistency) to the skill-and-tool-validator. The check compares docs/modes.md against live skills/*/SKILL.md frontmatter across four dimensions: 1. Listed skill missing from disk — row exists in modes.md but skills// does not. 2. Mode mismatch — a skill's mode: frontmatter differs from the section it appears in (e.g. listed under Triage but mode: Mentoring). 3. Skill count mismatch — the integer in "Modes at a glance" Skill count column does not equal the actual row count in that mode's section. 4. Unlisted skill — a live skill has a valid mode: frontmatter value but is absent from the corresponding docs/modes.md section. On the live tree the check surfaces two real gaps: - reviewer-routing has mode: Triage but is absent from docs/modes.md - good-first-issue-sweep has mode: Mentoring but is absent from docs/modes.md Ships 19 tests in TestParseModesDocs + TestValidateModeDocConsistency; all 283 validator tests pass. Generated-by: Claude (Opus 4.7) --- .../src/skill_and_tool_validator/__init__.py | 211 +++++++++++++- .../tests/test_validator.py | 260 ++++++++++++++++++ tools/spec-loop/IMPLEMENTATION_PLAN.md | 19 ++ 3 files changed, 489 insertions(+), 1 deletion(-) diff --git a/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py b/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py index a4ae895d..fa5e162b 100644 --- a/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py +++ b/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py @@ -17,7 +17,7 @@ """Validate framework skill definitions. -This module validates eleven aspects of every skill under +This module validates twelve aspects of every skill under skills/: 1. YAML frontmatter — every SKILL.md must have a valid frontmatter @@ -66,6 +66,14 @@ handling, supported operations, and adopter config keys. Missing fields are advisories so legacy adapters can be brought into compliance deliberately without blocking unrelated changes. +12. docs/modes.md consistency (SOFT) — compares the per-mode skill + tables in ``docs/modes.md`` against live ``skills/*/SKILL.md`` + frontmatter: every listed skill must exist on disk, each skill's + ``mode:`` frontmatter must match the section it appears in, the + claimed skill counts in the "Modes at a glance" table must equal + the actual per-section row counts, and every live skill with a + ``mode:`` frontmatter must appear in the corresponding section. + Advisory only — never fails the run unless ``--strict``. SOFT categories surface as advisory warnings (stderr) without failing the run unless ``--strict`` is passed. @@ -79,6 +87,7 @@ from __future__ import annotations import argparse +import contextlib import re import sys from collections.abc import Iterable @@ -93,6 +102,7 @@ DOCS_DIR = Path("docs") SKILL_EVALS_DIR = Path("tools/skill-evals/evals") PROJECTS_TEMPLATE_DIR = Path("projects/_template") +MODES_DOC_PATH = Path("docs/modes.md") # Categories for the tool-validator block. All HARD by default — every # tool must have a README that declares its capability and its prerequisites. @@ -362,6 +372,9 @@ def _read_mode_table() -> dict[str, str]: ASF_COUPLING_CATEGORY = "asf_coupling" # SOFT advisory: adapter authoring fields for contract:* tools. ADAPTER_AUTHORING_CATEGORY = "adapter-authoring" +# SOFT advisory: docs/modes.md skill lists and claimed counts are checked against +# live skill frontmatter — detects doc drift before review. +MODES_DOC_CATEGORY = "modes-doc-consistency" # The `magpie-` namespace prefix every installed framework skill carries. SKILL_NAME_PREFIX = "magpie-" @@ -377,6 +390,7 @@ def _read_mode_table() -> dict[str, str]: EVAL_COVERAGE_CATEGORY, ASF_COUPLING_CATEGORY, ADAPTER_AUTHORING_CATEGORY, + MODES_DOC_CATEGORY, } ) HARD_CATEGORIES: frozenset[str] = frozenset( @@ -2136,6 +2150,197 @@ def collect_doc_files(root: Path | None = None) -> set[Path]: return files +# --------------------------------------------------------------------------- +# docs/modes.md consistency check (check #11, SOFT) +# --------------------------------------------------------------------------- + +# Regex that matches a skill row in a per-mode section table: +# | [`skill-slug`](../skills/skill-slug/SKILL.md) | ... | +# Group 1: skill slug (the backtick-quoted identifier). +_MODES_DOC_SKILL_ROW_RE = re.compile(r"^\|\s*\[`([a-z][a-z0-9-]*)`\]\(\.\./skills/[^)]+/SKILL\.md\)") + +# Regex that matches the skill-count cell in the "Modes at a glance" table: +# | **ModeName** | purpose text | status text | 30 | +# Group 1: mode name (the bold identifier). +# Group 2: skill count (last non-empty cell, integer). +_MODES_GLANCE_ROW_RE = re.compile(r"^\|\s*\*\*([^*]+)\*\*\s*\|[^|]+\|[^|]+\|\s*(\d+)\s*\|") + +# The h2 headings in docs/modes.md that map 1-to-1 to mode names in skill +# frontmatter. "Outside the modes" and "Agentic Autonomous" are listed +# separately because they don't correspond to mode: frontmatter values. +_MODES_DOC_NAMED_SECTIONS: frozenset[str] = frozenset({"Triage", "Mentoring", "Drafting", "Pairing"}) +_MODES_DOC_SKIP_SECTIONS: frozenset[str] = frozenset({"Agentic Autonomous", "Outside the modes"}) + + +def _parse_modes_doc( + text: str, +) -> tuple[dict[str, int], dict[str, list[str]], list[str]]: + """Parse docs/modes.md into (claimed_counts, section_skills, outside_skills). + + claimed_counts — {mode_name: claimed_int} from "Modes at a glance" table. + section_skills — {mode_name: [slug, …]} from each named h2 section. + outside_skills — [slug, …] listed under "## Outside the modes". + """ + claimed_counts: dict[str, int] = {} + section_skills: dict[str, list[str]] = {} + outside_skills: list[str] = [] + + # --- "Modes at a glance" table --- + if "## Modes at a glance" in text: + glance_section = text.split("## Modes at a glance", 1)[1] + next_h2 = glance_section.find("\n## ") + if next_h2 > 0: + glance_section = glance_section[:next_h2] + for line in glance_section.splitlines(): + m = _MODES_GLANCE_ROW_RE.match(line) + if m: + mode_name = m.group(1).strip() + with contextlib.suppress(ValueError): + claimed_counts[mode_name] = int(m.group(2)) + + # --- Per-section skill rows --- + current_section: str | None = None + for line in text.splitlines(): + h2_match = re.match(r"^## (.+)$", line) + if h2_match: + current_section = h2_match.group(1).strip() + continue + if current_section is None: + continue + row_match = _MODES_DOC_SKILL_ROW_RE.match(line) + if not row_match: + continue + slug = row_match.group(1) + if current_section == "Outside the modes": + outside_skills.append(slug) + elif current_section in _MODES_DOC_NAMED_SECTIONS: + section_skills.setdefault(current_section, []).append(slug) + + return claimed_counts, section_skills, outside_skills + + +def validate_modes_doc_consistency(root: Path | None = None) -> Iterable[Violation]: + """Compare docs/modes.md skill tables against live skill frontmatter. + + Four advisory checks (all SOFT — never fails the run unless --strict): + + 1. **Missing skill** — a slug listed in a named per-mode section + (Triage / Mentoring / Drafting / Pairing) has no matching + ``skills//`` directory on disk. + + 2. **Mode mismatch** — a skill listed in section ``X`` has a ``mode:`` + frontmatter value that differs from ``X``. Skills without a + ``mode:`` frontmatter field are exempt (not every skill declares one). + + 3. **Count mismatch** — the integer in the Skill-count column of the + "Modes at a glance" table does not match the number of skill rows + actually present in that section. Counts for "Agentic Autonomous" + and "Outside the modes" are skipped (no skill rows expected there). + + 4. **Unlisted skill** — a live skill under ``skills/`` has a ``mode:`` + frontmatter value that is a named section (Triage / Mentoring / + Drafting / Pairing) but the skill does not appear in that section. + This catches new skills that were added to the skill directory without + updating docs/modes.md. + """ + repo_root = root or find_repo_root() + doc_path = repo_root / MODES_DOC_PATH + if not doc_path.exists(): + return + + try: + doc_text = doc_path.read_text(encoding="utf-8") + except OSError: + return + + claimed_counts, section_skills, _outside_skills = _parse_modes_doc(doc_text) + + # Build the set of skills listed per section for O(1) membership tests. + section_skill_sets: dict[str, set[str]] = {mode: set(slugs) for mode, slugs in section_skills.items()} + + # Check 1 & 2 — per-listed-skill checks. + for mode, slugs in section_skills.items(): + for slug in slugs: + skill_dir = repo_root / SKILLS_DIR / slug + if not skill_dir.is_dir(): + yield Violation( + doc_path, + None, + f"modes-doc: skill '{slug}' listed in '## {mode}' section " + f"but skills/{slug}/ does not exist — remove the row or add the skill", + category=MODES_DOC_CATEGORY, + ) + continue + skill_md = skill_dir / "SKILL.md" + if not skill_md.exists(): + continue + try: + skill_text = skill_md.read_text(encoding="utf-8") + except OSError: + continue + fm = parse_frontmatter(skill_text) + if fm is None: + continue + fm_mode = fm.get("mode", "") + if fm_mode and fm_mode != mode: + yield Violation( + doc_path, + None, + f"modes-doc: skill '{slug}' is listed under '## {mode}' " + f"but its frontmatter declares mode: {fm_mode!r} — " + f"move the row to '## {fm_mode}' or fix the frontmatter", + category=MODES_DOC_CATEGORY, + ) + + # Check 3 — claimed count vs actual row count. + for mode, claimed in claimed_counts.items(): + if mode in _MODES_DOC_SKIP_SECTIONS: + continue + if mode not in _MODES_DOC_NAMED_SECTIONS: + continue + actual = len(section_skills.get(mode, [])) + if actual != claimed: + yield Violation( + doc_path, + None, + f"modes-doc: '## Modes at a glance' claims {claimed} skill(s) for " + f"'{mode}' but the '## {mode}' section lists {actual} skill row(s) — " + f"update the Skill count column", + category=MODES_DOC_CATEGORY, + ) + + # Check 4 — live skills with mode: not listed in the corresponding section. + skills_base = repo_root / SKILLS_DIR + if not skills_base.exists(): + return + for skill_dir in sorted(skills_base.iterdir()): + if not skill_dir.is_dir(): + continue + skill_md = skill_dir / "SKILL.md" + if not skill_md.exists(): + continue + try: + skill_text = skill_md.read_text(encoding="utf-8") + except OSError: + continue + fm = parse_frontmatter(skill_text) + if fm is None: + continue + fm_mode = fm.get("mode", "") + if fm_mode not in _MODES_DOC_NAMED_SECTIONS: + continue + slug = skill_dir.name + if slug not in section_skill_sets.get(fm_mode, set()): + yield Violation( + doc_path, + None, + f"modes-doc: skill '{slug}' has frontmatter mode: {fm_mode!r} " + f"but is not listed in the '## {fm_mode}' section of docs/modes.md — " + f"add a row for this skill", + category=MODES_DOC_CATEGORY, + ) + + # --------------------------------------------------------------------------- # Eval-coverage check (check #9, SOFT) # --------------------------------------------------------------------------- @@ -2224,6 +2429,9 @@ def run_validation(root: Path | None = None) -> list[Violation]: # Eval-coverage check: every skill must have a matching eval suite. violations.extend(validate_eval_coverage(repo_root)) + # docs/modes.md consistency check: skill lists and counts match live frontmatter. + violations.extend(validate_modes_doc_consistency(repo_root)) + return violations @@ -2295,6 +2503,7 @@ def main(argv: list[str] | None = None) -> int: "criteria-source", "distinct-from", "lowercase-f-field", + "modes-doc:", "parenthetical rationale", "trigger phrase", "injection-guard TODO", diff --git a/tools/skill-and-tool-validator/tests/test_validator.py b/tools/skill-and-tool-validator/tests/test_validator.py index 21a4e918..ccce7f98 100644 --- a/tools/skill-and-tool-validator/tests/test_validator.py +++ b/tools/skill-and-tool-validator/tests/test_validator.py @@ -43,11 +43,13 @@ LICENSE_HEADER_CATEGORY, LOWERCASE_F_FIELD_CATEGORY, MAX_METADATA_CHARS, + MODES_DOC_CATEGORY, PRINCIPLE_CATEGORY, PRIVACY_CATEGORY, SECURITY_PATTERN_CATEGORY, SOFT_CATEGORIES, TRIGGER_PRESERVATION_CATEGORY, + _parse_modes_doc, _read_mode_table, collect_doc_files, collect_files_to_check, @@ -74,6 +76,7 @@ validate_license_header, validate_links, validate_lowercase_f_field, + validate_modes_doc_consistency, validate_name_convention, validate_placeholders, validate_principle_compliance, @@ -3022,3 +3025,260 @@ def test_non_directory_entries_in_skills_are_skipped(self, tmp_path: Path) -> No (skills_dir / "README.md").write_text("# skills\n") violations = list(validate_eval_coverage(tmp_path)) assert violations == [] + + +# --------------------------------------------------------------------------- +# docs/modes.md consistency check (check #11 — SOFT) +# --------------------------------------------------------------------------- + + +class TestParseModesDocs: + """Unit tests for the _parse_modes_doc internal parser.""" + + def test_parses_claimed_counts(self) -> None: + text = ( + "## Modes at a glance\n" + "| **Triage** | purpose | stable | 5 |\n" + "| **Mentoring** | purpose | experimental | 3 |\n" + "\n" + "## Triage\n" + "| [`issue-triage`](../skills/issue-triage/SKILL.md) | desc | experimental |\n" + ) + counts, _, _ = _parse_modes_doc(text) + assert counts == {"Triage": 5, "Mentoring": 3} + + def test_parses_section_skills(self) -> None: + text = ( + "## Modes at a glance\n" + "| **Triage** | p | s | 2 |\n" + "\n" + "## Triage\n" + "| [`issue-triage`](../skills/issue-triage/SKILL.md) | d | experimental |\n" + "| [`issue-reassess`](../skills/issue-reassess/SKILL.md) | d | experimental |\n" + ) + _, section, outside = _parse_modes_doc(text) + assert section == {"Triage": ["issue-triage", "issue-reassess"]} + assert outside == [] + + def test_parses_outside_skills(self) -> None: + text = ( + "## Outside the modes\n" + "| [`setup`](../skills/setup/SKILL.md) | d |\n" + "| [`list-skills`](../skills/list-skills/SKILL.md) | d |\n" + ) + _, section, outside = _parse_modes_doc(text) + assert "setup" in outside + assert "list-skills" in outside + assert section == {} + + def test_skips_non_skill_rows(self) -> None: + text = ( + "## Triage\n" + "| Skill | Domain | Status |\n" + "|---|---|---|\n" + "| [`issue-triage`](../skills/issue-triage/SKILL.md) | d | experimental |\n" + "| Doc | Purpose |\n" + "| [`docs/README.md`](README.md) | overview |\n" + ) + _, section, _ = _parse_modes_doc(text) + # The doc row must not be parsed as a skill (its link isn't to skills/). + assert section == {"Triage": ["issue-triage"]} + + def test_empty_doc_returns_empty_structures(self) -> None: + counts, section, outside = _parse_modes_doc("") + assert counts == {} + assert section == {} + assert outside == [] + + +class TestValidateModeDocConsistency: + """Behavioural tests for validate_modes_doc_consistency.""" + + _GLANCE = ( + "## Modes at a glance\n" + "| **Triage** | purpose | stable | {triage_count} |\n" + "| **Mentoring** | purpose | experimental | {mentoring_count} |\n" + "\n" + ) + + def _make_skill(self, root: Path, slug: str, mode: str | None = None) -> None: + skill_dir = root / "skills" / slug + skill_dir.mkdir(parents=True, exist_ok=True) + mode_line = f"mode: {mode}\n" if mode else "" + (skill_dir / "SKILL.md").write_text( + f"---\nname: magpie-{slug}\ndescription: test skill\n" + f"capability: capability:triage\nlicense: Apache-2.0\n{mode_line}---\n" + ) + + def _make_modes_md(self, root: Path, text: str) -> Path: + docs_dir = root / "docs" + docs_dir.mkdir(parents=True, exist_ok=True) + path = docs_dir / "modes.md" + path.write_text(text) + return path + + # --- Check 1: missing skill on disk --- + + def test_listed_skill_exists_passes(self, tmp_path: Path) -> None: + self._make_skill(tmp_path, "issue-triage", mode="Triage") + doc = ( + self._GLANCE.format(triage_count=1, mentoring_count=0) + + "## Triage\n" + + "| [`issue-triage`](../skills/issue-triage/SKILL.md) | d | experimental |\n" + ) + self._make_modes_md(tmp_path, doc) + violations = [ + v + for v in validate_modes_doc_consistency(tmp_path) + if "missing" in v.message or "does not exist" in v.message + ] + assert violations == [] + + def test_listed_skill_missing_from_disk_yields_violation(self, tmp_path: Path) -> None: + doc = ( + self._GLANCE.format(triage_count=1, mentoring_count=0) + + "## Triage\n" + + "| [`ghost-skill`](../skills/ghost-skill/SKILL.md) | d | experimental |\n" + ) + self._make_modes_md(tmp_path, doc) + violations = list(validate_modes_doc_consistency(tmp_path)) + assert len(violations) == 1 + v = violations[0] + assert v.category == MODES_DOC_CATEGORY + assert "ghost-skill" in v.message + assert "does not exist" in v.message + + # --- Check 2: mode mismatch --- + + def test_mode_matches_section_passes(self, tmp_path: Path) -> None: + self._make_skill(tmp_path, "issue-triage", mode="Triage") + doc = ( + self._GLANCE.format(triage_count=1, mentoring_count=0) + + "## Triage\n" + + "| [`issue-triage`](../skills/issue-triage/SKILL.md) | d | experimental |\n" + ) + self._make_modes_md(tmp_path, doc) + violations = [v for v in validate_modes_doc_consistency(tmp_path) if "mode" in v.message.lower()] + # Count mismatch is the only expected warning; no mode mismatch. + assert not any("frontmatter declares mode" in v.message for v in violations) + + def test_mode_mismatch_yields_violation(self, tmp_path: Path) -> None: + # Skill is listed under Triage but its frontmatter says Mentoring. + self._make_skill(tmp_path, "issue-triage", mode="Mentoring") + doc = ( + self._GLANCE.format(triage_count=1, mentoring_count=0) + + "## Triage\n" + + "| [`issue-triage`](../skills/issue-triage/SKILL.md) | d | experimental |\n" + ) + self._make_modes_md(tmp_path, doc) + violations = list(validate_modes_doc_consistency(tmp_path)) + mismatch = [v for v in violations if "frontmatter declares mode" in v.message] + assert len(mismatch) == 1 + assert "issue-triage" in mismatch[0].message + assert "Mentoring" in mismatch[0].message + assert mismatch[0].category == MODES_DOC_CATEGORY + + def test_skill_without_mode_frontmatter_exempt_from_mismatch_check(self, tmp_path: Path) -> None: + # Skill in Triage section but has no mode: field → no mismatch warning. + self._make_skill(tmp_path, "issue-triage", mode=None) + doc = ( + self._GLANCE.format(triage_count=1, mentoring_count=0) + + "## Triage\n" + + "| [`issue-triage`](../skills/issue-triage/SKILL.md) | d | experimental |\n" + ) + self._make_modes_md(tmp_path, doc) + violations = [ + v for v in validate_modes_doc_consistency(tmp_path) if "frontmatter declares mode" in v.message + ] + assert violations == [] + + # --- Check 3: count mismatch --- + + def test_count_matches_section_row_count_passes(self, tmp_path: Path) -> None: + self._make_skill(tmp_path, "issue-triage", mode="Triage") + self._make_skill(tmp_path, "issue-reassess", mode="Triage") + doc = ( + self._GLANCE.format(triage_count=2, mentoring_count=0) + + "## Triage\n" + + "| [`issue-triage`](../skills/issue-triage/SKILL.md) | d | experimental |\n" + + "| [`issue-reassess`](../skills/issue-reassess/SKILL.md) | d | experimental |\n" + ) + self._make_modes_md(tmp_path, doc) + count_violations = [ + v + for v in validate_modes_doc_consistency(tmp_path) + if "Skill count" in v.message or "claims" in v.message + ] + assert count_violations == [] + + def test_count_mismatch_yields_violation(self, tmp_path: Path) -> None: + self._make_skill(tmp_path, "issue-triage", mode="Triage") + doc = ( + # Claims 3 but only 1 row. + self._GLANCE.format(triage_count=3, mentoring_count=0) + + "## Triage\n" + + "| [`issue-triage`](../skills/issue-triage/SKILL.md) | d | experimental |\n" + ) + self._make_modes_md(tmp_path, doc) + violations = list(validate_modes_doc_consistency(tmp_path)) + count_v = [v for v in violations if "claims" in v.message] + assert len(count_v) == 1 + assert "3" in count_v[0].message + assert "1" in count_v[0].message + assert count_v[0].category == MODES_DOC_CATEGORY + + # --- Check 4: live skill with mode: not listed in section --- + + def test_unlisted_skill_with_mode_yields_violation(self, tmp_path: Path) -> None: + # Skill has mode: Triage but is absent from the Triage section. + self._make_skill(tmp_path, "new-triage-skill", mode="Triage") + doc = self._GLANCE.format(triage_count=0, mentoring_count=0) + "## Triage\n" + self._make_modes_md(tmp_path, doc) + violations = list(validate_modes_doc_consistency(tmp_path)) + unlisted = [v for v in violations if "is not listed" in v.message] + assert len(unlisted) == 1 + assert "new-triage-skill" in unlisted[0].message + assert unlisted[0].category == MODES_DOC_CATEGORY + + def test_skill_with_no_mode_not_flagged_as_unlisted(self, tmp_path: Path) -> None: + # Skill has no mode: frontmatter → must NOT be flagged as unlisted. + self._make_skill(tmp_path, "utility-skill", mode=None) + doc = self._GLANCE.format(triage_count=0, mentoring_count=0) + "## Triage\n" + self._make_modes_md(tmp_path, doc) + violations = [v for v in validate_modes_doc_consistency(tmp_path) if "is not listed" in v.message] + assert violations == [] + + def test_skill_with_outside_modes_mode_not_flagged(self, tmp_path: Path) -> None: + # Skills whose mode field value isn't a named section aren't flagged. + self._make_skill(tmp_path, "setup", mode=None) + doc = "## Outside the modes\n| [`setup`](../skills/setup/SKILL.md) | d |\n" + self._make_modes_md(tmp_path, doc) + violations = [v for v in validate_modes_doc_consistency(tmp_path) if "is not listed" in v.message] + assert violations == [] + + # --- General --- + + def test_no_modes_md_returns_no_violations(self, tmp_path: Path) -> None: + self._make_skill(tmp_path, "issue-triage", mode="Triage") + # docs/modes.md does not exist — silent, no violations. + violations = list(validate_modes_doc_consistency(tmp_path)) + assert violations == [] + + def test_modes_doc_category_is_soft(self) -> None: + assert MODES_DOC_CATEGORY in SOFT_CATEGORIES + assert MODES_DOC_CATEGORY not in HARD_CATEGORIES + + def test_modes_doc_category_in_all_categories(self) -> None: + assert MODES_DOC_CATEGORY in ALL_CATEGORIES + + def test_all_violations_point_to_modes_md(self, tmp_path: Path) -> None: + doc = ( + self._GLANCE.format(triage_count=5, mentoring_count=0) + + "## Triage\n" + + "| [`ghost-skill`](../skills/ghost-skill/SKILL.md) | d | e |\n" + ) + self._make_modes_md(tmp_path, doc) + violations = list(validate_modes_doc_consistency(tmp_path)) + modes_md = tmp_path / "docs" / "modes.md" + for v in violations: + assert v.path == modes_md diff --git a/tools/spec-loop/IMPLEMENTATION_PLAN.md b/tools/spec-loop/IMPLEMENTATION_PLAN.md index 3ea42c3e..7b8ef298 100644 --- a/tools/spec-loop/IMPLEMENTATION_PLAN.md +++ b/tools/spec-loop/IMPLEMENTATION_PLAN.md @@ -490,6 +490,25 @@ slugs, not numbers (numbering implies an order the specs don't carry). Spec: [`specs/adapters.md`](specs/adapters.md). Branch `adapter-readme-authoring-compliance`. +22. **Reconcile docs/modes.md with the modes-doc detection.** + The `modes-doc-consistency-check` item added detection only; running the + validator now surfaces two real gaps it was meant to catch. `reviewer-routing` + carries `mode: Triage` in frontmatter but has no row in the `## Triage` + table, and `good-first-issue-sweep` carries `mode: Mentoring` but has no row + in the `## Mentoring` section. Add the missing `reviewer-routing` Triage row + (with its current status), then re-run the validator to confirm the doc is + clean. The `good-first-issue-sweep` row is already owned by the post-merge + sync item above and stays blocked until that PR lands, so do not add it here + unless that PR has merged; just confirm the only remaining `modes-doc` + warning is the blocked one. Detection-only stays as the validator's job; + this item is the human-confirmed doc update it was designed to trigger. + Validation: + ```bash + uv run --project tools/skill-and-tool-validator --group dev skill-and-tool-validate + ``` + Spec: [`specs/meta-and-quality-tooling.md`](specs/meta-and-quality-tooling.md). + Branch `modes-doc-reviewer-routing-row`. + --- ## Notes & discoveries