codeforge-dev 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.devcontainer/.env +3 -0
  2. package/.devcontainer/CHANGELOG.md +125 -0
  3. package/.devcontainer/CLAUDE.md +41 -11
  4. package/.devcontainer/README.md +73 -3
  5. package/.devcontainer/config/defaults/main-system-prompt.md +187 -201
  6. package/.devcontainer/config/defaults/rules/session-search.md +66 -0
  7. package/.devcontainer/config/defaults/rules/spec-workflow.md +48 -13
  8. package/.devcontainer/config/defaults/settings.json +2 -1
  9. package/.devcontainer/config/defaults/writing-system-prompt.md +143 -0
  10. package/.devcontainer/config/file-manifest.json +12 -0
  11. package/.devcontainer/connect-external-terminal.sh +17 -17
  12. package/.devcontainer/devcontainer.json +150 -144
  13. package/.devcontainer/features/ccms/README.md +50 -0
  14. package/.devcontainer/features/ccms/devcontainer-feature.json +21 -0
  15. package/.devcontainer/features/ccms/install.sh +105 -0
  16. package/.devcontainer/features/ccstatusline/install.sh +24 -2
  17. package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +8 -1
  18. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/architect.md +5 -3
  19. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/claude-guide.md +1 -1
  20. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/doc-writer.md +7 -7
  21. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/generalist.md +1 -0
  22. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/spec-writer.md +22 -12
  23. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/hooks/hooks.json +11 -1
  24. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/skill-suggester.cpython-314.pyc +0 -0
  25. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/advisory-test-runner.py +186 -13
  26. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/git-state-injector.py +15 -4
  27. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/inject-cwd.py +37 -0
  28. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/skill-suggester.py +24 -0
  29. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/spec-reminder.py +4 -2
  30. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/SKILL.md +1 -1
  31. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-build/SKILL.md +353 -0
  32. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-build/references/review-checklist.md +175 -0
  33. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-check/SKILL.md +28 -15
  34. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/SKILL.md +16 -13
  35. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/references/backlog-template.md +19 -3
  36. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/references/milestones-template.md +32 -0
  37. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-new/SKILL.md +28 -20
  38. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-new/references/template.md +35 -6
  39. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-refine/SKILL.md +194 -0
  40. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-review/SKILL.md +229 -0
  41. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-update/SKILL.md +24 -2
  42. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/SKILL.md +20 -13
  43. package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/.claude-plugin/plugin.json +38 -5
  44. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/.claude-plugin/plugin.json +7 -0
  45. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/hooks/hooks.json +17 -0
  46. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/__pycache__/guard-workspace-scope.cpython-314.pyc +0 -0
  47. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/guard-workspace-scope.py +132 -0
  48. package/.devcontainer/scripts/check-setup.sh +24 -25
  49. package/.devcontainer/scripts/setup-aliases.sh +95 -90
  50. package/.devcontainer/scripts/setup-projects.sh +172 -131
  51. package/.devcontainer/scripts/setup-terminal.sh +48 -0
  52. package/.devcontainer/scripts/setup-update-claude.sh +49 -107
  53. package/.devcontainer/scripts/setup.sh +4 -17
  54. package/README.md +2 -2
  55. package/package.json +1 -1
  56. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/references/roadmap-template.md +0 -13
