codeforge-dev 1.8.0 → 1.10.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/.devcontainer/.env +3 -0
- package/.devcontainer/CHANGELOG.md +107 -0
- package/.devcontainer/CLAUDE.md +30 -9
- package/.devcontainer/README.md +61 -2
- package/.devcontainer/config/defaults/main-system-prompt.md +162 -128
- package/.devcontainer/config/defaults/rules/spec-workflow.md +75 -0
- package/.devcontainer/config/defaults/rules/workspace-scope.md +7 -0
- package/.devcontainer/config/defaults/settings.json +63 -66
- package/.devcontainer/config/file-manifest.json +30 -18
- package/.devcontainer/connect-external-terminal.sh +17 -17
- package/.devcontainer/devcontainer.json +143 -144
- package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +104 -97
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/README.md +158 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/hooks/hooks.json +39 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/collect-edited-files.py +47 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/format-on-stop.py +297 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/lint-file.py +536 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/syntax-validator.py +146 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/architect.md +81 -4
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/debug-logs.md +18 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/dependency-analyst.md +18 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/doc-writer.md +89 -4
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/explorer.md +18 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/generalist.md +142 -8
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/git-archaeologist.md +18 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/migrator.md +108 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/perf-profiler.md +24 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/refactorer.md +97 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/researcher.md +33 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/security-auditor.md +24 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/spec-writer.md +50 -12
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/test-writer.md +96 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/hooks/hooks.json +100 -95
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/advisory-test-runner.py +186 -13
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/spec-reminder.py +122 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/SKILL.md +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-check/SKILL.md +98 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/SKILL.md +99 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/references/backlog-template.md +23 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/references/roadmap-template.md +33 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-new/SKILL.md +110 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-new/references/template.md +129 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-refine/SKILL.md +194 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-update/SKILL.md +142 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/SKILL.md +19 -12
- package/.devcontainer/scripts/check-setup.sh +24 -25
- package/.devcontainer/scripts/setup-aliases.sh +88 -76
- package/.devcontainer/scripts/setup-config.sh +86 -83
- package/.devcontainer/scripts/setup-projects.sh +172 -131
- package/.devcontainer/scripts/setup-terminal.sh +48 -0
- package/.devcontainer/scripts/setup-update-claude.sh +49 -107
- package/.devcontainer/scripts/setup.sh +4 -17
- package/README.md +2 -2
- package/package.json +42 -42
|
@@ -1,97 +1,102 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
2
|
+
"description": "Code quality hooks and skill suggestions for the CodeDirective project",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "Task",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/redirect-builtin-agents.py",
|
|
11
|
+
"timeout": 5
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"UserPromptSubmit": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "*",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/skill-suggester.py",
|
|
23
|
+
"timeout": 3
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/ticket-linker.py",
|
|
28
|
+
"timeout": 12
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"SubagentStart": [
|
|
34
|
+
{
|
|
35
|
+
"matcher": "Plan",
|
|
36
|
+
"hooks": [
|
|
37
|
+
{
|
|
38
|
+
"type": "command",
|
|
39
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/skill-suggester.py",
|
|
40
|
+
"timeout": 3
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"Stop": [
|
|
46
|
+
{
|
|
47
|
+
"matcher": "",
|
|
48
|
+
"hooks": [
|
|
49
|
+
{
|
|
50
|
+
"type": "command",
|
|
51
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/advisory-test-runner.py",
|
|
52
|
+
"timeout": 20
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"type": "command",
|
|
56
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/commit-reminder.py",
|
|
57
|
+
"timeout": 8
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"type": "command",
|
|
61
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/spec-reminder.py",
|
|
62
|
+
"timeout": 8
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"SessionStart": [
|
|
68
|
+
{
|
|
69
|
+
"matcher": "",
|
|
70
|
+
"hooks": [
|
|
71
|
+
{
|
|
72
|
+
"type": "command",
|
|
73
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/git-state-injector.py",
|
|
74
|
+
"timeout": 10
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"type": "command",
|
|
78
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/todo-harvester.py",
|
|
79
|
+
"timeout": 8
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"PostToolUse": [
|
|
85
|
+
{
|
|
86
|
+
"matcher": "Edit|Write",
|
|
87
|
+
"hooks": [
|
|
88
|
+
{
|
|
89
|
+
"type": "command",
|
|
90
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/syntax-validator.py",
|
|
91
|
+
"timeout": 5
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"type": "command",
|
|
95
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/collect-edited-files.py",
|
|
96
|
+
"timeout": 3
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
97
102
|
}
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Advisory test runner — Stop hook that injects test results as context.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Reads the list of files edited this session (written by collect-edited-files.py),
|
|
6
|
+
maps them to affected test files, and runs only those tests. Skips entirely
|
|
7
|
+
if no files were edited. Results are returned as additionalContext so Claude
|
|
8
|
+
sees pass/fail info without being blocked.
|
|
8
9
|
|
|
9
10
|
Reads hook input from stdin (JSON). Returns JSON on stdout.
|
|
10
11
|
Always exits 0 (advisory, never blocking).
|
|
@@ -15,15 +16,37 @@ import os
|
|
|
15
16
|
import subprocess
|
|
16
17
|
import sys
|
|
17
18
|
|
|
19
|
+
TIMEOUT_SECONDS = 15
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_edited_files(session_id: str) -> list[str]:
|
|
23
|
+
"""Read the list of files edited this session.
|
|
24
|
+
|
|
25
|
+
Relies on collect-edited-files.py writing paths to a temp file.
|
|
26
|
+
Returns deduplicated list of paths that still exist on disk.
|
|
27
|
+
"""
|
|
28
|
+
tmp_path = f"/tmp/claude-edited-files-{session_id}"
|
|
29
|
+
try:
|
|
30
|
+
with open(tmp_path, "r") as f:
|
|
31
|
+
raw = f.read()
|
|
32
|
+
except OSError:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
seen: set[str] = set()
|
|
36
|
+
result: list[str] = []
|
|
37
|
+
for line in raw.strip().splitlines():
|
|
38
|
+
path = line.strip()
|
|
39
|
+
if path and path not in seen and os.path.isfile(path):
|
|
40
|
+
seen.add(path)
|
|
41
|
+
result.append(path)
|
|
42
|
+
return result
|
|
43
|
+
|
|
18
44
|
|
|
19
45
|
def detect_test_framework(cwd: str) -> tuple[str, list[str]]:
|
|
20
46
|
"""Detect which test framework is available in the project.
|
|
21
47
|
|
|
22
|
-
Checks for: pytest, vitest, jest, mocha, go test, cargo test.
|
|
23
|
-
Falls back to npm test if a test script is defined.
|
|
24
|
-
|
|
25
48
|
Returns:
|
|
26
|
-
Tuple of (framework_name,
|
|
49
|
+
Tuple of (framework_name, base_command) or ("", []) if none found.
|
|
27
50
|
"""
|
|
28
51
|
try:
|
|
29
52
|
entries = set(os.listdir(cwd))
|
|
@@ -102,7 +125,7 @@ def detect_test_framework(cwd: str) -> tuple[str, list[str]]:
|
|
|
102
125
|
|
|
103
126
|
# --- Go ---
|
|
104
127
|
if "go.mod" in entries:
|
|
105
|
-
return ("go", ["go", "test", "
|
|
128
|
+
return ("go", ["go", "test", "-count=1"])
|
|
106
129
|
|
|
107
130
|
# --- Rust ---
|
|
108
131
|
if "Cargo.toml" in entries:
|
|
@@ -111,6 +134,139 @@ def detect_test_framework(cwd: str) -> tuple[str, list[str]]:
|
|
|
111
134
|
return ("", [])
|
|
112
135
|
|
|
113
136
|
|
|
137
|
+
def resolve_pytest_tests(edited_files: list[str], cwd: str) -> tuple[list[str], bool]:
|
|
138
|
+
"""Map edited Python files to their corresponding pytest test files.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
(test_files, run_all) — if run_all is True, run the whole suite
|
|
142
|
+
(e.g. conftest.py was edited).
|
|
143
|
+
"""
|
|
144
|
+
test_files: list[str] = []
|
|
145
|
+
|
|
146
|
+
for path in edited_files:
|
|
147
|
+
if not path.endswith(".py"):
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
basename = os.path.basename(path)
|
|
151
|
+
|
|
152
|
+
# conftest changes can affect anything — run full suite
|
|
153
|
+
if basename == "conftest.py":
|
|
154
|
+
return ([], True)
|
|
155
|
+
|
|
156
|
+
# Already a test file — include directly
|
|
157
|
+
if basename.startswith("test_") or "/tests/" in path:
|
|
158
|
+
if os.path.isfile(path):
|
|
159
|
+
test_files.append(path)
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
# Map source → test via directory mirroring
|
|
163
|
+
# e.g. src/engine/db/sessions.py → tests/engine/db/test_sessions.py
|
|
164
|
+
# e.g. src/engine/api/routes/github.py → tests/engine/api/test_routes_github.py
|
|
165
|
+
rel = os.path.relpath(path, cwd)
|
|
166
|
+
parts = rel.split(os.sep)
|
|
167
|
+
|
|
168
|
+
# Strip leading "src/" if present
|
|
169
|
+
if parts and parts[0] == "src":
|
|
170
|
+
parts = parts[1:]
|
|
171
|
+
|
|
172
|
+
if not parts:
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
module = parts[-1] # e.g. "sessions.py"
|
|
176
|
+
module_name = module.removesuffix(".py")
|
|
177
|
+
parent_parts = parts[:-1] # e.g. ["engine", "db"]
|
|
178
|
+
|
|
179
|
+
# Standard mapping: tests/<parent>/test_<module>.py
|
|
180
|
+
test_path = os.path.join(cwd, "tests", *parent_parts, f"test_{module_name}.py")
|
|
181
|
+
if os.path.isfile(test_path):
|
|
182
|
+
test_files.append(test_path)
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# Routes mapping: src/engine/api/routes/github.py
|
|
186
|
+
# → tests/engine/api/test_routes_github.py
|
|
187
|
+
if len(parent_parts) >= 2 and parent_parts[-1] == "routes":
|
|
188
|
+
route_test = os.path.join(
|
|
189
|
+
cwd,
|
|
190
|
+
"tests",
|
|
191
|
+
*parent_parts[:-1],
|
|
192
|
+
f"test_routes_{module_name}.py",
|
|
193
|
+
)
|
|
194
|
+
if os.path.isfile(route_test):
|
|
195
|
+
test_files.append(route_test)
|
|
196
|
+
|
|
197
|
+
# Deduplicate while preserving order
|
|
198
|
+
seen: set[str] = set()
|
|
199
|
+
unique: list[str] = []
|
|
200
|
+
for t in test_files:
|
|
201
|
+
if t not in seen:
|
|
202
|
+
seen.add(t)
|
|
203
|
+
unique.append(t)
|
|
204
|
+
|
|
205
|
+
return (unique, False)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def resolve_affected_tests(
|
|
209
|
+
edited_files: list[str], cwd: str, framework: str
|
|
210
|
+
) -> tuple[list[str], bool]:
|
|
211
|
+
"""Resolve edited files to framework-specific test arguments.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
(extra_args, run_all) — extra_args to append to the base command.
|
|
215
|
+
If run_all is True, run the whole suite (no extra args needed).
|
|
216
|
+
If extra_args is empty and run_all is False, skip testing entirely.
|
|
217
|
+
"""
|
|
218
|
+
if framework == "pytest":
|
|
219
|
+
test_files, run_all = resolve_pytest_tests(edited_files, cwd)
|
|
220
|
+
return (test_files, run_all)
|
|
221
|
+
|
|
222
|
+
if framework == "vitest":
|
|
223
|
+
# vitest --related does dep-graph analysis natively
|
|
224
|
+
source_files = [
|
|
225
|
+
f
|
|
226
|
+
for f in edited_files
|
|
227
|
+
if not f.endswith(
|
|
228
|
+
(".md", ".json", ".yaml", ".yml", ".toml", ".txt", ".css")
|
|
229
|
+
)
|
|
230
|
+
]
|
|
231
|
+
if not source_files:
|
|
232
|
+
return ([], False)
|
|
233
|
+
return (["--related"] + source_files, False)
|
|
234
|
+
|
|
235
|
+
if framework == "jest":
|
|
236
|
+
source_files = [
|
|
237
|
+
f
|
|
238
|
+
for f in edited_files
|
|
239
|
+
if not f.endswith(
|
|
240
|
+
(".md", ".json", ".yaml", ".yml", ".toml", ".txt", ".css")
|
|
241
|
+
)
|
|
242
|
+
]
|
|
243
|
+
if not source_files:
|
|
244
|
+
return ([], False)
|
|
245
|
+
return (["--findRelatedTests"] + source_files, False)
|
|
246
|
+
|
|
247
|
+
if framework == "go":
|
|
248
|
+
# Map edited .go files to their package directories
|
|
249
|
+
pkgs: set[str] = set()
|
|
250
|
+
for path in edited_files:
|
|
251
|
+
if path.endswith(".go"):
|
|
252
|
+
pkg_dir = os.path.dirname(path)
|
|
253
|
+
rel = os.path.relpath(pkg_dir, cwd)
|
|
254
|
+
pkgs.add(f"./{rel}")
|
|
255
|
+
if not pkgs:
|
|
256
|
+
return ([], False)
|
|
257
|
+
return (sorted(pkgs), False)
|
|
258
|
+
|
|
259
|
+
# cargo, mocha, npm-test — no granular selection, run full suite
|
|
260
|
+
code_files = [
|
|
261
|
+
f
|
|
262
|
+
for f in edited_files
|
|
263
|
+
if not f.endswith((".md", ".json", ".yaml", ".yml", ".toml", ".txt"))
|
|
264
|
+
]
|
|
265
|
+
if not code_files:
|
|
266
|
+
return ([], False)
|
|
267
|
+
return ([], True)
|
|
268
|
+
|
|
269
|
+
|
|
114
270
|
def main():
|
|
115
271
|
try:
|
|
116
272
|
input_data = json.load(sys.stdin)
|
|
@@ -121,34 +277,51 @@ def main():
|
|
|
121
277
|
if input_data.get("stop_hook_active"):
|
|
122
278
|
sys.exit(0)
|
|
123
279
|
|
|
280
|
+
session_id = input_data.get("session_id", "")
|
|
281
|
+
if not session_id:
|
|
282
|
+
sys.exit(0)
|
|
283
|
+
|
|
284
|
+
# No files edited this session — nothing to test
|
|
285
|
+
edited_files = get_edited_files(session_id)
|
|
286
|
+
if not edited_files:
|
|
287
|
+
sys.exit(0)
|
|
288
|
+
|
|
124
289
|
cwd = os.getcwd()
|
|
125
|
-
framework,
|
|
290
|
+
framework, base_cmd = detect_test_framework(cwd)
|
|
126
291
|
|
|
127
292
|
if not framework:
|
|
128
293
|
sys.exit(0)
|
|
129
294
|
|
|
295
|
+
extra_args, run_all = resolve_affected_tests(edited_files, cwd, framework)
|
|
296
|
+
|
|
297
|
+
# No affected tests and not a run-all situation — skip
|
|
298
|
+
if not extra_args and not run_all:
|
|
299
|
+
sys.exit(0)
|
|
300
|
+
|
|
301
|
+
cmd = base_cmd + extra_args
|
|
302
|
+
|
|
130
303
|
try:
|
|
131
304
|
result = subprocess.run(
|
|
132
305
|
cmd,
|
|
133
306
|
cwd=cwd,
|
|
134
307
|
capture_output=True,
|
|
135
308
|
text=True,
|
|
136
|
-
timeout=
|
|
309
|
+
timeout=TIMEOUT_SECONDS,
|
|
137
310
|
)
|
|
138
311
|
except subprocess.TimeoutExpired:
|
|
139
312
|
json.dump(
|
|
140
|
-
{
|
|
313
|
+
{
|
|
314
|
+
"additionalContext": f"[Tests] {framework} timed out after {TIMEOUT_SECONDS}s"
|
|
315
|
+
},
|
|
141
316
|
sys.stdout,
|
|
142
317
|
)
|
|
143
318
|
sys.exit(0)
|
|
144
319
|
except (FileNotFoundError, OSError):
|
|
145
|
-
# Test runner not installed or not accessible
|
|
146
320
|
sys.exit(0)
|
|
147
321
|
|
|
148
322
|
output = (result.stdout + "\n" + result.stderr).strip()
|
|
149
323
|
|
|
150
324
|
if result.returncode == 0:
|
|
151
|
-
# Extract test count from output if possible
|
|
152
325
|
json.dump(
|
|
153
326
|
{"additionalContext": f"[Tests] All tests passed ({framework})"},
|
|
154
327
|
sys.stdout,
|
package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/spec-reminder.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Spec reminder — Stop hook that advises about spec updates after code changes.
|
|
4
|
+
|
|
5
|
+
On Stop, checks if source code was modified but no .specs/ files were updated.
|
|
6
|
+
Injects an advisory reminder as additionalContext pointing the user to
|
|
7
|
+
/spec-update.
|
|
8
|
+
|
|
9
|
+
Only fires when a .specs/ directory exists (project uses the spec system).
|
|
10
|
+
|
|
11
|
+
Reads hook input from stdin (JSON). Returns JSON on stdout.
|
|
12
|
+
Always exits 0 (advisory, never blocking).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
GIT_CMD_TIMEOUT = 5
|
|
21
|
+
|
|
22
|
+
# Directories whose changes should trigger the spec reminder
|
|
23
|
+
CODE_DIRS = (
|
|
24
|
+
"src/",
|
|
25
|
+
"lib/",
|
|
26
|
+
"app/",
|
|
27
|
+
"pkg/",
|
|
28
|
+
"internal/",
|
|
29
|
+
"cmd/",
|
|
30
|
+
"tests/",
|
|
31
|
+
"api/",
|
|
32
|
+
"frontend/",
|
|
33
|
+
"backend/",
|
|
34
|
+
"packages/",
|
|
35
|
+
"services/",
|
|
36
|
+
"components/",
|
|
37
|
+
"pages/",
|
|
38
|
+
"routes/",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _run_git(args: list[str]) -> str | None:
|
|
43
|
+
"""Run a git command and return stdout, or None on any failure."""
|
|
44
|
+
try:
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
["git"] + args,
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
timeout=GIT_CMD_TIMEOUT,
|
|
50
|
+
)
|
|
51
|
+
if result.returncode == 0:
|
|
52
|
+
return result.stdout.strip()
|
|
53
|
+
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
|
|
54
|
+
pass
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main():
|
|
59
|
+
try:
|
|
60
|
+
input_data = json.load(sys.stdin)
|
|
61
|
+
except (json.JSONDecodeError, ValueError):
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
|
|
64
|
+
# Skip if another Stop hook is already blocking
|
|
65
|
+
if input_data.get("stop_hook_active"):
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
cwd = os.getcwd()
|
|
69
|
+
|
|
70
|
+
# Only fire if this project uses the spec system
|
|
71
|
+
if not os.path.isdir(os.path.join(cwd, ".specs")):
|
|
72
|
+
sys.exit(0)
|
|
73
|
+
|
|
74
|
+
# Get all changed files (staged + unstaged)
|
|
75
|
+
diff_output = _run_git(["diff", "--name-only", "HEAD"])
|
|
76
|
+
staged_output = _run_git(["diff", "--name-only", "--cached"])
|
|
77
|
+
|
|
78
|
+
# Also include untracked files in source dirs
|
|
79
|
+
untracked = _run_git(["ls-files", "--others", "--exclude-standard"])
|
|
80
|
+
|
|
81
|
+
all_files: set[str] = set()
|
|
82
|
+
for output in (diff_output, staged_output, untracked):
|
|
83
|
+
if output:
|
|
84
|
+
all_files.update(output.splitlines())
|
|
85
|
+
|
|
86
|
+
if not all_files:
|
|
87
|
+
sys.exit(0)
|
|
88
|
+
|
|
89
|
+
# Check if any code directories have changes
|
|
90
|
+
has_code_changes = any(f.startswith(d) for f in all_files for d in CODE_DIRS)
|
|
91
|
+
|
|
92
|
+
if not has_code_changes:
|
|
93
|
+
sys.exit(0)
|
|
94
|
+
|
|
95
|
+
# Check if any spec files were also changed
|
|
96
|
+
has_spec_changes = any(f.startswith(".specs/") for f in all_files)
|
|
97
|
+
|
|
98
|
+
if has_spec_changes:
|
|
99
|
+
# Specs were updated alongside code — no reminder needed
|
|
100
|
+
sys.exit(0)
|
|
101
|
+
|
|
102
|
+
# Code changed but specs didn't — inject reminder
|
|
103
|
+
code_dirs_touched = sorted(
|
|
104
|
+
{f.split("/")[0] + "/" for f in all_files if "/" in f}
|
|
105
|
+
& {d.rstrip("/") + "/" for d in CODE_DIRS}
|
|
106
|
+
)
|
|
107
|
+
dirs_str = ", ".join(code_dirs_touched[:3])
|
|
108
|
+
|
|
109
|
+
message = (
|
|
110
|
+
f"[Spec Reminder] Code was modified in {dirs_str} "
|
|
111
|
+
"but no specs were updated. "
|
|
112
|
+
"Use /spec-update to update the relevant spec, "
|
|
113
|
+
"/spec-new if no spec exists for this feature, "
|
|
114
|
+
"or /spec-refine if the spec is still in draft status."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
json.dump({"additionalContext": message}, sys.stdout)
|
|
118
|
+
sys.exit(0)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
main()
|
|
@@ -65,7 +65,7 @@ Development setup, how to run tests, how to submit changes. Link to CONTRIBUTING
|
|
|
65
65
|
|
|
66
66
|
## Sizing Rules
|
|
67
67
|
|
|
68
|
-
Documentation files consumed by AI tools (CLAUDE.md, specs, architecture docs) should
|
|
68
|
+
Documentation files consumed by AI tools (CLAUDE.md, specs, architecture docs) should aim for **~200 lines** each. Split large documents by concern when practical. Each file should be independently useful.
|
|
69
69
|
|
|
70
70
|
For human-facing docs (README, API reference), there is no hard limit, but prefer shorter docs that link to detailed sub-pages over monolithic documents.
|
|
71
71
|
|