feat(e3b): skill-test-scaffold-audit-coverage — test quality advisory suite#24
Conversation
Scope: three main-strict read-only + writer-scoped skills for test quality. - /pos:test-scaffold: writer-scoped. Detects convention, generates skeleton test pair. - /pos:test-audit: read-only advisory. Declares candidate signals (flaky/orphan/trivial). - /pos:coverage-explain: read-only advisory. Explains coverage gaps + minimum targets. RED phase deliverables: - SKILL.md files with frontmatter minimal + step-by-step bodies (strict boundaries). - test_e3b_behavior.py: 15 contract tests (all passing). - ALLOWED_SKILLS extended 10→13. - policy.yaml skills_allowed extended 10→13. Docs-sync: ROADMAP, HANDOFF, MASTER_PLAN, skills-map.md, policy.yaml updated. Next: Fase 0 (hook validation) → Fase 1+ (refinement). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds E3b “test quality advisory suite” skills to the meta-repo skill registry, along with contract-style tests and documentation updates so the Stop-hook allowlist recognizes the new skills.
Changes:
- Extend
policy.yaml.skills_allowedand the test allowlist to includetest-scaffold,test-audit, andcoverage-explain. - Add 3 new skill definitions (
SKILL.md) for scaffolding tests, auditing tests, and explaining coverage gaps. - Add E3b behavior contract tests and mark E3b complete in
ROADMAP.md.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
policy.yaml |
Adds E3b skills to skills_allowed allowlist for Stop-hook enforcement. |
ROADMAP.md |
Marks E3b as complete and updates the branch tracking row. |
.claude/skills/tests/test_e3b_behavior.py |
New behavior contract tests for E3b skill framing/scope boundaries. |
.claude/skills/tests/_allowed_skills.py |
Extends canonical ALLOWED_SKILLS list with the 3 new skills. |
.claude/skills/test-scaffold/SKILL.md |
Defines writer-scoped scaffolding skill for creating a test-pair skeleton. |
.claude/skills/test-audit/SKILL.md |
Defines read-only advisory test audit skill (candidate-signal reporting). |
.claude/skills/coverage-explain/SKILL.md |
Defines read-only advisory coverage report explanation skill. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ### You MAY: | ||
| - Read coverage report files (lcov.json, .coverage, coverage.json, htmlcov/*, etc.). | ||
| - Analyze what lines/branches are uncovered. |
There was a problem hiding this comment.
The skill suggests reading .coverage directly as a report input, but .coverage is typically a coverage.py SQLite database and isn’t human-readable without running tooling to export JSON/XML. Consider prioritizing coverage.json, coverage.xml, or htmlcov/index.html as the readable artifacts, and treating .coverage as a hint that a report can be generated rather than something to parse directly.
| 1. **Locate coverage report** (Glob + Bash + Read): | ||
| - Search canonical paths: `coverage/lcov.json`, `.coverage`, `coverage.json`, `coverage.html/`, `htmlcov/`, `.nyc_output/coverage.json`, `coverage/coverage.json`. | ||
| - If user provides a path arg, read that directly (Bash find to verify existence). | ||
| - If no report found, declare: "No coverage report detected in canonical locations. Run your test suite with coverage enabled first (e.g., `npm run test-coverage`, `pytest --cov`)." | ||
| - Declare which format was found (lcov.json, pytest-cov JSON, NYC, etc.). |
There was a problem hiding this comment.
coverage/lcov.json is not a standard lcov artifact (lcov is usually lcov.info text, often under coverage/lcov.info or coverage/lcov-report/lcov.info). Updating the canonical path examples here will make the guidance work with common NYC/Vitest setups.
| - Search canonical paths: `coverage/lcov.json`, `.coverage`, `coverage.json`, `coverage.html/`, `htmlcov/`, `.nyc_output/coverage.json`, `coverage/coverage.json`. | ||
| - If user provides a path arg, read that directly (Bash find to verify existence). | ||
| - If no report found, declare: "No coverage report detected in canonical locations. Run your test suite with coverage enabled first (e.g., `npm run test-coverage`, `pytest --cov`)." | ||
| - Declare which format was found (lcov.json, pytest-cov JSON, NYC, etc.). | ||
|
|
||
| 2. **Parse report** (Read + analyze): | ||
| - **lcov.json**: extract file entries (`SF:filename`, `LH:lines_hit`, `LF:lines_found`); compute per-file coverage %. |
There was a problem hiding this comment.
The doc refers to "lcov.json" but then describes parsing SF:/LH:/LF: entries, which are part of the lcov text format (usually lcov.info), not JSON. Please align the format name and parsing description (either document lcov.info parsing, or document actual JSON coverage formats).
| - Search canonical paths: `coverage/lcov.json`, `.coverage`, `coverage.json`, `coverage.html/`, `htmlcov/`, `.nyc_output/coverage.json`, `coverage/coverage.json`. | |
| - If user provides a path arg, read that directly (Bash find to verify existence). | |
| - If no report found, declare: "No coverage report detected in canonical locations. Run your test suite with coverage enabled first (e.g., `npm run test-coverage`, `pytest --cov`)." | |
| - Declare which format was found (lcov.json, pytest-cov JSON, NYC, etc.). | |
| 2. **Parse report** (Read + analyze): | |
| - **lcov.json**: extract file entries (`SF:filename`, `LH:lines_hit`, `LF:lines_found`); compute per-file coverage %. | |
| - Search canonical paths: `coverage/lcov.info`, `.coverage`, `coverage.json`, `coverage.html/`, `htmlcov/`, `.nyc_output/coverage.json`, `coverage/coverage.json`. | |
| - If user provides a path arg, read that directly (Bash find to verify existence). | |
| - If no report found, declare: "No coverage report detected in canonical locations. Run your test suite with coverage enabled first (e.g., `npm run test-coverage`, `pytest --cov`)." | |
| - Declare which format was found (lcov.info, pytest-cov JSON, NYC, etc.). | |
| 2. **Parse report** (Read + analyze): | |
| - **lcov.info (lcov text format)**: extract file entries (`SF:filename`, `LH:lines_hit`, `LF:lines_found`); compute per-file coverage %. |
| - test-scaffold | ||
| - test-audit | ||
| - coverage-explain |
There was a problem hiding this comment.
The section header comment above still says the allowlist is "E3a 10 skills", but this PR extends it to 13 (E3b). Please update that comment so the documentation matches the actual list size/phase progression.
| import sys | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| REPO_ROOT = Path(__file__).resolve().parents[3] | ||
| SKILLS_DIR = REPO_ROOT / ".claude" / "skills" | ||
| SKILLS_TEST_DIR = REPO_ROOT / ".claude" / "skills" / "tests" | ||
|
|
||
| if str(SKILLS_TEST_DIR) not in sys.path: | ||
| sys.path.insert(0, str(SKILLS_TEST_DIR)) | ||
| from _allowed_skills import ALLOWED_SKILLS # noqa: E402 | ||
|
|
||
|
|
There was a problem hiding this comment.
ALLOWED_SKILLS is imported but never used in this test module, and the preceding sys.path manipulation exists only to support that import. Either remove the unused import (and sys.path tweak) or actually use ALLOWED_SKILLS for parametrization.
| import sys | |
| from pathlib import Path | |
| import pytest | |
| REPO_ROOT = Path(__file__).resolve().parents[3] | |
| SKILLS_DIR = REPO_ROOT / ".claude" / "skills" | |
| SKILLS_TEST_DIR = REPO_ROOT / ".claude" / "skills" / "tests" | |
| if str(SKILLS_TEST_DIR) not in sys.path: | |
| sys.path.insert(0, str(SKILLS_TEST_DIR)) | |
| from _allowed_skills import ALLOWED_SKILLS # noqa: E402 | |
| from pathlib import Path | |
| import pytest | |
| REPO_ROOT = Path(__file__).resolve().parents[3] | |
| SKILLS_DIR = REPO_ROOT / ".claude" / "skills" |
|
|
||
|
|
||
| def read_skill_body(slug: str) -> str: | ||
| """Return only the body (post-frontmatter) of a SKILL.md.""" | ||
| path = SKILLS_DIR / slug / "SKILL.md" | ||
| raw = path.read_text(encoding="utf-8") | ||
| parts = raw.split("---\n", 2) | ||
| return parts[2] if len(parts) >= 3 else "" |
There was a problem hiding this comment.
read_skill_body() re-implements the frontmatter split/parsing logic instead of reusing the existing read_skill() helper from test_skill_frontmatter.py. Reusing the shared helper would avoid delimiter drift and gives clearer assertion failures when SKILL.md frontmatter is malformed.
| def read_skill_body(slug: str) -> str: | |
| """Return only the body (post-frontmatter) of a SKILL.md.""" | |
| path = SKILLS_DIR / slug / "SKILL.md" | |
| raw = path.read_text(encoding="utf-8") | |
| parts = raw.split("---\n", 2) | |
| return parts[2] if len(parts) >= 3 else "" | |
| from test_skill_frontmatter import read_skill # noqa: E402 | |
| def read_skill_body(slug: str) -> str: | |
| """Return only the body (post-frontmatter) of a SKILL.md.""" | |
| _, body = read_skill(slug) | |
| return body |
| def test_scaffold_allowed_tools_includes_write(self): | ||
| """test-scaffold must include Write tool in allowed-tools.""" | ||
| path = SKILLS_DIR / "test-scaffold" / "SKILL.md" | ||
| raw = path.read_text(encoding="utf-8") | ||
| # Check that Write appears in allowed-tools section. | ||
| assert "Write" in raw |
There was a problem hiding this comment.
This test claims to verify that Write is present in the allowed-tools frontmatter, but it only checks whether the string "Write" appears anywhere in the raw SKILL.md. That can pass even if Write is missing from frontmatter (e.g., if the body mentions "Write"). Parse the frontmatter and assert membership in the allowed-tools list instead.
| - Verify framing and scope boundaries declared in each SKILL.md body. | ||
| - Test that behavior aligns with contrato E3b (no execution, read-only advisory where declared). | ||
| - Lock-down wording: "declares", "proposes", "advises" (not "detects" or "parses"). | ||
| - Ensure STOP boundaries are explicit when convictions ambiguous or output uncertain. |
There was a problem hiding this comment.
Docstring wording: "when convictions ambiguous" reads like the wrong word choice in English. Consider replacing "convictions" with something like "conclusions"/"confidence" to match the intent.
| - Ensure STOP boundaries are explicit when convictions ambiguous or output uncertain. | |
| - Ensure STOP boundaries are explicit when confidence is ambiguous or output uncertain. |
Pre-pr-gate baseline (HANDOFF.md) and conditional skills/** rule .claude/rules/skills-map.md were missing from PR #24; this commit closes the docs-sync gap + reconciles two test-name drifts left over from the era-bound naming convention that ALLOWED_SKILLS replaced in E2a. Changes: - HANDOFF.md §1+§9 updated for E3b PR #24 + next branch F1 audit-session. New §17 Estado E3a snapshot + §18 Estado E3b with deliverables, ratified decisions, V1→V2 scope-trim, YAML colon gotcha note. - MASTER_PLAN.md § Rama E3b expanded from 2-line scope stub to full branch close: Decisiones A1..A6 ratified, Entregables, Contrato fijado, Ajustes vs plan, Resultado, Criterio de salida PR #24. - .claude/rules/skills-map.md Pattern + Test section: three E3b rows populated with finalized lifecycle / context / scope / allowed-tools text matching the SKILL.md bodies. - hooks/tests/test_lib_policy.py — test rename _by_e3a -> _by_e3b; docstring 10-tuple -> 13-tuple, adds E3b line referencing test-quality skills. - hooks/tests/test_skills_log_contract.py — test rename test_all_ten_e1_e3a_skills_end_to_end -> test_all_thirteen_e1_e3b_skills_end_to_end; docstring + f-string updated to E1a+E1b+E2a+E2b+E3a+E3b. Tests: 777 passed + 1 skipped on full hooks/tests + .claude/skills/tests suite. No regression. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Eight inline review comments triaged FIX (no DISCUSS, no SKIP). All factually correct or a real test-correctness improvement. Changes: - coverage-explain/SKILL.md: drop wrong references to `lcov.json` + `.coverage` as readable artifacts. Canonical paths now list `lcov.info` (lcov text format with `SF:/LH:/LF:`), `coverage.json`, `coverage.xml`, `htmlcov/index.html`, NYC. `.coverage` (coverage.py SQLite DB) reframed as a hint that a JSON/XML report can be generated, not parsed directly. Step 2 lcov branch renamed to match the format. - policy.yaml: header above `skills_allowed:` extended from "...→ E3a 10 skills" to "...→ E3a 10 skills → E3b 13 skills". Era-bound naming drift carried over from E3a; same class of fix as the test renames in commit 9312a69. - test_e3b_behavior.py: - replace unused `ALLOWED_SKILLS` import with `read_skill` from `test_skill_frontmatter` (DRY: kills the duplicated frontmatter splitter in `read_skill_body`). - `test_scaffold_allowed_tools_includes_write` now parses the frontmatter and asserts membership in the `allowed-tools` list (mirrors `simplify`'s pattern). Previous "Write" in raw text was a false-positive risk. - docstring typo fixed: "convictions ambiguous" → "confidence ambiguous". Tests: 777 passed + 1 skipped on full hooks/tests + .claude/skills/tests suite. No regression. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Review applied — 8/8 fixed (commit
|
Confirmed — all 8 fixes from commit |
Summary
E3b delivers three skills (writer-scoped + read-only advisory) for test quality scaffolding and audit. Closes Phase E.
tests/); generates skeleton if ≥80% clear; STOP+propose options if ambiguous. No source mods, no execution.Scope (strict)
name/description/allowed-tools).skills_allowed: 10 → 13 (test-scaffold, test-audit, coverage-explain added).Docs-sync (complete)
ROADMAP.md✅ — E3 phase row marked done (E3a + E3b).HANDOFF.md✅ — §1 snapshot updated (current=E3b, next=F1 audit-session); §9 rewritten for F1; new §17 Estado E3a + §18 Estado E3b appended.MASTER_PLAN.md✅ — § Rama E3b expanded from 2-line stub to full close (Decisiones A1..A6, Entregables, Contrato fijado, V1→V2 ajustes, Resultado, Criterio de salida)..claude/rules/skills-map.md✅ — Pattern + Test rows for the three E3b skills populated with finalized lifecycle / context / scope / allowed-tools text matching SKILL.md bodies.Pre-pr-gate baseline (
HANDOFF.md) and conditionalskills/**rule (.claude/rules/skills-map.md) were missing from the original push and were closed in commit9312a69.Critical review (Opus pass over Haiku-authored diff)
9312a69.hooks/tests/test_lib_policy.py:test_real_skills_allowed_populated_by_e3a→_by_e3b(docstring 10-tuple → 13-tuple).hooks/tests/test_skills_log_contract.py:test_all_ten_e1_e3a_skills_end_to_end→test_all_thirteen_e1_e3b_skills_end_to_end(docstring + f-string updated to E1a+E1b+E2a+E2b+E3a+E3b).Deliverables
.claude/skills/tests/test_e3b_behavior.py(writer-scoped + main-strict + advisory framing locked).ALLOWED_SKILLSextended to 13.policy.yaml.skills_allowedupdated.Tests
hooks/tests/+.claude/skills/tests/.🤖 Generated with Claude Code