claude-dev-env 1.28.1 → 1.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/agents/caveman.md +74 -0
  2. package/hooks/blocking/code_rules_enforcer.py +82 -7
  3. package/hooks/blocking/code_rules_path_utils.py +31 -0
  4. package/hooks/blocking/es_exe_path_rewriter.py +159 -0
  5. package/hooks/blocking/hedging_language_blocker.py +12 -2
  6. package/hooks/blocking/test_code_rules_enforcer.py +148 -0
  7. package/hooks/blocking/test_code_rules_enforcer_config_path.py +123 -0
  8. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  9. package/hooks/blocking/test_code_rules_path_utils.py +52 -0
  10. package/hooks/blocking/test_es_exe_path_rewriter.py +369 -0
  11. package/hooks/blocking/test_hedging_language_blocker.py +7 -6
  12. package/hooks/config/dynamic_stderr_handler.py +22 -0
  13. package/hooks/config/path_rewriter_constants.py +13 -0
  14. package/hooks/config/project_paths_reader.py +78 -0
  15. package/hooks/config/setup_project_paths_constants.py +41 -0
  16. package/hooks/config/test_dynamic_stderr_handler.py +48 -0
  17. package/hooks/config/test_messages.py +5 -1
  18. package/hooks/config/test_path_rewriter_constants.py +57 -0
  19. package/hooks/config/test_project_paths_reader.py +149 -0
  20. package/hooks/config/test_setup_project_paths_constants.py +74 -0
  21. package/hooks/git-hooks/test_config.py +1 -0
  22. package/hooks/git-hooks/test_gate_utils.py +1 -0
  23. package/hooks/git-hooks/test_pre_commit.py +1 -0
  24. package/hooks/git-hooks/test_pre_push.py +1 -0
  25. package/hooks/hooks.json +10 -0
  26. package/hooks/session/test_untracked_repo_detector.py +192 -0
  27. package/hooks/session/untracked_repo_detector.py +103 -0
  28. package/hooks/validators/exempt_paths.py +17 -14
  29. package/hooks/validators/test_exempt_paths.py +65 -0
  30. package/hooks/validators/test_git_checks.py +17 -17
  31. package/package.json +1 -1
  32. package/scripts/config/__init__.py +1 -0
  33. package/scripts/config/groq_bugteam_config.py +118 -0
  34. package/scripts/config/test_groq_bugteam_config.py +72 -0
  35. package/scripts/groq_bugteam.README.md +129 -0
  36. package/scripts/groq_bugteam.py +586 -0
  37. package/scripts/setup_project_paths.py +352 -0
  38. package/scripts/test_groq_bugteam.py +391 -0
  39. package/scripts/test_setup_project_paths.py +532 -0
  40. package/scripts/test_setup_project_paths_config.py +6 -0
  41. package/skills/bugteam/CONSTRAINTS.md +1 -1
  42. package/skills/bugteam/PROMPTS.md +1 -1
  43. package/skills/bugteam/SKILL.md +5 -5
  44. package/skills/bugteam/SKILL_EVALS.md +5 -5
  45. package/skills/bugteam/reference/audit-and-teammates.md +3 -3
  46. package/skills/bugteam/reference/audit-contract.md +159 -0
  47. package/skills/bugteam/reference/team-setup.md +2 -2
  48. package/skills/bugteam/scripts/bugteam_preflight.py +66 -0
  49. package/skills/bugteam/scripts/test_bugteam_preflight.py +189 -0
  50. package/skills/copilot-review/SKILL.md +145 -0
  51. package/skills/findbugs/SKILL.md +14 -22
  52. package/skills/qbug/SKILL.md +56 -13
  53. package/skills/qbug/test_qbug_skill_audit_schema.py +156 -0
  54. package/skills/qbug/test_qbug_skill_post_fix_audit.py +103 -0