@@ -2,9 +2,10 @@
2
2
  """
3
3
  Advisory test runner — Stop hook that injects test results as context.
4
4
 
5
- Detects the project's test framework and runs the test suite. Results are
6
- returned as additionalContext so Claude sees pass/fail info without being
7
- blocked. If tests fail, Claude's next response will naturally address them.
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, command_list) or ("", []) if none found.
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", "./...", "-count=1"])
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, cmd = detect_test_framework(cwd)
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=60,
309
+ timeout=TIMEOUT_SECONDS,
137
310
  )
138
311
  except subprocess.TimeoutExpired:
139
312
  json.dump(
140
- {"additionalContext": f"[Tests] {framework} timed out after 60s"},
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,
@@ -47,20 +47,31 @@ def _cap_lines(text: str, limit: int) -> str:
47
47
 
48
48
 
49
49
  def main():
50
+ # Parse hook input to get cwd from Claude Code (falls back to os.getcwd())
51
+ cwd = os.getcwd()
50
52
  try:
51
- json.load(sys.stdin)
53
+ input_data = json.load(sys.stdin)
54
+ cwd = input_data.get("cwd", cwd)
52
55
  except (json.JSONDecodeError, ValueError):
53
56
  pass
54
57
 
55
- cwd = os.getcwd()
56
-
57
58
  # Check if we're in a git repo at all
58
59
  branch = _run_git(["branch", "--show-current"], cwd)
59
60
  if branch is None:
60
- # Not a git repo or git not available
61
+ # Not a git repo or git not available — still inject working directory
62
+ output = (
63
+ f"[Git State]\n"
64
+ f"Working Directory: {cwd} — restrict all file operations to this "
65
+ f"directory unless explicitly instructed otherwise."
66
+ )
67
+ json.dump({"additionalContext": output}, sys.stdout)
61
68
  sys.exit(0)
62
69
 
63
70
  sections = []
71
+ sections.append(
72
+ f"Working Directory: {cwd} — restrict all file operations to this "
73
+ f"directory unless explicitly instructed otherwise."
74
+ )
64
75
  sections.append(f"Branch: {branch or '(detached HEAD)'}")
65
76
 
66
77
  # Git status
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CWD injector — SubagentStart hook that tells subagents the working directory.
4
+
5
+ Reads hook input from stdin (JSON), extracts cwd, and returns it as
6
+ additionalContext so every subagent knows where to scope its work.
7
+
8
+ Always exits 0 (advisory, never blocking).
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+
15
+
16
+ def main():
17
+ cwd = os.getcwd()
18
+ try:
19
+ input_data = json.load(sys.stdin)
20
+ cwd = input_data.get("cwd", cwd)
21
+ except (json.JSONDecodeError, ValueError):
22
+ pass
23
+
24
+ json.dump(
25
+ {
26
+ "additionalContext": (
27
+ f"Working Directory: {cwd} — restrict all file operations to "
28
+ f"this directory unless explicitly instructed otherwise."
29
+ )
30
+ },
31
+ sys.stdout,
32
+ )
33
+ sys.exit(0)
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -296,6 +296,30 @@ SKILLS = {
296
296
  ],
297
297
  "terms": ["migrate", "migration", "upgrade"],
298
298
  },
299
+ "spec-build": {
300
+ "phrases": [
301
+ "implement the spec",
302
+ "build from spec",
303
+ "start building",
304
+ "spec-build",
305
+ "implement this feature from the spec",
306
+ "build what the spec describes",
307
+ "implement from the spec",
308
+ "build the feature",
309
+ ],
310
+ "terms": ["spec-build"],
311
+ },
312
+ "spec-review": {
313
+ "phrases": [
314
+ "review the spec",
315
+ "check spec adherence",
316
+ "verify implementation",
317
+ "spec-review",
318
+ "does code match spec",
319
+ "audit implementation",
320
+ ],
321
+ "terms": ["spec-review"],
322
+ },
299
323
  }
300
324
 
301
325
  # Pre-compile term patterns for whole-word matching
@@ -109,8 +109,10 @@ def main():
109
109
  message = (
110
110
  f"[Spec Reminder] Code was modified in {dirs_str} "
111
111
  "but no specs were updated. "
112
- "Use /spec-update to update the relevant spec, "
113
- "or /spec-new if no spec exists for this feature."
112
+ "Use /spec-review to verify implementation against the spec, "
113
+ "then /spec-update to close the loop. "
114
+ "Use /spec-new if no spec exists for this feature, "
115
+ "or /spec-refine if the spec is still in draft status."
114
116
  )
115
117
 
116
118
  json.dump({"additionalContext": message}, sys.stdout)
@@ -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 be **≤200 lines** each. Split large documents by concern. Each file should be independently useful.
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