claude-dev-env 1.41.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.
- package/CLAUDE.md +8 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +231 -3
- package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +56 -2
- package/_shared/pr-loop/scripts/config/claude_settings_keys_constants.py +2 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +121 -4
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +73 -17
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +33 -0
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +3 -1
- package/package.json +1 -1
- package/skills/bugteam/scripts/_claude_permissions_common.py +109 -0
- package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +51 -2
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +115 -4
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +69 -17
- package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
- package/skills/implement/SKILL.md +66 -0
- package/skills/implement/scripts/append_note.py +133 -0
- package/skills/implement/scripts/config/__init__.py +0 -0
- package/skills/implement/scripts/config/notes_constants.py +12 -0
- package/skills/implement/scripts/test_append_note.py +191 -0
- package/skills/pr-converge/config/constants.py +5 -0
- package/skills/pr-converge/scripts/check_bugbot_ci.py +1 -1
- package/skills/pr-converge/scripts/check_convergence.py +167 -28
- package/skills/pr-converge/scripts/check_pending_reviews.py +1 -1
- package/skills/pr-converge/scripts/conftest.py +60 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +1 -1
- package/skills/pr-converge/scripts/post_fix_reply.py +1 -1
- package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
- package/skills/pr-converge/scripts/test_check_convergence.py +306 -0
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +1 -1
- package/skills/refine/SKILL.md +257 -0
- package/skills/refine/templates/implementation-notes-template.html +56 -0
- package/skills/refine/templates/plan-template.md +60 -0
|
@@ -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
|
|
@@ -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>
|