@@ -22,6 +22,7 @@ Shared artifacts with /bugteam are referenced below by path, using the `${CLAUDE
22
22
  - Pre-flight script: `${CLAUDE_SKILL_DIR}/../bugteam/scripts/bugteam_preflight.py`
23
23
  - Code-rules gate script: `${CLAUDE_SKILL_DIR}/../bugteam/scripts/bugteam_code_rules_gate.py`
24
24
  - Bug category rubric A–J: [`bugteam/PROMPTS.md`](../bugteam/PROMPTS.md#audit-spawn-prompt-xml-bugfind-teammate)
25
+ - **Audit contract** (finding schema, proof-of-absence, adversarial pass, Haiku secondary, post-fix self-audit, diagnostics JSON): [`bugteam/reference/audit-contract.md`](../bugteam/reference/audit-contract.md)
25
26
  - PR comment lifecycle shape: [`bugteam/SKILL.md`](../bugteam/SKILL.md#step-25-pr-comments-one-review-per-loop)
26
27
 
27
28
  ## When this skill applies
@@ -52,6 +53,17 @@ python "${CLAUDE_SKILL_DIR}/../bugteam/scripts/bugteam_preflight.py"
52
53
 
53
54
  `${CLAUDE_SKILL_DIR}` is host-substituted before the shell runs. Non-zero → fix before continuing. `BUGTEAM_PREFLIGHT_SKIP=1` is emergency only. Add `--pre-commit` when `.pre-commit-config.yaml` exists.
54
55
 
56
+ Pre-flight checks (in order):
57
+
58
+ 1. **Git hooks path** — verifies `git -C <repository_root> config --get core.hooksPath` resolves to a path ending in `hooks/git-hooks`. Queries the repository-effective config so repo-level overrides (Husky, lefthook) are detected. If unset or pointing elsewhere, exits non-zero:
59
+ ```
60
+ Git-side CODE_RULES enforcement is not active on this host.
61
+ Run: npx claude-dev-env .
62
+ Or: git config --global core.hooksPath ~/.claude/hooks/git-hooks
63
+ ```
64
+ 2. **pytest** — runs the test suite when `pytest.ini` or `[tool.pytest]` is present.
65
+ 3. **pre-commit** — runs when `--pre-commit` flag is passed and `.pre-commit-config.yaml` exists.
66
+
55
67
  ## Step 1: Resolve PR scope (lead)
56
68
 
57
69
  1. `gh pr view --json number,baseRefName,headRefName,url`
@@ -76,7 +88,7 @@ qbug_temp_dir: Path = Path(tempfile.gettempdir()) / f"qbug-pr-{pr_number}"
76
88
  qbug_temp_dir.mkdir(parents=True, exist_ok=True)
77
89
  ```
78
90
 
79
- ## Step 2: Spawn the single subagent
91
+ ## Step 2: Spawn the primary and secondary audit agents
80
92
 
81
93
  Before calling `Agent`, the lead resolves the three absolute paths the subagent needs and substitutes them into the prompt template (the `<gate_script>`, `<categories_file>`, and `<qbug_temp_dir>` placeholders in § Subagent cycle prompt):
82
94
 
