Imported from aimasteracc/tree-sitter-analyzer (
tree_sitter_analyzer/skills/tsa-edit-then-verify/SKILL.md). Install upstream withnpx skills add aimasteracc/tree-sitter-analyzer --skill tsa-edit-then-verify. Copyright stays with the author.
tsa-edit-then-verify — The full edit loop, scoped not full-suite
Source of authority. From
docs/agent-tooling-gap-report.mdline 58:Every feature update must run the self-hosted workflow:
edit action=safebefore risky edits,health action=fileon changed files,edit action=impactafter edits, its reportedverification_command, then the full default suite when risk remains.This skill is the executable form of that rule.
tsa-edit-safetyonly covers Phase A (the pre-edit gate). This skill covers A + B + C end-to-end and is the canonical replacement for the globalverifyskill (which runs the full ~5-minute suite every time).
When to use vs. not
Use when you're about to edit (or just edited) a real code file in this repo and the change is more than a 1-line cosmetic fix. The smaller the diff, the bigger the win — full pytest is ~5 min, scoped verification is typically 30-60s.
Don't use for:
- One-line typo in a doc/comment (no Python AST impact)
- Pure markdown / README edits (no symbol-level risk)
- File you're creating from scratch with no callers yet
- Read-only exploration
Why this exists (vs. verify skill)
| Skill | What it runs | Time | When right |
|---|---|---|---|
verify (global) |
uv run pytest -q (15k tests) |
~5 min | Pre-commit gate, large refactor |
tsa-edit-then-verify |
scoped verification_command from edit action=impact |
30-60s | Every feature update (the mandate) |
tsa-edit-safety |
Pre-edit verdict only | ~3s | "Is this safe to touch?" — no edit yet |
The scoped command targets only the tests reachable from the changed symbols,
via the AST call-graph. That's the whole point — edit action=impact
returns a verification_command that's narrower than pytest -q but still
covers the blast radius the AST sees.
Procedure
Phase A — Pre-edit (1 parallel MCP batch)
Call these in ONE message (parallel tool use):
edit action=safewithfile_path: "<file>"andedit_type: "<refactor|add_feature|fix_bug|rename>"health action=filewithfile_path: "<file>"— record the grade. This is your baseline.- (Optional, only if file is unfamiliar to you in this session)
project action=smartwithfile_path: "<file>"andquery: "<what you intend to change>"
Gate the verdict. From edit action=safe verdict:
UNSAFE→ STOP. Surface therisk_factors, ask the user how to proceed. Do not edit.CAUTION→ narrow your scope. Write a failing test first if there isn't one for the behavior you're changing.REVIEW→ write a test before the edit ifrisk_factorsmentions "no tests nearby".SAFE→ proceed to Phase B.
Persist the baseline grade. Note health action=file grade and
health action=file score in your scratch — you will diff against these in
Phase C.
Phase B — Edit (out of skill scope)
This is where the LLM actually does the work — Edit / Write / MultiEdit tool calls. The skill's only job here is to remind you: keep the diff focused. Large diffs make Phase C's scoped command less reliable (and may bump you into "full suite" territory at step 7).
If you find yourself making >100 lines of changes across >3 files, pause and reconsider whether this is really one task or three.
Phase C — Post-edit verify (1 parallel MCP batch, then Bash)
After your last edit, call these in ONE message (parallel tool use):
health action=filewithfile_path: "<file>"again → diff against Phase A baseline. Grade MUST NOT regress. (B→C is a regression. A→A is fine. F→D is improvement.)edit action=impactwithmode: "staged"(ormode: "branch"if you haven't staged) — returnsverification_command,pytest_command,queue_ledger,risk_remainsflag.
Then sequentially via Bash:
-
Run the exact
verification_commandreturned by step 5. This is the scoped command. Example shape:uv run pytest tests/unit/test_health_scorer.py tests/unit/test_file_health_smells.py -qExpect 30-60s. If it fails, fix and re-run from step 4.
-
Only if
edit action=impact risk_remains == true(or grade regressed in step 4, or impact reports cross-module reach), run the full suite:uv run pytest -qThis is the ~5-minute fallback. Use it sparingly — the whole point of step 6 is to avoid this when the AST says we don't need it.
If step 7 also passes, you're done. Report verdict, grade delta, and the command you ran.
Worked example: editing tree_sitter_analyzer/health_scorer.py
This file is 891 lines, complex, and has many callers — a realistic "feature update" target. Suppose you're adding a new dimension to the health-grading rubric.
Phase A trace
PARALLEL BATCH (one message):
→ edit action=safe file_path="tree_sitter_analyzer/health_scorer.py" edit_type="add_feature"
→ health action=file file_path="tree_sitter_analyzer/health_scorer.py"
→ project action=smart file_path="tree_sitter_analyzer/health_scorer.py"
Returned:
edit action=safe: verdict=REVIEW
risk_factors=[{factor: "hot_zone", detail: "mod_count_30d=7"}]
verification_command="uv run pytest tests/unit/test_health_scorer.py -q"
health action=file: grade=B, score=84, weakest=complexity ← BASELINE
project action=smart: top 3 callers = [health action=project, health action=file, cli/__main__]
Verdict is REVIEW + hot_zone — write a test for the new dimension first.
Phase B (out of scope)
You write the failing test, then edit health_scorer.py to add the new
dimension. Diff is ~40 lines.
Phase C trace
PARALLEL BATCH (one message):
→ health action=file file_path="tree_sitter_analyzer/health_scorer.py"
→ edit action=impact mode="staged"
Returned:
health action=file: grade=B, score=82, weakest=complexity ← B→B, no regression ✓
edit action=impact:
verification_command="uv run pytest tests/unit/test_health_scorer.py
tests/unit/test_file_health_blocks.py
tests/unit/test_file_health_smells.py -q"
pytest_required=true
risk_remains=false ← scoped command is sufficient
queue_ledger=[{file: health_scorer.py, symbols: [grade_file, _compute_dim]}]
THEN Bash (sequential):
$ uv run pytest tests/unit/test_health_scorer.py \
tests/unit/test_file_health_blocks.py \
tests/unit/test_file_health_smells.py -q
... 42 passed in 38.1s ✓ ← ~38s, scoped
(risk_remains=false → skip full suite)
Done. Total verify time: ~38s vs. ~5 min for uv run pytest -q.
That's an ~8× reduction on the verify step alone, and you still ran
the tests the AST said were reachable from your edit.
Phase D — Doc Sync (gate before commit)
If your edits touched any of these surfaces, the corresponding codemap MUST be updated in the SAME commit:
Surface touched (in tree_sitter_analyzer/) |
Codemap to refresh |
|---|---|
mcp/_tool_registry.py (tool added/removed) |
docs/CODEMAPS/mcp-tools.md |
cli/argument_parser_builder.py (new flag/command) |
docs/CODEMAPS/cli.md |
languages/<lang>_plugin/ (new plugin) |
docs/CODEMAPS/languages.md |
formatters/ (new formatter) |
docs/CODEMAPS/formatters.md |
Step D1 — Detect surface change
git diff --cached --name-only
git diff --cached -- tree_sitter_analyzer/mcp/_tool_registry.py | grep -E '^\+.*\("(codegraph_|[a-z_]+",)'
Step D2 — If a surface changed, run the codemap skill
Invoke /update-codemaps — it scans the registries and regenerates the codemap section. Alternatively dispatch the global doc-updater agent for deeper sync (also touches README/AGENTS.md).
Step D3 — Verify with the hard gate
scripts/codemap-sync-check.sh runs automatically as a pre-commit hook and BLOCKs the commit if a surface change is staged but its codemap isn't. To dry-run locally:
bash scripts/codemap-sync-check.sh
echo "exit=$?" # 0 = pass, 1 = block
Escape hatch (intentional rename without semantic change): SKIP_CODEMAP_SYNC=1 git commit .... The pytest test_registered_mcp_tools_have_codemap_parity is the safety net that catches abuse in CI.
Why Phase D exists
The hook (Phase D's hard gate) only catches what's staged at commit time. Phase D in this skill catches drift earlier — at edit-time the agent already knows which codemap to update, so the user never sees the commit-time block. The hook is the fallback for forgotten cases / non-AI commits.
CLI equivalents (when MCP is unavailable)
Run pre-edit (Phase A) in parallel:
uv run tree-sitter-analyzer tree_sitter_analyzer/health_scorer.py \
--safe-to-edit --edit-type add_feature --output-format json
uv run tree-sitter-analyzer tree_sitter_analyzer/health_scorer.py \
--file-health --output-format json
uv run tree-sitter-analyzer tree_sitter_analyzer/health_scorer.py \
--smart-context --output-format json
Run post-edit (Phase C):
uv run tree-sitter-analyzer tree_sitter_analyzer/health_scorer.py \
--file-health --output-format json # grade diff
uv run python -m tree_sitter_analyzer --change-impact \
--change-impact-mode staged --output-format json # gets verification_command
# then copy-paste the verification_command from the JSON output and run it
Verdict cascade (Phase A gate)
edit action=safe verdict |
Action |
|---|---|
UNSAFE |
STOP. Surface risk_factors. Ask user. Do not edit. |
CAUTION |
Narrow scope. Write test first. Then edit. |
REVIEW |
Edit OK; write a test first if "no tests nearby". |
SAFE |
Proceed to Phase B. |
Grade-regression rule (Phase C step 4)
Compare health action=file grade before vs. after:
| Before → After | Verdict | Action |
|---|---|---|
| A → A, B → B, etc. | No regression | Continue to step 5 |
| F → D, D → C, etc. | Improvement | Continue to step 5 |
| A → B, B → C, C → D, D → F | REGRESSION | Investigate weakest_dimension; consider revert or refactor before commit |
A grade regression is a soft fail — surface it to the user, don't auto-abort. Some edits legitimately worsen one dimension while improving overall behavior. Let the user decide.
When to escalate to full pytest (Phase C step 7)
Run uv run pytest -q (full suite, ~5 min) only when ANY of these is true:
edit action=impact risk_remains == true- Grade regressed in step 4 (A→B or worse)
- Edit touched any of:
BaseMCPTool.__init__,PathResolver,SecurityValidator,plugin registry,cli/__main__.py,mcp/server.py - Edit crossed >3 files
edit action=impact queue_ledgershows cross-module reach (impact crossing top-level packages)
Otherwise the scoped verification_command from step 6 is the official
verify gate — no need to burn 5 minutes.
Anti-patterns
- Skipping Phase A because "it's just a small change" — Phase A is ~3s
and catches
UNSAFEconstraint violations that would waste your Phase B effort. - Running
pytest -qimmediately in Phase C without first callingedit action=impact— defeats the whole purpose of this skill. The scoped command is the point. - Ignoring grade regression in Phase C step 4 — a B→C regression on a hot-zone file is a real signal even if tests pass.
- Running each phase in serial MCP calls — Phase A is one parallel batch, Phase C is one parallel batch. Don't make 6 sequential calls.
- Treating
verification_commandas advisory — it's the literal command to run. Don't paraphrase it or "improve" it.
Decision surface returned
phase_a:
verdict: SAFE | REVIEW | CAUTION | UNSAFE
baseline_grade: A | B | C | D | F
baseline_score: 0-100
risk_factors: [...]
phase_c:
current_grade: A | B | C | D | F
current_score: 0-100
grade_regression: bool
verification_command: <exact bash line>
verification_result: PASS | FAIL
risk_remains: bool
ran_full_suite: bool
total_verify_seconds: <int>
verdict: PASS | FAIL | NEEDS_REVIEW
See also
tsa-edit-safety— Phase A alone, when you only want the gate verdicttsa-health-watch— grade tracking over time (daemon)verify(global) — full-suite fallback for pre-commitCLAUDE.md§ Build & Test — the project rule this skill implementsdocs/agent-tooling-gap-report.mdline 58 — the source-of-authority mandate