claude-dev-env 1.40.0 → 1.42.0

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 (66) hide show
  1. package/CLAUDE.md +9 -1
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +231 -3
  3. package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +56 -2
  4. package/_shared/pr-loop/scripts/config/claude_settings_keys_constants.py +2 -0
  5. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +173 -6
  6. package/_shared/pr-loop/scripts/post_audit_thread.py +2 -2
  7. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +135 -14
  8. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +33 -0
  10. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +1 -1
  11. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +4 -2
  12. package/hooks/_gh_pr_author_swap_utils.py +1211 -0
  13. package/hooks/blocking/gh_body_arg_blocker.py +9 -6
  14. package/hooks/blocking/gh_pr_author_enforcer.py +480 -0
  15. package/hooks/blocking/gh_pr_author_restore.py +100 -0
  16. package/hooks/blocking/pr_converge_bugteam_enforcer.py +170 -0
  17. package/hooks/blocking/pr_description_enforcer.py +1 -3
  18. package/hooks/blocking/test_gh_body_arg_blocker.py +25 -3
  19. package/hooks/blocking/test_gh_pr_author_enforcer.py +1166 -0
  20. package/hooks/blocking/test_gh_pr_author_restore.py +512 -0
  21. package/hooks/blocking/test_gh_pr_author_swap_utils.py +910 -0
  22. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +311 -0
  23. package/hooks/config/gh_pr_author_swap_constants.py +76 -0
  24. package/hooks/config/pr_converge_bugteam_enforcer_constants.py +55 -0
  25. package/hooks/config/pr_converge_bugteam_enforcer_state.py +67 -0
  26. package/hooks/config/pr_description_enforcer_constants.py +5 -0
  27. package/hooks/config/test_pr_description_enforcer_constants.py +82 -0
  28. package/hooks/hooks.json +40 -0
  29. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +204 -0
  30. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +283 -0
  31. package/hooks/session/gh_pr_author_session_cleanup.py +171 -0
  32. package/hooks/session/test_gh_pr_author_session_cleanup.py +575 -0
  33. package/hooks/test__gh_pr_author_swap_utils.py +333 -0
  34. package/package.json +1 -1
  35. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
  36. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
  37. package/skills/bugteam/reference/audit-contract.md +22 -0
  38. package/skills/bugteam/reference/github-pr-reviews.md +1 -1
  39. package/skills/bugteam/scripts/_claude_permissions_common.py +109 -0
  40. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +8 -2
  41. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +51 -2
  42. package/skills/bugteam/scripts/grant_project_claude_permissions.py +115 -4
  43. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +69 -17
  44. package/skills/bugteam/scripts/test__claude_permissions_common.py +48 -0
  45. package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
  46. package/skills/bugteam/scripts/test_claude_permissions_common.py +18 -10
  47. package/skills/implement/SKILL.md +66 -0
  48. package/skills/implement/scripts/append_note.py +133 -0
  49. package/skills/implement/scripts/config/__init__.py +0 -0
  50. package/skills/implement/scripts/config/notes_constants.py +12 -0
  51. package/skills/implement/scripts/test_append_note.py +191 -0
  52. package/skills/pr-converge/SKILL.md +8 -2
  53. package/skills/pr-converge/config/constants.py +7 -1
  54. package/skills/pr-converge/reference/state-schema.md +36 -8
  55. package/skills/pr-converge/scripts/check_bugbot_ci.py +1 -1
  56. package/skills/pr-converge/scripts/check_convergence.py +167 -28
  57. package/skills/pr-converge/scripts/check_pending_reviews.py +1 -1
  58. package/skills/pr-converge/scripts/conftest.py +60 -0
  59. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +1 -1
  60. package/skills/pr-converge/scripts/post_fix_reply.py +1 -1
  61. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
  62. package/skills/pr-converge/scripts/test_check_convergence.py +306 -0
  63. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +1 -1
  64. package/skills/refine/SKILL.md +257 -0
  65. package/skills/refine/templates/implementation-notes-template.html +56 -0
  66. package/skills/refine/templates/plan-template.md +60 -0
@@ -21,7 +21,7 @@ from unittest.mock import MagicMock, patch
21
21
 
22
22
  import pytest
23
23
 
24
- _SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
24
+ _SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
25
25
 
26
26
 
27
27
  @pytest.fixture(scope="session")