@@ -89,19 +101,27 @@ gate_script_path = (skill_dir / ".." / "bugteam" / "scripts" / "bugteam_code_rul
89
101
  categories_file_path = (skill_dir / ".." / "bugteam" / "PROMPTS.md").resolve()
90
102
  ```
91
103
 
92
- Then call `Agent`:
104
+ Then call `Agent` twice in the same message — the primary clean-coder and the Haiku secondary run in parallel per the audit contract. The Haiku secondary receives an **audit-only** prompt (no FIX step, no git operations) and returns findings to the lead only. The lead merges their findings before the FIX step:
93
105
 
94
106
  ```
95
107
  Agent(
96
108
  subagent_type="clean-coder",
97
- model="sonnet",
98
- description="qbug audit/fix cycle for PR <number>",
109
+ model="opus",
110
+ description="qbug primary audit/fix cycle for PR <number>",
99
111
  prompt="<filled cycle XML; see § Subagent cycle prompt>",
100
112
  run_in_background=False
101
113
  )
114
+
115
+ Agent(
116
+ subagent_type="code-quality-agent",
117
+ model="haiku",
118
+ description="qbug Haiku secondary audit for PR <number>",
119
+ prompt="<audit-only prompt: read the PR diff, apply A-J categories from <categories_file>, return structured findings. No FIX, no git add, no git commit, no git push.>",
120
+ run_in_background=False
121
+ )
102
122
  ```
103
123
 
104
- One subagent, not a team. No `TeamCreate`, no `team_name`, no teammate shutdown protocol. The subagent returns when it has exited the cycle (`converged`, `stuck`, or `error`).
124
+ The Haiku secondary is a read-only auditor per `audit-contract.md` — it returns findings to the lead and never modifies the working tree. The lead merges primary and Haiku secondary findings per the de-dup rules in the audit contract before proceeding. No `TeamCreate`, no `team_name`, no teammate shutdown protocol. The primary subagent returns when it has exited the cycle (`converged`, `stuck`, or `error`).
105
125
 
106
126
  ## Subagent cycle prompt
107
127
 
@@ -128,7 +148,8 @@ The subagent receives this prompt and loops internally — the lead does not re-
128
148
 
129
149
  <exit_conditions>
130
150
  The cycle stops when ONE of these is true. Check on every iteration:
131
- - converged: most recent AUDIT returned zero findings.
151
+ - converged: most recent AUDIT returned zero findings AND
152
+ post_fix_audit_clean is true for the committing loop.
132
153
  - stuck: most recent FIX left `git rev-parse HEAD` unchanged.
133
154
  - error: three consecutive pre-audit gate rounds failed (three is
134
155
  chosen because two is within normal clean-coder variance; four
@@ -166,14 +187,22 @@ The subagent receives this prompt and loops internally — the lead does not re-
166
187
 
167
188
  - Read the patch file.
168
189
  - Audit only added/modified lines. Read <categories_file> for the
169
- A–J category definitions; investigate each category explicitly
170
- and return either at least one finding or a verified-clean
171
- entry with cleared evidence.
172
- - Assign each finding a stable id of the form `loop<N>-<K>`
173
- (N=loop_count, K=1-based within this loop).
174
- - Partition into anchored (line appears in the diff) vs
190
+ A–J category definitions; investigate each category explicitly.
191
+ - Follow the shared audit contract at
192
+ bugteam/reference/audit-contract.md. Per category: produce
193
+ either a Shape A structured finding or a Shape B structured
194
+ proof-of-absence. Bare "verified clean" labels are REJECTED.
195
+ - Run the contract's adversarial second pass after the primary
196
+ finding list.
197
+ - The LEAD spawns the Haiku secondary auditor in parallel with
198
+ this primary audit per the contract's Haiku secondary section.
199
+ - Partition findings into anchored (line appears in the diff) vs
175
200
  unanchored (line does not).
176
201
 
202
+ Persist the merged audit result to
203
+ <qbug_temp_dir>/loop-<loop_count>-audit.json per the contract's
204
+ persistence schema.
205
+
177
206
  Post ONE review per loop. Use the payload shape from
178
207
  <categories_file>'s sibling SKILL.md § "PR comments" — build
179
208
  the JSON with jq `--rawfile` / `-Rs` reading per-finding body
@@ -192,6 +221,7 @@ The subagent receives this prompt and loops internally — the lead does not re-
192
221
 
193
222
  4. FIX:
194
223
  Capture the pre-FIX sha: `pre_fix_sha = git rev-parse HEAD`.
224
+ Capture pre-fix file contents for every file this FIX will touch.
195
225
 
196
226
  Apply each fix. Read every file before editing. Preserve existing
197
227
  comments on lines you do not modify. Add type hints on every
@@ -200,6 +230,16 @@ The subagent receives this prompt and loops internally — the lead does not re-
200
230
  Validate each modified Python file with `python -m py_compile`
201
231
  (or the language-equivalent compile check).
202
232
 
233
+ Compute fix_diff: the diff between pre-fix and post-fix file contents
234
+ for every modified file.
235
+
236
+ Post-fix self-audit: follow the contract's post-fix self-audit
237
+ sequence at bugteam/reference/audit-contract.md. Paranoid mode
238
+ (Haiku secondary in parallel), internal iteration cap = 3, exit
239
+ "stuck: post-fix audit not converging" after 3 rounds with fresh
240
+ findings. Only when gate_findings empty AND post_fix_findings
241
+ empty: proceed to git add.
242
+
203
243
  Stage each modified path by explicit name: `git add <path>`.
204
244
  Create one commit summarizing the fixed findings. Let every git
205
245
  hook run. If a hook blocks the commit, capture its stderr, mark
@@ -208,6 +248,9 @@ The subagent receives this prompt and loops internally — the lead does not re-
208
248
 
209
249
  Push with a plain fast-forward: `git push`.
210
250
 
251
+ Write <qbug_temp_dir>/loop-<loop_count>-diagnostics.json per the
252
+ contract's diagnostics schema (all eight keys required).
253
+
211
254
  Reply to each finding at loop_comment_index[finding_id].finding_comment_id
212
255
  using the reply CLI shape (jq `-Rs` → `gh api .../comments/<id>/replies --input -`).
213
256
  Reply body is one of:
@@ -273,7 +316,7 @@ Delete the resolved `<qbug_temp_dir>` tree and any `.qbug-*.md` temp files in th
273
316
 
274
317
  ## Constraints
275
318
 
276
- - **One subagent, not a team.** Lead spawns a single `clean-coder` via the Agent tool. No `TeamCreate`. The subagent does not spawn further subagents.
319
+ - **One primary + one secondary auditor, not a team.** Lead spawns a `clean-coder` primary (audit + fix cycle) and a `code-quality-agent` Haiku secondary (audit-only, read-only — no FIX, no git). No `TeamCreate`. Neither subagent spawns further subagents.
277
320
  - **No loop cap.** Cycle runs until `converged`, `stuck`, or `error`. User can interrupt.
278
321
  - **Code rules gate before every AUDIT.** Same `validate_content` logic as /bugteam.
279
322
  - **One commit per FIX action.** Linear branch, fast-forward push only.
@@ -0,0 +1,156 @@
1
+ """Tests verifying the shared audit contract and qbug's reference to it.
2
+
3
+ The contract lives in bugteam/reference/audit-contract.md and is the single
4
+ source of truth for finding schema, proof-of-absence shape, adversarial pass,
5
+ Haiku secondary, and de-dup/merge rules. qbug/SKILL.md must reference it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+
13
+ SKILL_FILE_PATH = Path(__file__).parent / "SKILL.md"
14
+ PROMPTS_FILE_PATH = Path(__file__).parent.parent / "bugteam" / "PROMPTS.md"
15
+ CONTRACT_FILE_PATH = (
16
+ Path(__file__).parent.parent / "bugteam" / "reference" / "audit-contract.md"
17
+ )
18
+
19
+
20
+ def _load_skill_text() -> str:
21
+ return SKILL_FILE_PATH.read_text(encoding="utf-8")
22
+
23
+
24
+ def _load_prompts_text() -> str:
25
+ return PROMPTS_FILE_PATH.read_text(encoding="utf-8")
26
+
27
+
28
+ def _load_contract_text() -> str:
29
+ return CONTRACT_FILE_PATH.read_text(encoding="utf-8")
30
+
31
+
32
+ def test_skill_should_reference_audit_contract_by_path() -> None:
33
+ skill_text = _load_skill_text()
34
+ assert "audit-contract.md" in skill_text, (
35
+ "qbug/SKILL.md must reference the shared audit contract by path"
36
+ )
37
+
38
+
39
+ def test_contract_should_require_structured_finding_schema() -> None:
40
+ contract_text = _load_contract_text()
41
+ assert "evidence_files" in contract_text, (
42
+ "Contract must require structured finding with evidence_files[]"
43
+ )
44
+ assert "proof_of_absence" in contract_text, (
45
+ "Contract must require structured proof-of-absence for clean categories"
46
+ )
47
+
48
+
49
+ def test_contract_should_reject_bare_verified_clean_labels() -> None:
50
+ contract_text = _load_contract_text()
51
+ assert "lines_quoted" in contract_text, (
52
+ "Proof-of-absence must require lines_quoted[] not bare 'verified clean'"
53
+ )
54
+ assert "adversarial_probes" in contract_text, (
55
+ "Proof-of-absence must require adversarial_probes[]"
56
+ )
57
+
58
+
59
+ def test_contract_should_require_adversarial_second_pass() -> None:
60
+ contract_text = _load_contract_text()
61
+ assert "Assume your first pass missed" in contract_text, (
62
+ "Contract must include the adversarial second-pass re-prompt"
63
+ )
64
+
65
+
66
+ def test_should_require_haiku_secondary_auditor_spawn() -> None:
67
+ skill_text = _load_skill_text()
68
+ assert "haiku" in skill_text.lower(), (
69
+ "SKILL.md must reference Haiku secondary auditor"
70
+ )
71
+ assert "secondary" in skill_text.lower(), (
72
+ "SKILL.md must reference secondary auditor concept"
73
+ )
74
+
75
+
76
+ def test_contract_should_require_dedup_merge_by_file_line_category() -> None:
77
+ contract_text = _load_contract_text()
78
+ assert (
79
+ "file, line, category" in contract_text
80
+ or "(file, line, category)" in contract_text
81
+ ), "De-dup key must be (file, line, category)"
82
+
83
+
84
+ def test_contract_should_require_severity_max_wins_on_conflict() -> None:
85
+ contract_text = _load_contract_text()
86
+ assert (
87
+ "max wins" in contract_text.lower() or "severity conflict" in contract_text.lower()
88
+ ), "Severity conflict resolution must specify max wins"
89
+
90
+
91
+ def test_should_require_loop_n_audit_json_persistence() -> None:
92
+ skill_text = _load_skill_text()
93
+ assert "loop-" in skill_text and "audit.json" in skill_text, (
94
+ "SKILL.md must reference loop-N-audit.json persistence path"
95
+ )
96
+
97
+
98
+ def test_contract_should_require_findings_and_proof_of_absence_keys_in_json() -> None:
99
+ contract_text = _load_contract_text()
100
+ assert '"findings"' in contract_text or "findings[]" in contract_text, (
101
+ "loop-N-audit.json must have findings[] key"
102
+ )
103
+ assert (
104
+ '"proof_of_absence"' in contract_text or "proof_of_absence[]" in contract_text
105
+ ), "loop-N-audit.json must have proof_of_absence[] key"
106
+
107
+
108
+ def test_contract_should_require_files_opened_in_proof_of_absence() -> None:
109
+ contract_text = _load_contract_text()
110
+ assert "files_opened" in contract_text, (
111
+ "Proof-of-absence struct must include files_opened[]"
112
+ )
113
+
114
+
115
+ def test_step2_spawn_should_include_model_sonnet_parameter() -> None:
116
+ skill_text = _load_skill_text()
117
+ assert 'model="sonnet"' in skill_text, (
118
+ "Step 2 Agent() spawn template must include model=\"sonnet\" for the primary subagent"
119
+ )
120
+
121
+
122
+ def test_step2_spawn_should_reference_clean_coder_and_haiku_secondary() -> None:
123
+ skill_text = _load_skill_text()
124
+ step2_marker = "## Step 2:"
125
+ step3_marker = "## Step 3:"
126
+ step2_start = skill_text.find(step2_marker)
127
+ step3_start = skill_text.find(step3_marker)
128
+ assert step2_start != -1, "SKILL.md must have a Step 2 section"
129
+ assert step3_start != -1, "SKILL.md must have a Step 3 section"
130
+ step2_region = skill_text[step2_start:step3_start]
131
+ assert "clean-coder" in step2_region, (
132
+ "Step 2 must reference the clean-coder primary subagent spawn"
133
+ )
134
+ assert "haiku" in step2_region.lower(), (
135
+ "Step 2 must reference the Haiku secondary auditor spawn"
136
+ )
137
+
138
+
139
+ def test_prompts_md_should_contain_expanded_category_e_dead_code_variants() -> None:
140
+ prompts_text = _load_prompts_text()
141
+ assert (
142
+ "dead parameter" in prompts_text.lower()
143
+ or "dead parameters" in prompts_text.lower()
144
+ ), "Category E must cover dead parameters"
145
+ assert (
146
+ "dead local" in prompts_text.lower() or "dead locals" in prompts_text.lower()
147
+ ), "Category E must cover dead locals"
148
+ assert (
149
+ "dead import" in prompts_text.lower() or "dead imports" in prompts_text.lower()
150
+ ), "Category E must cover dead imports"
151
+ assert (
152
+ "dead branch" in prompts_text.lower() or "dead branches" in prompts_text.lower()
153
+ ), "Category E must cover dead branches"
154
+ assert (
155
+ "dead return" in prompts_text.lower() or "dead returns" in prompts_text.lower()
156
+ ), "Category E must cover dead returns"
@@ -0,0 +1,103 @@
1
+ """Tests verifying qbug SKILL.md contains required post-fix self-audit structural elements.
2
+
3
+ Covers:
4
+ - Post-fix self-audit inserted between py_compile and git add
5
+ - Internal iteration cap of 3
6
+ - loop-N-diagnostics.json with all eight source keys
7
+ - converged condition requires both primary and post-fix audits clean
8
+ - Stuck state when post-fix audit does not converge after 3 iterations
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+
15
+
16
+ SKILL_FILE_PATH = Path(__file__).parent / "SKILL.md"
17
+
18
+
19
+ def _load_skill_text() -> str:
20
+ return SKILL_FILE_PATH.read_text(encoding="utf-8")
21
+
22
+
23
+ def test_should_require_post_fix_gate_before_git_add() -> None:
24
+ skill_text = _load_skill_text()
25
+ assert "bugteam_code_rules_gate" in skill_text, (
26
+ "FIX step must run bugteam_code_rules_gate against modified files"
27
+ )
28
+ assert "post-fix" in skill_text.lower() or "post_fix" in skill_text.lower(), (
29
+ "FIX step must reference a post-fix audit phase"
30
+ )
31
+
32
+
33
+ def test_should_require_post_fix_audit_of_fix_diff() -> None:
34
+ skill_text = _load_skill_text()
35
+ assert "fix_diff" in skill_text, (
36
+ "FIX step must compute fix_diff for the post-fix scoped audit"
37
+ )
38
+
39
+
40
+ def test_should_require_paranoid_mode_with_haiku_on_post_fix() -> None:
41
+ skill_text = _load_skill_text()
42
+ assert "paranoid" in skill_text.lower(), (
43
+ "Post-fix audit must be flagged as paranoid mode with Haiku secondary"
44
+ )
45
+
46
+
47
+ def test_should_require_internal_iteration_cap_of_three() -> None:
48
+ skill_text = _load_skill_text()
49
+ assert "internal iteration cap = 3" in skill_text, (
50
+ "FIX step must specify the exact phrase 'internal iteration cap = 3'"
51
+ )
52
+ assert "stuck: post-fix audit not converging" in skill_text, (
53
+ "Exit message for cap exceeded must be 'stuck: post-fix audit not converging'"
54
+ )
55
+
56
+
57
+ def test_should_only_git_add_when_post_fix_audit_is_clean() -> None:
58
+ skill_text = _load_skill_text()
59
+ post_fix_audit_block_header = "Post-fix self-audit"
60
+ post_fix_audit_block_index = skill_text.find(post_fix_audit_block_header)
61
+ assert post_fix_audit_block_index != -1, (
62
+ f"SKILL.md must contain the literal block header '{post_fix_audit_block_header}'"
63
+ )
64
+ git_add_index = skill_text.find("git add", post_fix_audit_block_index)
65
+ assert git_add_index > post_fix_audit_block_index, (
66
+ "git add must appear after the Post-fix self-audit block header, not before"
67
+ )
68
+
69
+
70
+ def test_should_require_loop_n_diagnostics_json() -> None:
71
+ skill_text = _load_skill_text()
72
+ assert "diagnostics.json" in skill_text, (
73
+ "Each loop must write loop-N-diagnostics.json"
74
+ )
75
+
76
+
77
+ def test_contract_should_require_all_eight_source_keys_in_diagnostics() -> None:
78
+ contract_path = (
79
+ Path(__file__).parent.parent / "bugteam" / "reference" / "audit-contract.md"
80
+ )
81
+ contract_text = contract_path.read_text(encoding="utf-8")
82
+ required_keys = [
83
+ "loop",
84
+ "gate_findings",
85
+ "primary_findings",
86
+ "adversarial_findings",
87
+ "haiku_findings",
88
+ "post_fix_findings",
89
+ "merged",
90
+ "deduped",
91
+ ]
92
+ for each_key in required_keys:
93
+ assert each_key in contract_text, (
94
+ f"loop-N-diagnostics.json schema in audit-contract.md must contain key '{each_key}'"
95
+ )
96
+
97
+
98
+ def test_should_update_exit_conditions_to_require_post_fix_clean() -> None:
99
+ skill_text = _load_skill_text()
100
+ assert (
101
+ "post_fix_audit_clean" in skill_text
102
+ or "post-fix audit clean" in skill_text.lower()
103
+ ), "converged exit condition must require post_fix_audit_clean"