codeforge-dev 1.5.7 → 1.7.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 +2 -1
- package/.devcontainer/CHANGELOG.md +55 -9
- package/.devcontainer/CLAUDE.md +65 -15
- package/.devcontainer/README.md +67 -6
- package/.devcontainer/config/keybindings.json +5 -0
- package/.devcontainer/config/main-system-prompt.md +63 -2
- package/.devcontainer/config/settings.json +25 -6
- package/.devcontainer/devcontainer.json +23 -7
- package/.devcontainer/features/README.md +21 -7
- package/.devcontainer/features/ccburn/README.md +60 -0
- package/.devcontainer/features/ccburn/devcontainer-feature.json +38 -0
- package/.devcontainer/features/ccburn/install.sh +174 -0
- package/.devcontainer/features/ccstatusline/README.md +22 -21
- package/.devcontainer/features/ccstatusline/devcontainer-feature.json +1 -1
- package/.devcontainer/features/ccstatusline/install.sh +48 -16
- package/.devcontainer/features/claude-code/config/settings.json +60 -24
- package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +1 -1
- package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/__pycache__/format-on-stop.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-on-stop.py +21 -6
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/__pycache__/lint-file.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +7 -10
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/REVIEW-RUBRIC.md +440 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/architect.md +190 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/bash-exec.md +173 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/claude-guide.md +155 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/dependency-analyst.md +248 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/doc-writer.md +233 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/explorer.md +235 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/generalist.md +125 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/git-archaeologist.md +242 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/migrator.md +195 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/perf-profiler.md +265 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/refactorer.md +209 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/researcher.md +195 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/security-auditor.md +289 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/spec-writer.md +284 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/statusline-config.md +188 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/test-writer.md +245 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/hooks/hooks.json +12 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/guard-readonly-bash.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/redirect-builtin-agents.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/skill-suggester.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/syntax-validator.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/verify-no-regression.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/verify-tests-pass.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/guard-readonly-bash.py +611 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/redirect-builtin-agents.py +83 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/skill-suggester.py +85 -2
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/syntax-validator.py +9 -4
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/verify-no-regression.py +221 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/verify-tests-pass.py +176 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/claude-agent-sdk/SKILL.md +599 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/claude-agent-sdk/references/sdk-typescript-reference.md +954 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/git-forensics/SKILL.md +276 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/git-forensics/references/advanced-commands.md +332 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/git-forensics/references/investigation-playbooks.md +319 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/performance-profiling/SKILL.md +341 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/performance-profiling/references/interpreting-results.md +235 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/performance-profiling/references/tool-commands.md +395 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/refactoring-patterns/SKILL.md +344 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/refactoring-patterns/references/safe-transformations.md +247 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/refactoring-patterns/references/smell-catalog.md +332 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/security-checklist/SKILL.md +277 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/security-checklist/references/owasp-patterns.md +269 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/security-checklist/references/secrets-patterns.md +253 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/SKILL.md +288 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/references/criteria-patterns.md +245 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/references/ears-templates.md +239 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/__pycache__/guard-protected.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +40 -39
- package/.devcontainer/scripts/setup-aliases.sh +10 -20
- package/.devcontainer/scripts/setup-config.sh +2 -0
- package/.devcontainer/scripts/setup-plugins.sh +38 -46
- package/.devcontainer/scripts/setup-projects.sh +175 -0
- package/.devcontainer/scripts/setup-symlink-claude.sh +36 -0
- package/.devcontainer/scripts/setup-update-claude.sh +11 -8
- package/.devcontainer/scripts/setup.sh +4 -2
- package/package.json +1 -1
- package/.devcontainer/scripts/setup-irie-claude.sh +0 -32
package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/skill-suggester.py
CHANGED
|
@@ -46,13 +46,23 @@ SKILLS = {
|
|
|
46
46
|
"headless mode",
|
|
47
47
|
"claude -p",
|
|
48
48
|
"stream-json",
|
|
49
|
-
"agent sdk",
|
|
50
49
|
"claude code headless",
|
|
51
50
|
"run claude in ci",
|
|
52
51
|
"claude in pipeline",
|
|
53
52
|
],
|
|
54
53
|
"terms": [],
|
|
55
54
|
},
|
|
55
|
+
"claude-agent-sdk": {
|
|
56
|
+
"phrases": [
|
|
57
|
+
"agent sdk",
|
|
58
|
+
"claude agent sdk",
|
|
59
|
+
"build an agent",
|
|
60
|
+
"create an agent",
|
|
61
|
+
"canusetool",
|
|
62
|
+
"sdk permissions",
|
|
63
|
+
],
|
|
64
|
+
"terms": ["claude-agent-sdk", "claude_agent_sdk"],
|
|
65
|
+
},
|
|
56
66
|
"pydantic-ai": {
|
|
57
67
|
"phrases": [
|
|
58
68
|
"pydantic ai",
|
|
@@ -115,6 +125,7 @@ SKILLS = {
|
|
|
115
125
|
"dockerfile",
|
|
116
126
|
"docker compose",
|
|
117
127
|
"docker-compose",
|
|
128
|
+
"compose file",
|
|
118
129
|
"multi-stage build",
|
|
119
130
|
"health check",
|
|
120
131
|
"healthcheck",
|
|
@@ -122,7 +133,7 @@ SKILLS = {
|
|
|
122
133
|
"docker volume",
|
|
123
134
|
"docker image",
|
|
124
135
|
],
|
|
125
|
-
"terms": ["dockerfile"],
|
|
136
|
+
"terms": ["dockerfile", "compose"],
|
|
126
137
|
},
|
|
127
138
|
"skill-building": {
|
|
128
139
|
"phrases": [
|
|
@@ -152,6 +163,78 @@ SKILLS = {
|
|
|
152
163
|
],
|
|
153
164
|
"terms": ["diagnose", "troubleshoot"],
|
|
154
165
|
},
|
|
166
|
+
"refactoring-patterns": {
|
|
167
|
+
"phrases": [
|
|
168
|
+
"refactor this",
|
|
169
|
+
"clean up code",
|
|
170
|
+
"improve code structure",
|
|
171
|
+
"reduce complexity",
|
|
172
|
+
],
|
|
173
|
+
"terms": [
|
|
174
|
+
"refactor",
|
|
175
|
+
"refactoring",
|
|
176
|
+
"code smell",
|
|
177
|
+
"extract function",
|
|
178
|
+
"dead code",
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
"security-checklist": {
|
|
182
|
+
"phrases": [
|
|
183
|
+
"security review",
|
|
184
|
+
"check for vulnerabilities",
|
|
185
|
+
"audit security",
|
|
186
|
+
"find security issues",
|
|
187
|
+
],
|
|
188
|
+
"terms": [
|
|
189
|
+
"security",
|
|
190
|
+
"vulnerability",
|
|
191
|
+
"owasp",
|
|
192
|
+
"injection",
|
|
193
|
+
"xss",
|
|
194
|
+
"secrets",
|
|
195
|
+
"cve",
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
"git-forensics": {
|
|
199
|
+
"phrases": [
|
|
200
|
+
"git history",
|
|
201
|
+
"who changed this",
|
|
202
|
+
"when did this break",
|
|
203
|
+
"git blame",
|
|
204
|
+
],
|
|
205
|
+
"terms": ["bisect", "blame", "archaeology", "git log", "pickaxe", "reflog"],
|
|
206
|
+
},
|
|
207
|
+
"specification-writing": {
|
|
208
|
+
"phrases": [
|
|
209
|
+
"write a spec",
|
|
210
|
+
"define requirements",
|
|
211
|
+
"acceptance criteria",
|
|
212
|
+
"user stories",
|
|
213
|
+
],
|
|
214
|
+
"terms": [
|
|
215
|
+
"specification",
|
|
216
|
+
"requirements",
|
|
217
|
+
"ears",
|
|
218
|
+
"gherkin",
|
|
219
|
+
"given when then",
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
"performance-profiling": {
|
|
223
|
+
"phrases": [
|
|
224
|
+
"profile performance",
|
|
225
|
+
"find bottleneck",
|
|
226
|
+
"benchmark this",
|
|
227
|
+
"why is this slow",
|
|
228
|
+
],
|
|
229
|
+
"terms": [
|
|
230
|
+
"profiling",
|
|
231
|
+
"benchmark",
|
|
232
|
+
"flamegraph",
|
|
233
|
+
"bottleneck",
|
|
234
|
+
"latency",
|
|
235
|
+
"throughput",
|
|
236
|
+
],
|
|
237
|
+
},
|
|
155
238
|
}
|
|
156
239
|
|
|
157
240
|
# Pre-compile term patterns for whole-word matching
|
package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/syntax-validator.py
CHANGED
|
@@ -19,11 +19,16 @@ EXTENSIONS = {".json", ".jsonc", ".yaml", ".yml", ".toml"}
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def strip_jsonc_comments(text: str) -> str:
|
|
22
|
-
"""Remove // and /* */ comments from JSONC text.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
"""Remove // and /* */ comments from JSONC text.
|
|
23
|
+
|
|
24
|
+
Handles URLs (https://...) by only stripping // comments that are
|
|
25
|
+
preceded by whitespace or appear at line start, not those inside strings.
|
|
26
|
+
"""
|
|
27
|
+
# Remove multi-line comments first
|
|
26
28
|
text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
|
|
29
|
+
# Remove single-line comments: // preceded by start-of-line or whitespace
|
|
30
|
+
# This avoids stripping :// in URLs
|
|
31
|
+
text = re.sub(r"(^|[\s,\[\{])//.*$", r"\1", text, flags=re.MULTILINE)
|
|
27
32
|
return text
|
|
28
33
|
|
|
29
34
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Verify no regression - PostToolUse hook for refactorer agent.
|
|
4
|
+
|
|
5
|
+
After each Edit operation, runs the project test suite to ensure
|
|
6
|
+
the refactoring didn't break anything. Includes debounce to avoid
|
|
7
|
+
running tests too frequently during rapid edits.
|
|
8
|
+
|
|
9
|
+
Reads hook input from stdin (JSON). Returns JSON on stdout.
|
|
10
|
+
Non-blocking on detection failures: always exits 0 if no framework found.
|
|
11
|
+
|
|
12
|
+
Exit 0: Tests pass, no framework found, debounced, or timeout
|
|
13
|
+
Exit 2: Tests fail (forces agent to fix regression before continuing)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
DEBOUNCE_SECONDS = 10
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def detect_test_framework(cwd: str) -> tuple[str, list[str]]:
|
|
26
|
+
"""Detect which test framework is available in the project.
|
|
27
|
+
|
|
28
|
+
Checks for: pytest, vitest, jest, mocha, go test, cargo test.
|
|
29
|
+
Falls back to npm test if a test script is defined.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Tuple of (framework_name, command_list) or ("", []) if none found.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
entries = set(os.listdir(cwd))
|
|
36
|
+
except OSError:
|
|
37
|
+
return ("", [])
|
|
38
|
+
|
|
39
|
+
# --- Python: pytest ---
|
|
40
|
+
if "pytest.ini" in entries or "conftest.py" in entries:
|
|
41
|
+
return ("pytest", ["python3", "-m", "pytest", "--tb=short", "-q"])
|
|
42
|
+
|
|
43
|
+
for cfg_name in ("pyproject.toml", "setup.cfg", "tox.ini"):
|
|
44
|
+
cfg_path = os.path.join(cwd, cfg_name)
|
|
45
|
+
if os.path.isfile(cfg_path):
|
|
46
|
+
try:
|
|
47
|
+
with open(cfg_path, "r", encoding="utf-8") as f:
|
|
48
|
+
content = f.read()
|
|
49
|
+
if (
|
|
50
|
+
"[tool.pytest" in content
|
|
51
|
+
or "[pytest]" in content
|
|
52
|
+
or "[tool:pytest]" in content
|
|
53
|
+
):
|
|
54
|
+
return ("pytest", ["python3", "-m", "pytest", "--tb=short", "-q"])
|
|
55
|
+
except OSError:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
if "tests" in entries and os.path.isdir(os.path.join(cwd, "tests")):
|
|
59
|
+
return ("pytest", ["python3", "-m", "pytest", "--tb=short", "-q"])
|
|
60
|
+
|
|
61
|
+
for entry in entries:
|
|
62
|
+
if entry.startswith("test_") and entry.endswith(".py"):
|
|
63
|
+
return ("pytest", ["python3", "-m", "pytest", "--tb=short", "-q"])
|
|
64
|
+
|
|
65
|
+
# --- JavaScript: vitest ---
|
|
66
|
+
for name in entries:
|
|
67
|
+
if name.startswith("vitest.config"):
|
|
68
|
+
return ("vitest", ["npx", "vitest", "run", "--reporter=verbose"])
|
|
69
|
+
|
|
70
|
+
for vite_cfg in ("vite.config.ts", "vite.config.js"):
|
|
71
|
+
cfg_path = os.path.join(cwd, vite_cfg)
|
|
72
|
+
if os.path.isfile(cfg_path):
|
|
73
|
+
try:
|
|
74
|
+
with open(cfg_path, "r", encoding="utf-8") as f:
|
|
75
|
+
if "test" in f.read():
|
|
76
|
+
return (
|
|
77
|
+
"vitest",
|
|
78
|
+
["npx", "vitest", "run", "--reporter=verbose"],
|
|
79
|
+
)
|
|
80
|
+
except OSError:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
# --- JavaScript: jest ---
|
|
84
|
+
for name in entries:
|
|
85
|
+
if name.startswith("jest.config"):
|
|
86
|
+
return ("jest", ["npx", "jest", "--verbose"])
|
|
87
|
+
|
|
88
|
+
pkg_json = os.path.join(cwd, "package.json")
|
|
89
|
+
if os.path.isfile(pkg_json):
|
|
90
|
+
try:
|
|
91
|
+
with open(pkg_json, "r", encoding="utf-8") as f:
|
|
92
|
+
pkg = json.loads(f.read())
|
|
93
|
+
|
|
94
|
+
if "jest" in pkg:
|
|
95
|
+
return ("jest", ["npx", "jest", "--verbose"])
|
|
96
|
+
|
|
97
|
+
dev_deps = pkg.get("devDependencies", {})
|
|
98
|
+
deps = pkg.get("dependencies", {})
|
|
99
|
+
|
|
100
|
+
if "mocha" in dev_deps or "mocha" in deps:
|
|
101
|
+
return ("mocha", ["npx", "mocha", "--reporter", "spec"])
|
|
102
|
+
|
|
103
|
+
test_script = pkg.get("scripts", {}).get("test", "")
|
|
104
|
+
if test_script and "no test specified" not in test_script:
|
|
105
|
+
return ("npm-test", ["npm", "test"])
|
|
106
|
+
except (OSError, json.JSONDecodeError):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
# --- Go ---
|
|
110
|
+
if "go.mod" in entries:
|
|
111
|
+
return ("go", ["go", "test", "./...", "-count=1"])
|
|
112
|
+
|
|
113
|
+
# --- Rust ---
|
|
114
|
+
if "Cargo.toml" in entries:
|
|
115
|
+
return ("cargo", ["cargo", "test"])
|
|
116
|
+
|
|
117
|
+
return ("", [])
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def should_debounce(session_id: str) -> bool:
|
|
121
|
+
"""Check if we should skip this run due to recent execution.
|
|
122
|
+
|
|
123
|
+
Uses a timestamp file in /tmp to throttle test runs during rapid edits.
|
|
124
|
+
Returns True if the last run was less than DEBOUNCE_SECONDS ago.
|
|
125
|
+
"""
|
|
126
|
+
stamp_path = f"/tmp/claude-regression-{session_id}"
|
|
127
|
+
now = time.time()
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
with open(stamp_path, "r") as f:
|
|
131
|
+
last_run = float(f.read().strip())
|
|
132
|
+
if now - last_run < DEBOUNCE_SECONDS:
|
|
133
|
+
return True
|
|
134
|
+
except (OSError, ValueError):
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
with open(stamp_path, "w") as f:
|
|
139
|
+
f.write(str(now))
|
|
140
|
+
except OSError:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main():
|
|
147
|
+
try:
|
|
148
|
+
input_data = json.load(sys.stdin)
|
|
149
|
+
except (json.JSONDecodeError, ValueError):
|
|
150
|
+
sys.exit(0)
|
|
151
|
+
|
|
152
|
+
tool_input = input_data.get("tool_input", {})
|
|
153
|
+
file_path = tool_input.get("file_path", "")
|
|
154
|
+
|
|
155
|
+
if not file_path:
|
|
156
|
+
sys.exit(0)
|
|
157
|
+
|
|
158
|
+
# Debounce: skip if tests ran recently in this session
|
|
159
|
+
session_id = input_data.get("session_id", "default")
|
|
160
|
+
if should_debounce(session_id):
|
|
161
|
+
sys.exit(0)
|
|
162
|
+
|
|
163
|
+
cwd = os.getcwd()
|
|
164
|
+
framework, cmd = detect_test_framework(cwd)
|
|
165
|
+
|
|
166
|
+
if not framework:
|
|
167
|
+
sys.exit(0)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
result = subprocess.run(
|
|
171
|
+
cmd,
|
|
172
|
+
cwd=cwd,
|
|
173
|
+
capture_output=True,
|
|
174
|
+
text=True,
|
|
175
|
+
timeout=60,
|
|
176
|
+
)
|
|
177
|
+
except subprocess.TimeoutExpired:
|
|
178
|
+
# Timeout is non-critical for PostToolUse — don't block the agent
|
|
179
|
+
json.dump(
|
|
180
|
+
{
|
|
181
|
+
"additionalContext": f"[Tests] {framework} timed out after 60s — skipping regression check."
|
|
182
|
+
},
|
|
183
|
+
sys.stdout,
|
|
184
|
+
)
|
|
185
|
+
sys.exit(0)
|
|
186
|
+
except FileNotFoundError:
|
|
187
|
+
sys.exit(0)
|
|
188
|
+
except OSError:
|
|
189
|
+
sys.exit(0)
|
|
190
|
+
|
|
191
|
+
output = (result.stdout + "\n" + result.stderr).strip()
|
|
192
|
+
if not output:
|
|
193
|
+
output = "(no test output)"
|
|
194
|
+
|
|
195
|
+
# Truncate to last 50 lines
|
|
196
|
+
lines = output.splitlines()
|
|
197
|
+
if len(lines) > 50:
|
|
198
|
+
output = "...(truncated)\n" + "\n".join(lines[-50:])
|
|
199
|
+
|
|
200
|
+
if result.returncode != 0:
|
|
201
|
+
edited = os.path.basename(file_path)
|
|
202
|
+
json.dump(
|
|
203
|
+
{
|
|
204
|
+
"error": (
|
|
205
|
+
f"Regression detected after editing {edited} "
|
|
206
|
+
f"({framework}). Fix the failing tests before continuing:\n{output}"
|
|
207
|
+
)
|
|
208
|
+
},
|
|
209
|
+
sys.stdout,
|
|
210
|
+
)
|
|
211
|
+
sys.exit(2)
|
|
212
|
+
|
|
213
|
+
json.dump(
|
|
214
|
+
{"additionalContext": f"[Tests] No regression ({framework}): all tests passed"},
|
|
215
|
+
sys.stdout,
|
|
216
|
+
)
|
|
217
|
+
sys.exit(0)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main()
|
package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/verify-tests-pass.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Verify tests pass - Stop hook for test-writer agent.
|
|
4
|
+
|
|
5
|
+
Detects the project's test framework and runs the test suite to verify
|
|
6
|
+
that tests written by the agent actually pass.
|
|
7
|
+
|
|
8
|
+
Reads hook input from stdin (JSON). Returns JSON on stdout.
|
|
9
|
+
Non-blocking on detection failures: always exits 0 if no framework found.
|
|
10
|
+
|
|
11
|
+
Exit 0: Tests pass (or no test framework / runner not installed)
|
|
12
|
+
Exit 2: Tests fail (blocks agent from completing with broken tests)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def detect_test_framework(cwd: str) -> tuple[str, list[str]]:
|
|
22
|
+
"""Detect which test framework is available in the project.
|
|
23
|
+
|
|
24
|
+
Checks for: pytest, vitest, jest, mocha, go test, cargo test.
|
|
25
|
+
Falls back to npm test if a test script is defined.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Tuple of (framework_name, command_list) or ("", []) if none found.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
entries = set(os.listdir(cwd))
|
|
32
|
+
except OSError:
|
|
33
|
+
return ("", [])
|
|
34
|
+
|
|
35
|
+
# --- Python: pytest ---
|
|
36
|
+
if "pytest.ini" in entries or "conftest.py" in entries:
|
|
37
|
+
return ("pytest", ["python3", "-m", "pytest", "--tb=short", "-q"])
|
|
38
|
+
|
|
39
|
+
for cfg_name in ("pyproject.toml", "setup.cfg", "tox.ini"):
|
|
40
|
+
cfg_path = os.path.join(cwd, cfg_name)
|
|
41
|
+
if os.path.isfile(cfg_path):
|
|
42
|
+
try:
|
|
43
|
+
with open(cfg_path, "r", encoding="utf-8") as f:
|
|
44
|
+
content = f.read()
|
|
45
|
+
if (
|
|
46
|
+
"[tool.pytest" in content
|
|
47
|
+
or "[pytest]" in content
|
|
48
|
+
or "[tool:pytest]" in content
|
|
49
|
+
):
|
|
50
|
+
return ("pytest", ["python3", "-m", "pytest", "--tb=short", "-q"])
|
|
51
|
+
except OSError:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
if "tests" in entries and os.path.isdir(os.path.join(cwd, "tests")):
|
|
55
|
+
return ("pytest", ["python3", "-m", "pytest", "--tb=short", "-q"])
|
|
56
|
+
|
|
57
|
+
for entry in entries:
|
|
58
|
+
if entry.startswith("test_") and entry.endswith(".py"):
|
|
59
|
+
return ("pytest", ["python3", "-m", "pytest", "--tb=short", "-q"])
|
|
60
|
+
|
|
61
|
+
# --- JavaScript: vitest ---
|
|
62
|
+
for name in entries:
|
|
63
|
+
if name.startswith("vitest.config"):
|
|
64
|
+
return ("vitest", ["npx", "vitest", "run", "--reporter=verbose"])
|
|
65
|
+
|
|
66
|
+
for vite_cfg in ("vite.config.ts", "vite.config.js"):
|
|
67
|
+
cfg_path = os.path.join(cwd, vite_cfg)
|
|
68
|
+
if os.path.isfile(cfg_path):
|
|
69
|
+
try:
|
|
70
|
+
with open(cfg_path, "r", encoding="utf-8") as f:
|
|
71
|
+
if "test" in f.read():
|
|
72
|
+
return (
|
|
73
|
+
"vitest",
|
|
74
|
+
["npx", "vitest", "run", "--reporter=verbose"],
|
|
75
|
+
)
|
|
76
|
+
except OSError:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# --- JavaScript: jest ---
|
|
80
|
+
for name in entries:
|
|
81
|
+
if name.startswith("jest.config"):
|
|
82
|
+
return ("jest", ["npx", "jest", "--verbose"])
|
|
83
|
+
|
|
84
|
+
pkg_json = os.path.join(cwd, "package.json")
|
|
85
|
+
if os.path.isfile(pkg_json):
|
|
86
|
+
try:
|
|
87
|
+
with open(pkg_json, "r", encoding="utf-8") as f:
|
|
88
|
+
pkg = json.loads(f.read())
|
|
89
|
+
|
|
90
|
+
if "jest" in pkg:
|
|
91
|
+
return ("jest", ["npx", "jest", "--verbose"])
|
|
92
|
+
|
|
93
|
+
dev_deps = pkg.get("devDependencies", {})
|
|
94
|
+
deps = pkg.get("dependencies", {})
|
|
95
|
+
|
|
96
|
+
# mocha
|
|
97
|
+
if "mocha" in dev_deps or "mocha" in deps:
|
|
98
|
+
return ("mocha", ["npx", "mocha", "--reporter", "spec"])
|
|
99
|
+
|
|
100
|
+
# Generic npm test script (skip default placeholder)
|
|
101
|
+
test_script = pkg.get("scripts", {}).get("test", "")
|
|
102
|
+
if test_script and "no test specified" not in test_script:
|
|
103
|
+
return ("npm-test", ["npm", "test"])
|
|
104
|
+
except (OSError, json.JSONDecodeError):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
# --- Go ---
|
|
108
|
+
if "go.mod" in entries:
|
|
109
|
+
return ("go", ["go", "test", "./...", "-count=1"])
|
|
110
|
+
|
|
111
|
+
# --- Rust ---
|
|
112
|
+
if "Cargo.toml" in entries:
|
|
113
|
+
return ("cargo", ["cargo", "test"])
|
|
114
|
+
|
|
115
|
+
return ("", [])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def main():
|
|
119
|
+
try:
|
|
120
|
+
input_data = json.load(sys.stdin)
|
|
121
|
+
except (json.JSONDecodeError, ValueError):
|
|
122
|
+
sys.exit(0)
|
|
123
|
+
|
|
124
|
+
cwd = os.getcwd()
|
|
125
|
+
framework, cmd = detect_test_framework(cwd)
|
|
126
|
+
|
|
127
|
+
if not framework:
|
|
128
|
+
sys.exit(0)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
cmd,
|
|
133
|
+
cwd=cwd,
|
|
134
|
+
capture_output=True,
|
|
135
|
+
text=True,
|
|
136
|
+
timeout=60,
|
|
137
|
+
)
|
|
138
|
+
except subprocess.TimeoutExpired:
|
|
139
|
+
json.dump(
|
|
140
|
+
{
|
|
141
|
+
"additionalContext": f"[Tests] {framework} timed out after 60s. Consider running tests manually."
|
|
142
|
+
},
|
|
143
|
+
sys.stdout,
|
|
144
|
+
)
|
|
145
|
+
sys.exit(2)
|
|
146
|
+
except FileNotFoundError:
|
|
147
|
+
# Test runner not installed — non-critical
|
|
148
|
+
sys.exit(0)
|
|
149
|
+
except OSError:
|
|
150
|
+
sys.exit(0)
|
|
151
|
+
|
|
152
|
+
output = (result.stdout + "\n" + result.stderr).strip()
|
|
153
|
+
if not output:
|
|
154
|
+
output = "(no test output)"
|
|
155
|
+
|
|
156
|
+
# Truncate to last 50 lines
|
|
157
|
+
lines = output.splitlines()
|
|
158
|
+
if len(lines) > 50:
|
|
159
|
+
output = "...(truncated)\n" + "\n".join(lines[-50:])
|
|
160
|
+
|
|
161
|
+
if result.returncode != 0:
|
|
162
|
+
json.dump(
|
|
163
|
+
{"error": f"Tests failed ({framework}):\n{output}"},
|
|
164
|
+
sys.stdout,
|
|
165
|
+
)
|
|
166
|
+
sys.exit(2)
|
|
167
|
+
|
|
168
|
+
json.dump(
|
|
169
|
+
{"additionalContext": f"[Tests] All tests passed ({framework})"},
|
|
170
|
+
sys.stdout,
|
|
171
|
+
)
|
|
172
|
+
sys.exit(0)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
main()
|