@@ -0,0 +1,306 @@
1
+ """Tests for check_convergence.
2
+
3
+ Covers the bugteam audit gate (`_check_bugteam_clean`) which identifies
4
+ bugteam reviews by body header signature rather than by the posting user's
5
+ GitHub login. Three scenarios are exercised:
6
+
7
+ - a clean bugteam review on the current HEAD passes the gate
8
+ - a dirty bugteam review on the current HEAD fails the gate
9
+ - the absence of any bugteam review on the current HEAD fails the gate
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib.util
15
+ import json
16
+ import sys
17
+ from pathlib import Path
18
+ from types import ModuleType
19
+ from typing import Callable
20
+
21
+ import pytest
22
+
23
+ _SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
24
+ _PR_CONVERGE_DIRECTORY = _SCRIPTS_DIRECTORY.parent
25
+
26
+ if str(_PR_CONVERGE_DIRECTORY) not in sys.path:
27
+ sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
28
+
29
+
30
+ def _load_module() -> ModuleType:
31
+ for each_cached_name in [
32
+ each_key
33
+ for each_key in list(sys.modules)
34
+ if each_key == "config" or each_key.startswith("config.")
35
+ ]:
36
+ sys.modules.pop(each_cached_name, None)
37
+ if str(_PR_CONVERGE_DIRECTORY) in sys.path:
38
+ sys.path.remove(str(_PR_CONVERGE_DIRECTORY))
39
+ sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
40
+ module_path = _SCRIPTS_DIRECTORY / "check_convergence.py"
41
+ spec = importlib.util.spec_from_file_location(
42
+ "check_convergence_under_test", module_path
43
+ )
44
+ assert spec is not None
45
+ assert spec.loader is not None
46
+ module = importlib.util.module_from_spec(spec)
47
+ spec.loader.exec_module(module)
48
+ return module
49
+
50
+
51
+ check_convergence = _load_module()
52
+
53
+ CURRENT_HEAD_SHA = "abcdef1234567890abcdef1234567890abcdef12"
54
+ OTHER_HEAD_SHA = "0000000000000000000000000000000000000000"
55
+ CLEAN_BUGTEAM_BODY = (
56
+ "**Bugteam audit completed** —— Clean — no findings\n"
57
+ "\n"
58
+ "---\n"
59
+ "### Audit pass clean\n"
60
+ "\n"
61
+ "The Bugteam audit pass against commit `abcdef1` found no findings.\n"
62
+ )
63
+ DIRTY_BUGTEAM_BODY = (
64
+ "**Bugteam audit completed** —— Findings requested\n"
65
+ "\n"
66
+ "---\n"
67
+ "### Findings recorded as inline review comments\n"
68
+ "\n"
69
+ "The Bugteam audit pass against commit `abcdef1` surfaced 2 finding(s).\n"
70
+ )
71
+ NON_BUGTEAM_BODY = (
72
+ "Cursor Bugbot has reviewed your changes and found 0 potential issues."
73
+ )
74
+
75
+
76
+ def _make_stub_gh_paginated(
77
+ all_review_objects: list[dict[str, object]],
78
+ ) -> Callable[[str], tuple[int, str]]:
79
+ pages_payload = [all_review_objects]
80
+ serialized = json.dumps(pages_payload)
81
+
82
+ def stub_gh_api_paginated(endpoint_path: str) -> tuple[int, str]:
83
+ return 0, serialized
84
+
85
+ return stub_gh_api_paginated
86
+
87
+
88
+ def should_pass_when_clean_bugteam_review_present_on_current_head(
89
+ monkeypatch: pytest.MonkeyPatch,
90
+ ) -> None:
91
+ reviews_payload = [
92
+ {
93
+ "id": 1001,
94
+ "body": CLEAN_BUGTEAM_BODY,
95
+ "commit_id": CURRENT_HEAD_SHA,
96
+ "submitted_at": "2026-05-17T12:00:00Z",
97
+ }
98
+ ]
99
+ stub = _make_stub_gh_paginated(reviews_payload)
100
+ monkeypatch.setattr(check_convergence, "_gh_api_paginated", stub)
101
+ passed, detail = check_convergence._check_bugteam_clean(
102
+ owner="JonEcho",
103
+ repo="tests",
104
+ number=42,
105
+ head_sha=CURRENT_HEAD_SHA,
106
+ )
107
+ assert passed is True
108
+ assert "clean bugteam audit" in detail
109
+ assert CURRENT_HEAD_SHA[:7] in detail
110
+
111
+
112
+ def should_fail_when_dirty_bugteam_review_present_on_current_head(
113
+ monkeypatch: pytest.MonkeyPatch,
114
+ ) -> None:
115
+ reviews_payload = [
116
+ {
117
+ "id": 1002,
118
+ "body": DIRTY_BUGTEAM_BODY,
119
+ "commit_id": CURRENT_HEAD_SHA,
120
+ "submitted_at": "2026-05-17T12:00:00Z",
121
+ }
122
+ ]
123
+ stub = _make_stub_gh_paginated(reviews_payload)
124
+ monkeypatch.setattr(check_convergence, "_gh_api_paginated", stub)
125
+ passed, detail = check_convergence._check_bugteam_clean(
126
+ owner="JonEcho",
127
+ repo="tests",
128
+ number=42,
129
+ head_sha=CURRENT_HEAD_SHA,
130
+ )
131
+ assert passed is False
132
+ assert "dirty bugteam audit" in detail
133
+ assert CURRENT_HEAD_SHA[:7] in detail
134
+
135
+
136
+ def should_fail_when_no_bugteam_review_present_on_current_head(
137
+ monkeypatch: pytest.MonkeyPatch,
138
+ ) -> None:
139
+ reviews_payload = [
140
+ {
141
+ "id": 1003,
142
+ "body": NON_BUGTEAM_BODY,
143
+ "commit_id": CURRENT_HEAD_SHA,
144
+ "submitted_at": "2026-05-17T12:00:00Z",
145
+ },
146
+ {
147
+ "id": 1004,
148
+ "body": CLEAN_BUGTEAM_BODY,
149
+ "commit_id": OTHER_HEAD_SHA,
150
+ "submitted_at": "2026-05-17T11:00:00Z",
151
+ },
152
+ ]
153
+ stub = _make_stub_gh_paginated(reviews_payload)
154
+ monkeypatch.setattr(check_convergence, "_gh_api_paginated", stub)
155
+ passed, detail = check_convergence._check_bugteam_clean(
156
+ owner="JonEcho",
157
+ repo="tests",
158
+ number=42,
159
+ head_sha=CURRENT_HEAD_SHA,
160
+ )
161
+ assert passed is False
162
+ assert "no bugteam review found" in detail
163
+
164
+
165
+ def should_fail_with_shape_detail_when_gh_returns_non_list_payload(
166
+ monkeypatch: pytest.MonkeyPatch,
167
+ ) -> None:
168
+ error_object_payload = {"message": "Not Found", "documentation_url": "https://docs.github.com/rest"}
169
+ serialized_error = json.dumps(error_object_payload)
170
+
171
+ def stub_gh_api_paginated_returning_object(endpoint_path: str) -> tuple[int, str]:
172
+ return 0, serialized_error
173
+
174
+ monkeypatch.setattr(
175
+ check_convergence, "_gh_api_paginated", stub_gh_api_paginated_returning_object
176
+ )
177
+ passed, detail = check_convergence._check_bugteam_clean(
178
+ owner="JonEcho",
179
+ repo="tests",
180
+ number=42,
181
+ head_sha=CURRENT_HEAD_SHA,
182
+ )
183
+ assert passed is False
184
+ assert "unexpected gh api response shape" in detail
185
+
186
+
187
+ def test_private_helpers_recognize_clean_new_header_body() -> None:
188
+ assert check_convergence._is_bugteam_review(CLEAN_BUGTEAM_BODY) is True
189
+ assert check_convergence._is_clean_bugteam_review(CLEAN_BUGTEAM_BODY) is True
190
+
191
+
192
+ def test_private_helpers_recognize_dirty_new_header_body() -> None:
193
+ assert check_convergence._is_bugteam_review(DIRTY_BUGTEAM_BODY) is True
194
+ assert check_convergence._is_clean_bugteam_review(DIRTY_BUGTEAM_BODY) is False
195
+
196
+
197
+ def test_private_helpers_reject_non_bugteam_body() -> None:
198
+ assert check_convergence._is_bugteam_review(NON_BUGTEAM_BODY) is False
199
+ assert check_convergence._is_clean_bugteam_review(NON_BUGTEAM_BODY) is False
200
+
201
+
202
+ CLEAN_LEGACY_BUGTEAM_BODY = (
203
+ "## /bugteam loop 1 audit: 0 P0 / 0 P1 / 0 P2 → clean"
204
+ )
205
+ DIRTY_LEGACY_BUGTEAM_BODY = (
206
+ "## /bugteam loop 1 audit: 1 P0 / 0 P1 / 0 P2 → dirty"
207
+ )
208
+
209
+
210
+ def test_private_helpers_recognize_clean_legacy_header_body() -> None:
211
+ assert check_convergence._is_bugteam_review(CLEAN_LEGACY_BUGTEAM_BODY) is True
212
+ assert check_convergence._is_clean_bugteam_review(CLEAN_LEGACY_BUGTEAM_BODY) is True
213
+
214
+
215
+ def test_private_helpers_recognize_dirty_legacy_header_body() -> None:
216
+ assert check_convergence._is_bugteam_review(DIRTY_LEGACY_BUGTEAM_BODY) is True
217
+ assert check_convergence._is_clean_bugteam_review(DIRTY_LEGACY_BUGTEAM_BODY) is False
218
+
219
+
220
+ def should_bypass_bugbot_gates_when_bugbot_down_is_true(
221
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
222
+ ) -> None:
223
+ all_invocation_names: list[str] = []
224
+
225
+ def stub_get_pr_head_sha(*, owner: str, repo: str, number: int) -> str:
226
+ all_invocation_names.append("_get_pr_head_sha")
227
+ return CURRENT_HEAD_SHA
228
+
229
+ def stub_check_bugbot_should_not_be_called(
230
+ *, owner: str, repo: str, sha: str
231
+ ) -> tuple[bool, str]:
232
+ all_invocation_names.append("_check_bugbot")
233
+ raise AssertionError("_check_bugbot must not be invoked when bugbot_down=True")
234
+
235
+ def stub_check_bugbot_not_dirty_should_not_be_called(
236
+ *, owner: str, repo: str, number: int, head_sha: str
237
+ ) -> tuple[bool, str]:
238
+ all_invocation_names.append("_check_bugbot_not_dirty")
239
+ raise AssertionError(
240
+ "_check_bugbot_not_dirty must not be invoked when bugbot_down=True"
241
+ )
242
+
243
+ def stub_check_bugteam_clean(
244
+ *, owner: str, repo: str, number: int, head_sha: str
245
+ ) -> tuple[bool, str]:
246
+ all_invocation_names.append("_check_bugteam_clean")
247
+ return True, "stub passing"
248
+
249
+ def stub_check_bot_review(
250
+ *,
251
+ owner: str,
252
+ repo: str,
253
+ number: int,
254
+ head_sha: str,
255
+ login_substring: str,
256
+ clean_states: tuple[str, ...],
257
+ dirty_states: tuple[str, ...],
258
+ label: str,
259
+ ) -> tuple[bool, str]:
260
+ all_invocation_names.append("_check_bot_review")
261
+ return True, "stub passing"
262
+
263
+ def stub_count_unresolved_bot_threads(
264
+ *, owner: str, repo: str, number: int
265
+ ) -> tuple[bool, str]:
266
+ all_invocation_names.append("_count_unresolved_bot_threads")
267
+ return True, "stub passing"
268
+
269
+ def stub_get_mergeable(
270
+ *, owner: str, repo: str, number: int
271
+ ) -> tuple[bool, str]:
272
+ all_invocation_names.append("_get_mergeable")
273
+ return True, "stub passing"
274
+
275
+ def stub_check_no_pending_reviews(
276
+ *, owner: str, repo: str, number: int
277
+ ) -> tuple[bool, str]:
278
+ all_invocation_names.append("_check_no_pending_reviews")
279
+ return True, "stub passing"
280
+
281
+ monkeypatch.setattr(check_convergence, "_get_pr_head_sha", stub_get_pr_head_sha)
282
+ monkeypatch.setattr(check_convergence, "_check_bugbot", stub_check_bugbot_should_not_be_called)
283
+ monkeypatch.setattr(
284
+ check_convergence,
285
+ "_check_bugbot_not_dirty",
286
+ stub_check_bugbot_not_dirty_should_not_be_called,
287
+ )
288
+ monkeypatch.setattr(check_convergence, "_check_bugteam_clean", stub_check_bugteam_clean)
289
+ monkeypatch.setattr(check_convergence, "_check_bot_review", stub_check_bot_review)
290
+ monkeypatch.setattr(
291
+ check_convergence, "_count_unresolved_bot_threads", stub_count_unresolved_bot_threads
292
+ )
293
+ monkeypatch.setattr(check_convergence, "_get_mergeable", stub_get_mergeable)
294
+ monkeypatch.setattr(
295
+ check_convergence, "_check_no_pending_reviews", stub_check_no_pending_reviews
296
+ )
297
+
298
+ exit_code = check_convergence.check_all(
299
+ owner="o", repo="r", number=1, bugbot_down=True
300
+ )
301
+ captured_stdout = capsys.readouterr().out
302
+
303
+ assert "_check_bugbot" not in all_invocation_names
304
+ assert "_check_bugbot_not_dirty" not in all_invocation_names
305
+ assert "bypassed (bugbot_down)" in captured_stdout
306
+ assert exit_code == 0
@@ -6,7 +6,7 @@ import sys
6
6
  import types
7
7
  from pathlib import Path
8
8
 
9
- _scripts_directory = Path(__file__).resolve().parent
9
+ _scripts_directory = Path(__file__).absolute().parent
10
10
  if str(_scripts_directory) not in sys.path:
11
11
  sys.path.insert(0, str(_scripts_directory))
12
12
 
@@ -0,0 +1,257 @@
1
+ ---
2
+ name: refine
3
+ description: Interview-driven plan refiner with built-in audit loop. Takes a draft, a topic, or the active conversation; fans out research agents; interviews via AskUserQuestion until further questions would be impractical hypotheticals; writes the plan to the Obsidian vault under Research/<topic>/<slug>.md; then loops a general-purpose audit and fix pass (both with plan-quality rubrics) until the plan is clean, with a sibling <slug>-implementation-notes.html capturing design decisions, deviations, tradeoffs, and open questions across iterations. The interview is mandatory and the vault is the only output target — these survive any session-level "no clarifying questions" or local plans/ shortcuts. Use when the user says /refine, "refine this", "turn this into a plan", "flesh this out", "make a spec for this", "let's plan this out", or asks for a vague idea to be matured into a plan. Always operates on plans — skill plans, new-code implementation plans, or code-refinement plans.
4
+ ---
5
+
6
+ # refine
7
+
8
+ Walk a half-formed plan to a complete, audited implementation spec — research first, interview only what research cannot answer, write the result to the Obsidian vault, then loop a general-purpose audit and fix pass until the plan is clean. **A plan and a vault, always.**
9
+
10
+ ## Gotchas
11
+
12
+ - **Pre-work, not pre-interrogation.** Run fan-out agents over the draft, vault, and repo before asking the user anything substantive. The one explicit exception is the topic-confirmation AskUserQuestion when no draft and no topic argument are present (see the conversation-context fallback gotcha below) — that single question fires *before* fan-out so the wrong topic does not drive the research. After it, no further user questions until fan-out completes. Questions an agent could resolve still violate the verify-before-asking rule.
13
+ - **Layered fan-out, not blanket parallelism.** First pass: a single Explore agent across draft + vault + relevant repo area. Dispatch one parallel agent per source only if the Explore pass runs long or returns thin results.
14
+ - **AskUserQuestion is the only interview tool.** Plain-text questions in chat get blocked by the `question_to_user_enforcer` Stop hook. Every interview turn calls AskUserQuestion.
15
+ - **Interview is mandatory — overrides "no clarifying questions" directives.** /refine is interview-driven by definition. A session-level "work without clarifying questions" instruction, autonomous-mode flag, or bg-session preamble does NOT silence the interview. If something genuinely prevents AskUserQuestion from firing, halt and surface the conflict to the user rather than proceeding silently. There is no "skip the interview" branch.
16
+ - **Skill writes the plan inline.** Do not hand the initial write to any subagent. Assemble the answers and write the markdown directly. (The fix agent enters later, only for audit-driven fixes.)
17
+ - **Vault output, split by file type.** The output target is the Obsidian vault. The markdown plan (`<slug>.md`) is written through `mcp__obsidian__write_note` — that MCP is markdown-only by codebase precedent (every existing caller writes `.md`). The HTML notes file (`<slug>-implementation-notes.html`) is written through the filesystem Write/Edit tools resolving the vault path via `$OBSIDIAN_VAULT_PATH` (or `~/.claude/vault/` fallback), mirroring the `session-log` skill. Do not write to project `docs/`, `.claude/plans/`, `$CLAUDE_JOB_DIR`, the cwd, or anywhere else outside the vault subtree.
18
+ - **Never write in place even when a local plans/ folder exists.** The presence of `.claude/plans/`, `docs/plans/`, `plans/`, or any sibling plans directory in the cwd does NOT override the vault contract. Do not write the plan in-place "as a convenience" or "to keep it near the codebase." Do not dual-write. The vault is the canonical home; the local repo never receives the plan from this skill.
19
+ - **Slug is user-controlled.** Propose `Research/<topic>/<slug>.md` and confirm slug + path via AskUserQuestion before writing. Auto-writing breaks the user-owned-output contract. The user may choose the topic subfolder, but the `Research/` prefix is fixed. Both `<topic>` and `<slug>` must match `^[a-z0-9-]+$` — reject any value containing path separators (`/`, `\`), traversal segments (`..`), uppercase letters, whitespace, or any other character. Reprompt the user with a corrected proposal rather than writing.
20
+ - **Match before fresh.** If the fan-out surfaces existing plans on the same topic, ask the user which to refine or whether to start fresh. Skipping this step duplicates work the user already started.
21
+ - **Standalone.** Do not invoke `/anthropic-plan` or `/prompt-generator`. The user picks the slash command for the moment; this skill does not chain.
22
+ - **Conversation-context fallback needs confirmation.** When no draft and no topic argument are present, the active conversation is the source. This is the named carve-out to the pre-work rule above: a single AskUserQuestion confirming the inferred topic must fire *before* fan-out begins, so the wrong topic does not drive twenty minutes of research. After that single confirmation, no further user questions until fan-out completes.
23
+ - **Audit cycle is mandatory.** After the plan is written, spawn `general-purpose` with the plan-quality rubric to audit it; spawn `general-purpose` again with the fix rubric to address flagged findings; re-audit; loop. Skip only when the user explicitly opts out for the current run. Do not use `code-quality-agent` or `clean-coder` — both target source code (clean-coder's own definition excludes planning and audit artifacts), not markdown plans.
24
+ - **Verbatim notes instruction.** Every fix-agent iteration receives the exact `<notes_instruction>` block in §8 unchanged. The notes file is how the user reconstructs what the fixer did to the spec.
25
+ - **`<slug>-implementation-notes.html` is append-only across iterations.** The notes file lives at `Research/<topic>/<slug>-implementation-notes.html` resolved against the vault root. Each iteration appends one `<section>` block via the filesystem Edit tool — never overwrites earlier iterations. HTML cannot go through `mcp__obsidian__write_note`; see the vault-output gotcha above.
26
+ - **Cap at 10 audit iterations.** If the plan still fails audit after 10 rounds, halt and surface open findings. Do not raise the cap without user direction.
27
+
28
+ ## When this skill applies
29
+
30
+ **Triggers:** `/refine`, or natural phrases — "refine this", "turn this into a plan", "flesh this out", "make a spec for this", "let's plan this out".
31
+
32
+ **Always operates on plans.** Three flavors:
33
+
34
+ 1. **Skill plans** — skill scaffolding, slash-command behavior, agent prompts
35
+ 2. **New-code implementation plans** — feature work, automation, hook design, refactor sequencing
36
+ 3. **Code-refinement plans** — hardening an existing implementation, addressing review feedback, fleshing out a draft
37
+
38
+ **Refusal cases — first match wins:**
39
+
40
+ - **Topic is a direct task, not a plan to refine.** Respond exactly: `That looks like a direct task, not a plan to refine. Tell me to just do it, or describe what you want planned.`
41
+ - **User wants a quick suggestion, not a written spec.** Respond exactly: `Sounds like a question, not a refinement. I can answer here without writing a vault file — say the word if you'd rather do the full /refine pass.`
42
+ - **Upstream directive blocks AskUserQuestion or the vault MCP.** Respond exactly: `/refine needs the interview and a writable Obsidian vault. The current session is blocking one of those — confirm you want to lift the block, or I can stop here.`
43
+
44
+ ## The Process
45
+
46
+ Copy this checklist into your response and tick items as they complete.
47
+
48
+ ```
49
+ - [ ] 1. Resolve the topic
50
+ - [ ] 2. Layered fan-out (Explore first; parallel-per-source on escalation)
51
+ - [ ] 3. Existing-match decision (refine a match OR start fresh)
52
+ - [ ] 4. Interview loop via AskUserQuestion (mandatory; do not skip)
53
+ - [ ] 5. Propose slug + Research/<topic>/<slug>.md; confirm via AskUserQuestion
54
+ - [ ] 6. Write the plan inline via mcp__obsidian__write_note (vault only)
55
+ - [ ] 7. Initial audit via general-purpose (plan-quality rubric)
56
+ - [ ] 8. Audit-fix loop: general-purpose fix + verbatim notes instruction + re-audit
57
+ - [ ] 9. Cap at 10 iterations; halt and surface open findings if not clean
58
+ - [ ] 10. Report vault paths, iterations used, notes summary
59
+ ```
60
+
61
+ ### 1. Resolve the topic
62
+
63
+ Identify what is being refined:
64
+
65
+ - **$ARGUMENTS is a path** to a file → that file is the draft
66
+ - **$ARGUMENTS is a phrase** → that phrase is the topic seed
67
+ - **No $ARGUMENTS** → scan the active conversation for the dominant topic, then call AskUserQuestion once to confirm the inferred topic before any fan-out
68
+
69
+ ### 2. Layered fan-out
70
+
71
+ > **Do not ask the user anything the fan-out can answer.** Read the draft, search the vault, glance at the repo — then frame questions around real gaps.
72
+
73
+ **Layer A — single Explore agent (default):**
74
+
75
+ Spawn one `Explore` agent (`subagent_type: Explore`) with breadth `medium`. Prompt it to:
76
+
77
+ - Read the draft in full (if a draft was identified)
78
+ - Search the Obsidian vault for related plans, session logs, and decisions via `mcp__obsidian__search_notes` (include `searchFrontmatter: true` to catch `project` matches in session reports)
79
+ - Glance at the relevant repo area — sibling skills under `~/.claude/skills/` for a skill plan, hooks under `~/.claude/hooks/` for a hook plan, the cwd project source for a code plan
80
+
81
+ Have it return a structured digest: existing plans found, vault notes referenced, repo files touched, gaps it could not fill.
82
+
83
+ **Layer B — parallel-per-source (escalation):**
84
+
85
+ If Layer A returns thin results, runs unusually long, or the topic spans multiple domains, dispatch one parallel subagent per source in a single message:
86
+
87
+ | Subagent | Source |
88
+ |---|---|
89
+ | `Explore` (vault) | Obsidian vault search + read top matches |
90
+ | `Explore` (repo) | Targeted repo area (skills/hooks/source) |
91
+ | `general-purpose` (draft) | Deep read of the draft + every file it references |
92
+
93
+ Wait for all to complete, merge the digests, proceed.
94
+
95
+ ### 3. Existing-match decision
96
+
97
+ If the fan-out surfaces plans that look like prior work on the same topic, run an AskUserQuestion. AskUserQuestion's contract caps options at four, so cap the displayed matches at three to leave room for "Start fresh":
98
+
99
+ - Up to three match options, one per match (label = match title, description = match path + one-line summary). When more than three matches surface, name the top three by recency or relevance and list the remainder in the question prose so the user can name a specific one via the free-text fallback.
100
+ - A "Start fresh" option
101
+
102
+ The user's pick becomes the working draft for the interview. The chosen draft's decisions seed the plan; the interview targets gaps.
103
+
104
+ ### 4. Interview loop
105
+
106
+ > **Do not skip the interview.** A session-level "no clarifying questions" directive, autonomous-mode flag, or bg-session preamble does not silence /refine. The interview IS the skill. If AskUserQuestion is genuinely blocked, fall back to the third refusal case and halt rather than proceed silently.
107
+
108
+ Use `AskUserQuestion` for every question. Pick the shape that fits the moment:
109
+
110
+ - **Parallel batch of 4** — probing distinct breadth dimensions (purpose, output, audience, scope)
111
+ - **Themed batch of 2–3** — depth across a few sub-decisions in one area
112
+ - **Single deep question** — when the next answer forks the remainder of the interview
113
+
114
+ **Coverage to drive toward** (not a checklist to walk — let the answers steer):
115
+
116
+ - Goal and non-goals (what the plan delivers and what it deliberately excludes)
117
+ - Current state grounding (files, hooks, skills, prior decisions the plan touches)
118
+ - Implementation path (steps, file paths, agents to spawn, hooks to add or change)
119
+ - Decisions and tradeoffs (each meaningful choice + reasoning)
120
+ - Risks and open questions (what could break, what is left to resolve)
121
+ - Acceptance criteria (concrete observable signals — file existence, command output, behavior — that verify the plan was implemented correctly)
122
+
123
+ **Stop condition:** further questions would require inventing impractical scenarios (e.g. "what if the user has 10,000 concurrent invocations?"). When the marginal question feels like reaching, stop.
124
+
125
+ ### 5. Propose path + slug
126
+
127
+ Compose `Research/<topic>/<slug>.md`:
128
+
129
+ - `Research/` prefix is fixed — non-negotiable, even when the cwd has a local plans folder
130
+ - `<topic>` — kebab-case directory matching the dominant subject area (`skills`, `hooks`, `pr-converge`, `themes`, etc.). Must match `^[a-z0-9-]+$`.
131
+ - `<slug>` — kebab-case slug capturing the specific plan (`refine-skill`, `bugteam-orphan-fallback`). Must match `^[a-z0-9-]+$`.
132
+
133
+ Call AskUserQuestion with the proposed path as the first option and "Edit slug/path" as the second option. Accept the user's edit to slug or topic before writing. Reject any edit that:
134
+
135
+ - Contains path separators (`/`, `\`), traversal segments (`..`), uppercase letters, whitespace, or any character outside `[a-z0-9-]`
136
+ - Moves the file out of the `Research/` vault subtree
137
+
138
+ On reject, re-call AskUserQuestion with a corrected proposal rather than writing.
139
+
140
+ ### 6. Write the plan
141
+
142
+ > **Vault only.** Even if the cwd contains `.claude/plans/`, `docs/plans/`, or any local plans directory holding the user's prior drafts, do not write the plan there. Do not write a copy there. The vault path is the only target.
143
+
144
+ Load the structure from `templates/plan-template.md`. Fold in:
145
+
146
+ - **YAML frontmatter (mandatory)** → replace every placeholder in the YAML block at the top of the template: `project` with the kebab-case topic, `date` with today's date as `YYYY-MM-DD`, `status` with `Draft`, and `tags` keeping `refine, plan` plus any topic-specific tags surfaced during the interview. Leaving placeholder tokens (e.g. `<project-or-topic-area>`, `<YYYY-MM-DD>`) is a Completeness failure that Step 7 must catch.
147
+ - **H1 title (mandatory)** → replace `<Plan title>` with a concrete title that matches the plan's primary outcome.
148
+ - Fan-out digest → **Current state**
149
+ - Interview answers (Goal/Non-goals/Implementation/Decisions) → **Goal / Non-goals / Implementation / Decisions log**
150
+ - Interview answers about verification signals → **Acceptance**
151
+ - Stop-point items the interview surfaced → **Risks / Open questions**
152
+
153
+ Write the file via `mcp__obsidian__write_note` to the confirmed vault path. The skill itself does this — no subagent.
154
+
155
+ ### 7. Initial audit
156
+
157
+ Spawn `general-purpose` (`subagent_type: general-purpose`, foreground) with:
158
+
159
+ - The plan file path in the vault
160
+ - The plan-quality rubric — the agent audits markdown plan content (not source code) against these categories:
161
+ - **Clarity** — every step is uniquely interpretable; no vague verbs ("handle", "process", "manage") or undefined terms
162
+ - **Completeness** — Goal, Non-goals, Current state, Implementation, Decisions log, Risks, and Acceptance are all populated with concrete content (not placeholders); the YAML frontmatter has every placeholder replaced (no `<project-or-topic-area>`, `<YYYY-MM-DD>`, or similar tokens remain); the H1 carries a concrete title (no `<Plan title>` placeholder)
163
+ - **Internal consistency** — no contradictions between sections; no references to files, agents, or commands that contradict another section
164
+ - **Ambiguity** — no parked open questions where a decision is required for implementation to begin
165
+ - **Implementer-readiness** — a downstream implementer can act on each step without back-and-forth (file paths named, agents named, change concrete)
166
+ - A required return shape: structured findings as `severity (P0/P1/P2) | location | violation`, plus an explicit `CLEAN` verdict when no findings remain
167
+ - An explicit instruction NOT to apply code-review rubrics (CODE_RULES categories A–K, API contracts, resource cleanup, etc.) — the audit target is a markdown plan, not source code
168
+
169
+ If the verdict is `CLEAN`: skip step 8 and proceed to step 10.
170
+
171
+ If findings exist: proceed to step 8.
172
+
173
+ ### 8. Audit-fix loop
174
+
175
+ For each iteration `N` from 1 to 10:
176
+
177
+ 1. Spawn `general-purpose` (`subagent_type: general-purpose`, foreground) with the fix-agent role and:
178
+ - The plan file path
179
+ - The structured findings from the latest audit
180
+ - The path to `Research/<topic>/<slug>-implementation-notes.html`
181
+ - The path to `templates/implementation-notes-template.html` and a directive: on iteration 1 only, check whether the notes file already exists. If it does NOT exist, copy the template into the notes file path via the filesystem **Write** tool and substitute every `{{slug}}` placeholder (in the page `<title>`, the `<h1>`, and both occurrences inside the `<a href="{{slug}}.md">{{slug}}.md</a>` companion link) with the actual slug from the vault plan path. If it DOES exist (the user is re-running `/refine` on a slug that already has notes), skip the template-copy step entirely — the existing file already carries the `{{slug}}` substitutions from a prior run, and the filesystem **Write** tool is rejected on existing paths by the `write_existing_file_blocker` PreToolUse hook. On every iteration (including 1), copy the iteration `<section class="iteration">` markup from the HTML-commented reference block at the top of the template, substitute its placeholders (`<N>` → iteration number; `<YYYY-MM-DD HH:MM>` → UTC timestamp; `<count>` → number of findings addressed this iteration; each `<ul>` group → bullets covering Design decisions, Deviations, Tradeoffs, Open questions), and insert the populated `<section>` via the filesystem **Edit** tool immediately before the closing `</body>` tag
182
+ - The verbatim `<notes_instruction>` block below
183
+ 2. The fix agent rewrites the plan in place in the vault via `mcp__obsidian__write_note` addressing the findings. Separately, it appends one new `<section>` to the HTML notes file via the filesystem Edit tool (`mcp__obsidian__write_note` is markdown-only and cannot write `.html`) — with iteration number, timestamp, and the four bullet groups.
184
+ 3. Re-spawn `general-purpose` against the rewritten plan with the same audit prompt as step 7 (plan-quality rubric, not code rubric).
185
+ 4. If the verdict is `CLEAN`: exit the loop and proceed to step 10.
186
+ 5. If findings remain and `N < 10`: continue the loop with the new findings.
187
+ 6. If `N == 10` and findings remain: proceed to step 9.
188
+
189
+ #### Verbatim instruction for the fix agent
190
+
191
+ Pass this block exactly, every iteration. Do not paraphrase or trim.
192
+
193
+ ```
194
+ <notes_instruction>
195
+ As you work maintain a running implementation-notes.html file that captures anything I should know about how the implementation diverges from or interprets the spec, including:
196
+
197
+ - Design decisions: choices you made where the spec was ambiguous
198
+ - Deviations: places where you intentionally departed from the spec, and why
199
+ - Tradeoffs: alternatives you considered and why you picked what you did
200
+ - Open questions: anything you'd want me to confirm or revise
201
+ </notes_instruction>
202
+ ```
203
+
204
+ #### Notes file structure
205
+
206
+ Use `templates/implementation-notes-template.html` as the skeleton on iteration 1. The absolute path resolves the vault root from `$OBSIDIAN_VAULT_PATH` or `~/.claude/vault/`. Two iteration-1 paths exist:
207
+
208
+ - **Fresh slug — notes file does not yet exist.** Copy the template via the filesystem **Write** tool and substitute every `{{slug}}` placeholder in the title, h1, and companion-link anchor with the actual slug before writing.
209
+ - **Re-run on an existing slug — notes file already exists.** The repo's `write_existing_file_blocker` PreToolUse hook rejects **Write** on existing paths. Skip the template-copy step (the existing file already carries `{{slug}}` substitutions from the prior run) and proceed straight to the append using **Edit**.
210
+
211
+ On iterations 2+ (and on iteration 1 against an existing notes file), use the filesystem **Read** + **Edit** tools — read the existing file, append a new `<section>` block before the closing `</body>` tag, and write it back via **Edit**. Do not route the HTML notes through `mcp__obsidian__write_note`.
212
+
213
+ ### 9. Halt and surface (cap reached)
214
+
215
+ If iteration 10 still fails audit, stop the loop. Surface:
216
+
217
+ - The remaining findings (highest severity first)
218
+ - A one-line recommendation: drop scope, simplify the plan, or hand back to the user for a decision
219
+
220
+ Then proceed to step 10 so the cap-reached path emits the same standard final report (vault path of the plan, iteration count with `halted at cap` outcome, core-decision summary, and notes-file path) that the CLEAN-after-N-iterations path emits.
221
+
222
+ ### 10. Report
223
+
224
+ State, in this order:
225
+
226
+ - Vault path of the plan
227
+ - Number of audit iterations consumed (and the outcome: `CLEAN` on initial audit, `CLEAN` after N iterations, or `halted at cap`)
228
+ - One-line summary of the plan's core decision
229
+ - If at least one audit-fix iteration ran: vault path of `<slug>-implementation-notes.html` and the top 1–2 open questions from the notes file (omit both when the initial audit returned `CLEAN` and no notes file exists)
230
+
231
+ That is the entire deliverable.
232
+
233
+ ## Constraints
234
+
235
+ - **Output target is the Obsidian vault. Only the vault.** Filesystem writes outside the vault are out of scope, including `.claude/plans/`, `docs/plans/`, `plans/`, and the cwd. No dual writes.
236
+ - **Interview is mandatory.** Session-level "no clarifying questions" or autonomous directives do not silence the AskUserQuestion loop. Halt rather than skip.
237
+ - Initial write is inline by the skill; only audit-driven fixes are delegated to the general-purpose fix agent.
238
+ - AskUserQuestion is the only interview surface — plain-text questions in chat are blocked by the Stop hook.
239
+ - The skill does not call `/anthropic-plan` or `/prompt-generator`.
240
+ - Slug and path are user-confirmed before any write. `Research/` prefix is fixed.
241
+ - Fan-out before interview — research what can be researched.
242
+ - Audit-fix loop is mandatory unless the user explicitly opts out for the run.
243
+ - Iteration cap is 10. Do not raise without user direction.
244
+ - `<slug>-implementation-notes.html` is append-only across iterations within a single /refine run.
245
+
246
+ ## File index
247
+
248
+ | File | Purpose |
249
+ |---|---|
250
+ | `SKILL.md` | This hub — process, gotchas, constraints |
251
+ | `templates/plan-template.md` | Plan-mode-conformant structure for the written plan |
252
+ | `templates/implementation-notes-template.html` | Iteration-log skeleton the fix agent appends to during the audit-fix loop |
253
+
254
+ ## Folder map
255
+
256
+ - `SKILL.md` — hub.
257
+ - `templates/` — output structures for the plan and the iteration notes file.
@@ -0,0 +1,56 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Implementation notes — {{slug}}</title>
6
+ <style>
7
+ body { font-family: system-ui, sans-serif; max-width: 780px; margin: 2em auto; padding: 0 1em; color: #222; }
8
+ h1 { margin-bottom: .2em; }
9
+ .meta { color: #777; font-size: .9em; margin-bottom: 2em; }
10
+ section.iteration { border-top: 1px solid #ddd; padding-top: 1.5em; margin-top: 1.5em; }
11
+ section.iteration h2 { margin-bottom: .2em; }
12
+ section.iteration .iter-meta { color: #888; font-size: .85em; margin-bottom: 1em; }
13
+ h4 { margin: 1em 0 .2em; color: #444; }
14
+ ul { margin-top: 0; }
15
+ code { background: #f4f4f4; padding: 0 .25em; border-radius: 3px; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <h1>Implementation notes — {{slug}}</h1>
20
+ <p class="meta">Companion to <a href="{{slug}}.md">{{slug}}.md</a>. One section per audit-fix iteration.</p>
21
+
22
+ <!--
23
+ ITERATION TEMPLATE — reference only; this block is HTML-commented so it does not render.
24
+ For each audit-fix iteration, the fix agent copies the <section class="iteration"> markup
25
+ below (without the surrounding HTML comment), fills the placeholders with real iteration
26
+ content, and inserts the populated section immediately before </body>. The template itself
27
+ is never modified or removed; the rendered notes file is built entirely from appended
28
+ iteration sections.
29
+
30
+ <section class="iteration">
31
+ <h2>Iteration <N></h2>
32
+ <p class="iter-meta"><YYYY-MM-DD HH:MM> · findings addressed: <count></p>
33
+
34
+ <h4>Design decisions</h4>
35
+ <ul>
36
+ <li>Choice you made where the spec was ambiguous.</li>
37
+ </ul>
38
+
39
+ <h4>Deviations</h4>
40
+ <ul>
41
+ <li>Place where you intentionally departed from the spec, and why.</li>
42
+ </ul>
43
+
44
+ <h4>Tradeoffs</h4>
45
+ <ul>
46
+ <li>Alternatives considered and why you picked what you did.</li>
47
+ </ul>
48
+
49
+ <h4>Open questions</h4>
50
+ <ul>
51
+ <li>Anything you'd want the user to confirm or revise.</li>
52
+ </ul>
53
+ </section>
54
+ -->
55
+ </body>
56
+ </html>