codeforge-dev 1.7.0 → 1.8.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 (136) hide show
  1. package/.devcontainer/.env +4 -6
  2. package/.devcontainer/.env.example +29 -0
  3. package/.devcontainer/.gitignore +8 -0
  4. package/.devcontainer/.secrets.example +12 -0
  5. package/.devcontainer/CHANGELOG.md +130 -0
  6. package/.devcontainer/CLAUDE.md +56 -19
  7. package/.devcontainer/README.md +111 -56
  8. package/.devcontainer/config/{main-system-prompt.md → defaults/main-system-prompt.md} +72 -0
  9. package/.devcontainer/config/file-manifest.json +20 -0
  10. package/.devcontainer/devcontainer.json +20 -0
  11. package/.devcontainer/docs/configuration-reference.md +90 -0
  12. package/.devcontainer/docs/keybindings.md +100 -0
  13. package/.devcontainer/docs/optional-features.md +129 -0
  14. package/.devcontainer/docs/plugins.md +154 -0
  15. package/.devcontainer/docs/troubleshooting.md +128 -0
  16. package/.devcontainer/features/agent-browser/install.sh +6 -0
  17. package/.devcontainer/features/ast-grep/install.sh +6 -0
  18. package/.devcontainer/features/biome/README.md +27 -0
  19. package/.devcontainer/features/biome/install.sh +6 -0
  20. package/.devcontainer/features/ccburn/install.sh +6 -0
  21. package/.devcontainer/features/ccstatusline/devcontainer-feature.json +5 -0
  22. package/.devcontainer/features/ccstatusline/install.sh +7 -0
  23. package/.devcontainer/features/ccusage/install.sh +6 -0
  24. package/.devcontainer/features/claude-monitor/install.sh +6 -0
  25. package/.devcontainer/features/dprint/README.md +30 -0
  26. package/.devcontainer/features/dprint/devcontainer-feature.json +18 -0
  27. package/.devcontainer/features/dprint/install.sh +131 -0
  28. package/.devcontainer/features/hadolint/README.md +35 -0
  29. package/.devcontainer/features/hadolint/devcontainer-feature.json +13 -0
  30. package/.devcontainer/features/hadolint/install.sh +86 -0
  31. package/.devcontainer/features/lsp-servers/devcontainer-feature.json +5 -0
  32. package/.devcontainer/features/lsp-servers/install.sh +7 -0
  33. package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +5 -0
  34. package/.devcontainer/features/mcp-qdrant/install.sh +13 -6
  35. package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +5 -0
  36. package/.devcontainer/features/mcp-reasoner/install.sh +8 -1
  37. package/.devcontainer/features/notify-hook/devcontainer-feature.json +5 -0
  38. package/.devcontainer/features/notify-hook/install.sh +7 -0
  39. package/.devcontainer/features/ruff/README.md +26 -0
  40. package/.devcontainer/features/ruff/devcontainer-feature.json +21 -0
  41. package/.devcontainer/features/ruff/install.sh +74 -0
  42. package/.devcontainer/features/shellcheck/README.md +38 -0
  43. package/.devcontainer/features/shellcheck/devcontainer-feature.json +13 -0
  44. package/.devcontainer/features/shellcheck/install.sh +24 -0
  45. package/.devcontainer/features/shfmt/README.md +37 -0
  46. package/.devcontainer/features/shfmt/devcontainer-feature.json +13 -0
  47. package/.devcontainer/features/shfmt/install.sh +85 -0
  48. package/.devcontainer/features/splitrail/devcontainer-feature.json +5 -0
  49. package/.devcontainer/features/splitrail/install.sh +7 -0
  50. package/.devcontainer/features/tmux/install.sh +8 -0
  51. package/.devcontainer/features/tree-sitter/install.sh +6 -0
  52. package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +3 -10
  53. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +1 -1
  54. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/__pycache__/format-on-stop.cpython-314.pyc +0 -0
  55. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-on-stop.py +114 -9
  56. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +1 -1
  57. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +4 -5
  58. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/__pycache__/lint-file.cpython-314.pyc +0 -0
  59. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +478 -76
  60. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/.claude-plugin/plugin.json +1 -1
  61. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/AGENT-REDIRECTION.md +226 -0
  62. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/architect.md +17 -0
  63. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/bash-exec.md +4 -4
  64. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/claude-guide.md +14 -23
  65. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/debug-logs.md +2 -0
  66. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/dependency-analyst.md +2 -0
  67. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/doc-writer.md +13 -0
  68. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/explorer.md +2 -0
  69. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/generalist.md +10 -1
  70. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/migrator.md +6 -0
  71. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/refactorer.md +4 -0
  72. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/spec-writer.md +36 -23
  73. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/statusline-config.md +3 -3
  74. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/test-writer.md +3 -0
  75. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/hooks/hooks.json +39 -0
  76. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/advisory-test-runner.cpython-314.pyc +0 -0
  77. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/collect-edited-files.cpython-314.pyc +0 -0
  78. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/commit-reminder.cpython-314.pyc +0 -0
  79. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/git-state-injector.cpython-314.pyc +0 -0
  80. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/redirect-builtin-agents.cpython-314.pyc +0 -0
  81. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/ticket-linker.cpython-314.pyc +0 -0
  82. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/todo-harvester.cpython-314.pyc +0 -0
  83. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/advisory-test-runner.py +174 -0
  84. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/collect-edited-files.py +8 -6
  85. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/commit-reminder.py +90 -0
  86. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/git-state-injector.py +114 -0
  87. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/skill-suggester.py +61 -0
  88. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/ticket-linker.py +137 -0
  89. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/todo-harvester.py +130 -0
  90. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/api-design/SKILL.md +224 -0
  91. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/api-design/references/error-handling.md +166 -0
  92. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/api-design/references/rest-conventions.md +215 -0
  93. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/ast-grep-patterns/SKILL.md +211 -0
  94. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/ast-grep-patterns/references/language-patterns.md +327 -0
  95. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/dependency-management/SKILL.md +134 -0
  96. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/dependency-management/references/ecosystem-commands.md +264 -0
  97. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/dependency-management/references/license-compliance.md +80 -0
  98. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/SKILL.md +153 -0
  99. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/references/api-doc-templates.md +221 -0
  100. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/references/docstring-formats.md +296 -0
  101. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/migration-patterns/SKILL.md +150 -0
  102. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/migration-patterns/references/javascript-migrations.md +179 -0
  103. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/migration-patterns/references/python-migrations.md +141 -0
  104. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/SKILL.md +32 -0
  105. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/__pycache__/block-dangerous.cpython-314.pyc +0 -0
  106. package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +1 -1
  107. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/__pycache__/guard-protected.cpython-314.pyc +0 -0
  108. package/.devcontainer/scripts/check-setup.sh +72 -0
  109. package/.devcontainer/scripts/setup-aliases.sh +43 -3
  110. package/.devcontainer/scripts/setup-auth.sh +74 -0
  111. package/.devcontainer/scripts/setup-config.sh +112 -22
  112. package/.devcontainer/scripts/setup-update-claude.sh +8 -0
  113. package/.devcontainer/scripts/setup.sh +46 -13
  114. package/README.md +23 -190
  115. package/package.json +1 -1
  116. package/setup.js +245 -71
  117. package/.devcontainer/features/claude-code/README.md +0 -498
  118. package/.devcontainer/features/claude-code/config/settings.json +0 -72
  119. package/.devcontainer/features/claude-code/config/system-prompt.md +0 -118
  120. package/.devcontainer/features/claude-code/config/world-building-sp.md +0 -1432
  121. package/.devcontainer/features/claude-code/devcontainer-feature.json +0 -42
  122. package/.devcontainer/features/claude-code/install.sh +0 -466
  123. package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +0 -7
  124. package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +0 -17
  125. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +0 -6
  126. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +0 -14
  127. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +0 -989
  128. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +0 -33
  129. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
  130. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +0 -71
  131. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +0 -68
  132. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +0 -120
  133. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +0 -133
  134. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +0 -253
  135. /package/.devcontainer/config/{keybindings.json → defaults/keybindings.json} +0 -0
  136. /package/.devcontainer/config/{settings.json → defaults/settings.json} +0 -0
@@ -1,11 +1,20 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Auto-lint files after editing.
3
+ Batch linter runs as a Stop hook.
4
+
5
+ Reads file paths collected by collect-edited-files.py during the
6
+ conversation turn, deduplicates them, and lints each based on
7
+ extension:
8
+ .py / .pyi → Pyright (type checking) + Ruff check (style/correctness)
9
+ .js/.jsx/.ts/… → Biome lint
10
+ .css/.graphql/… → Biome lint
11
+ .sh/.bash/.zsh/… → ShellCheck
12
+ .go → go vet
13
+ Dockerfile → hadolint
14
+ .rs → clippy (conditional)
4
15
 
5
- Reads tool input from stdin, detects file type by extension,
6
- runs appropriate linter if available.
7
16
  Outputs JSON with additionalContext containing lint warnings.
8
- Non-blocking: exit 0 regardless of lint result.
17
+ Always cleans up the temp file. Always exits 0.
9
18
  """
10
19
 
11
20
  import json
@@ -14,120 +23,513 @@ import subprocess
14
23
  import sys
15
24
  from pathlib import Path
16
25
 
17
- # Linter configuration: extension -> (command, args, name, parser)
18
- PYTHON_EXTENSIONS = {".py", ".pyi"}
26
+ # ── Extension sets ──────────────────────────────────────────────────
19
27
 
28
+ PYTHON_EXTS = {".py", ".pyi"}
29
+ BIOME_EXTS = {
30
+ ".js",
31
+ ".jsx",
32
+ ".ts",
33
+ ".tsx",
34
+ ".mjs",
35
+ ".cjs",
36
+ ".mts",
37
+ ".cts",
38
+ ".css",
39
+ ".graphql",
40
+ ".gql",
41
+ }
42
+ SHELL_EXTS = {".sh", ".bash", ".zsh", ".mksh", ".bats"}
43
+ GO_EXTS = {".go"}
44
+ RUST_EXTS = {".rs"}
20
45
 
21
- def lint_python(file_path: str) -> tuple[bool, str]:
22
- """Run pyright on a Python file.
46
+ SUBPROCESS_TIMEOUT = 10
23
47
 
24
- Returns:
25
- (success, message)
26
- """
27
- pyright_cmd = "pyright"
28
48
 
29
- # Check if pyright is available
49
+ # ── Tool resolution ─────────────────────────────────────────────────
50
+
51
+
52
+ def _which(name: str) -> str | None:
53
+ """Check if a tool is available in PATH."""
30
54
  try:
31
- subprocess.run(["which", pyright_cmd], capture_output=True, check=True)
32
- except subprocess.CalledProcessError:
33
- return True, "" # Pyright not available
55
+ result = subprocess.run(["which", name], capture_output=True, text=True)
56
+ if result.returncode == 0:
57
+ return result.stdout.strip()
58
+ except Exception:
59
+ pass
60
+ return None
61
+
62
+
63
+ def _find_tool_upward(file_path: str, tool_name: str) -> str | None:
64
+ """Walk up from file directory looking for node_modules/.bin/<tool>."""
65
+ current = Path(file_path).parent
66
+ for _ in range(20):
67
+ candidate = current / "node_modules" / ".bin" / tool_name
68
+ if candidate.is_file():
69
+ return str(candidate)
70
+ parent = current.parent
71
+ if parent == current:
72
+ break
73
+ current = parent
74
+ return None
75
+
76
+
77
+ def _find_biome(file_path: str) -> str | None:
78
+ """Find biome binary: project-local first, then global."""
79
+ local = _find_tool_upward(file_path, "biome")
80
+ if local:
81
+ return local
82
+ return _which("biome")
83
+
84
+
85
+ # ── Diagnostic formatting ──────────────────────────────────────────
86
+
87
+
88
+ def _format_issues(filename: str, diagnostics: list[dict]) -> str:
89
+ """Format a list of {severity, line, message} dicts into display text."""
90
+ if not diagnostics:
91
+ return ""
92
+
93
+ issues = []
94
+ for diag in diagnostics[:5]:
95
+ severity = diag.get("severity", "info")
96
+ message = diag.get("message", "")
97
+ line = diag.get("line", 0)
98
+
99
+ if severity == "error":
100
+ icon = "\u2717"
101
+ elif severity == "warning":
102
+ icon = "!"
103
+ else:
104
+ icon = "\u2022"
105
+
106
+ issues.append(f" {icon} Line {line}: {message}")
107
+
108
+ total = len(diagnostics)
109
+ shown = min(5, total)
110
+ header = f" {filename}: {total} issue(s)"
111
+ if total > shown:
112
+ header += f" (showing first {shown})"
113
+
114
+ return header + "\n" + "\n".join(issues)
115
+
116
+
117
+ # ── Linters ─────────────────────────────────────────────────────────
118
+
119
+
120
+ def lint_python_pyright(file_path: str) -> str:
121
+ """Run Pyright type checker on a Python file."""
122
+ pyright = _which("pyright")
123
+ if not pyright:
124
+ return ""
34
125
 
35
126
  try:
36
127
  result = subprocess.run(
37
- [pyright_cmd, "--outputjson", file_path],
128
+ [pyright, "--outputjson", file_path],
38
129
  capture_output=True,
39
130
  text=True,
40
- timeout=10,
131
+ timeout=SUBPROCESS_TIMEOUT,
41
132
  )
42
133
 
43
- # Parse pyright JSON output
44
134
  try:
45
135
  output = json.loads(result.stdout)
46
- diagnostics = output.get("generalDiagnostics", [])
136
+ except json.JSONDecodeError:
137
+ return ""
138
+
139
+ diagnostics = output.get("generalDiagnostics", [])
140
+ if not diagnostics:
141
+ return ""
142
+
143
+ parsed = [
144
+ {
145
+ "severity": d.get("severity", "info"),
146
+ "line": d.get("range", {}).get("start", {}).get("line", 0) + 1,
147
+ "message": d.get("message", ""),
148
+ }
149
+ for d in diagnostics
150
+ ]
151
+ return _format_issues(Path(file_path).name, parsed)
47
152
 
48
- if not diagnostics:
49
- return True, "[Auto-linter] Pyright: No issues found"
153
+ except subprocess.TimeoutExpired:
154
+ return f" {Path(file_path).name}: pyright timed out"
155
+ except Exception:
156
+ return ""
50
157
 
51
- # Format diagnostics
52
- issues = []
53
- for diag in diagnostics[:5]: # Limit to first 5 issues
54
- severity = diag.get("severity", "info")
55
- message = diag.get("message", "")
56
- line = diag.get("range", {}).get("start", {}).get("line", 0) + 1
57
158
 
58
- if severity == "error":
59
- icon = ""
60
- elif severity == "warning":
61
- icon = "!"
62
- else:
63
- icon = "•"
159
+ def lint_python_ruff(file_path: str) -> str:
160
+ """Run Ruff linter on a Python file."""
161
+ ruff = _which("ruff")
162
+ if not ruff:
163
+ return ""
164
+
165
+ try:
166
+ result = subprocess.run(
167
+ [ruff, "check", "--output-format=json", "--no-fix", file_path],
168
+ capture_output=True,
169
+ text=True,
170
+ timeout=SUBPROCESS_TIMEOUT,
171
+ )
172
+
173
+ try:
174
+ issues = json.loads(result.stdout)
175
+ except json.JSONDecodeError:
176
+ return ""
177
+
178
+ if not issues:
179
+ return ""
180
+
181
+ parsed = [
182
+ {
183
+ "severity": "warning",
184
+ "line": issue.get("location", {}).get("row", 0),
185
+ "message": f"[{issue.get('code', '?')}] {issue.get('message', '')}",
186
+ }
187
+ for issue in issues
188
+ ]
189
+ return _format_issues(Path(file_path).name, parsed)
190
+
191
+ except subprocess.TimeoutExpired:
192
+ return f" {Path(file_path).name}: ruff timed out"
193
+ except Exception:
194
+ return ""
195
+
196
+
197
+ def lint_biome(file_path: str) -> str:
198
+ """Run Biome linter for JS/TS/CSS/GraphQL files."""
199
+ biome = _find_biome(file_path)
200
+ if not biome:
201
+ return ""
202
+
203
+ try:
204
+ result = subprocess.run(
205
+ [biome, "lint", "--reporter=json", file_path],
206
+ capture_output=True,
207
+ text=True,
208
+ timeout=SUBPROCESS_TIMEOUT,
209
+ )
210
+
211
+ try:
212
+ output = json.loads(result.stdout)
213
+ except json.JSONDecodeError:
214
+ return ""
215
+
216
+ diagnostics = output.get("diagnostics", [])
217
+ if not diagnostics:
218
+ return ""
219
+
220
+ parsed = [
221
+ {
222
+ "severity": d.get("severity", "warning"),
223
+ "line": (
224
+ d.get("location", {}).get("span", {}).get("start", {})
225
+ if isinstance(
226
+ d.get("location", {}).get("span", {}).get("start"), int
227
+ )
228
+ else 0
229
+ ),
230
+ "message": d.get("description", d.get("message", "")),
231
+ }
232
+ for d in diagnostics
233
+ ]
234
+ return _format_issues(Path(file_path).name, parsed)
235
+
236
+ except subprocess.TimeoutExpired:
237
+ return f" {Path(file_path).name}: biome lint timed out"
238
+ except Exception:
239
+ return ""
64
240
 
65
- issues.append(f" {icon} Line {line}: {message}")
66
241
 
67
- total = len(diagnostics)
68
- shown = min(5, total)
69
- header = f"[Auto-linter] Pyright: {total} issue(s)"
70
- if total > shown:
71
- header += f" (showing first {shown})"
242
+ def lint_shellcheck(file_path: str) -> str:
243
+ """Run ShellCheck on a shell script."""
244
+ shellcheck = _which("shellcheck")
245
+ if not shellcheck:
246
+ return ""
72
247
 
73
- return True, header + "\n" + "\n".join(issues)
248
+ try:
249
+ result = subprocess.run(
250
+ [shellcheck, "--format=json", file_path],
251
+ capture_output=True,
252
+ text=True,
253
+ timeout=SUBPROCESS_TIMEOUT,
254
+ )
74
255
 
256
+ try:
257
+ issues = json.loads(result.stdout)
75
258
  except json.JSONDecodeError:
76
- # Pyright output not JSON, might be an error
77
- if result.stderr:
78
- return (
79
- True,
80
- f"[Auto-linter] Pyright error: {result.stderr.strip()[:100]}",
259
+ return ""
260
+
261
+ if not issues:
262
+ return ""
263
+
264
+ severity_map = {
265
+ "error": "error",
266
+ "warning": "warning",
267
+ "info": "info",
268
+ "style": "info",
269
+ }
270
+ parsed = [
271
+ {
272
+ "severity": severity_map.get(issue.get("level", "info"), "info"),
273
+ "line": issue.get("line", 0),
274
+ "message": f"[SC{issue.get('code', '?')}] {issue.get('message', '')}",
275
+ }
276
+ for issue in issues
277
+ ]
278
+ return _format_issues(Path(file_path).name, parsed)
279
+
280
+ except subprocess.TimeoutExpired:
281
+ return f" {Path(file_path).name}: shellcheck timed out"
282
+ except Exception:
283
+ return ""
284
+
285
+
286
+ def lint_go_vet(file_path: str) -> str:
287
+ """Run go vet on a Go file."""
288
+ go = _which("go")
289
+ if not go:
290
+ return ""
291
+
292
+ try:
293
+ result = subprocess.run(
294
+ [go, "vet", file_path],
295
+ capture_output=True,
296
+ text=True,
297
+ timeout=SUBPROCESS_TIMEOUT,
298
+ )
299
+
300
+ # go vet outputs to stderr
301
+ output = result.stderr.strip()
302
+ if not output:
303
+ return ""
304
+
305
+ lines = output.splitlines()
306
+ parsed = []
307
+ for line in lines:
308
+ # Format: file.go:LINE:COL: message
309
+ parts = line.split(":", 3)
310
+ if len(parts) >= 4:
311
+ try:
312
+ line_num = int(parts[1])
313
+ except ValueError:
314
+ line_num = 0
315
+ parsed.append(
316
+ {
317
+ "severity": "warning",
318
+ "line": line_num,
319
+ "message": parts[3].strip(),
320
+ }
81
321
  )
82
- return True, ""
322
+ elif line.strip():
323
+ parsed.append(
324
+ {
325
+ "severity": "warning",
326
+ "line": 0,
327
+ "message": line.strip(),
328
+ }
329
+ )
330
+
331
+ return _format_issues(Path(file_path).name, parsed)
83
332
 
84
333
  except subprocess.TimeoutExpired:
85
- return True, "[Auto-linter] Pyright timed out"
86
- except Exception as e:
87
- return True, f"[Auto-linter] Error: {e}"
334
+ return f" {Path(file_path).name}: go vet timed out"
335
+ except Exception:
336
+ return ""
88
337
 
89
338
 
90
- def lint_file(file_path: str) -> tuple[bool, str]:
91
- """Run appropriate linter for file.
339
+ def lint_hadolint(file_path: str) -> str:
340
+ """Run hadolint on a Dockerfile."""
341
+ hadolint = _which("hadolint")
342
+ if not hadolint:
343
+ return ""
92
344
 
93
- Returns:
94
- (success, message)
95
- """
96
- ext = Path(file_path).suffix.lower()
345
+ try:
346
+ result = subprocess.run(
347
+ [hadolint, "--format", "json", file_path],
348
+ capture_output=True,
349
+ text=True,
350
+ timeout=SUBPROCESS_TIMEOUT,
351
+ )
97
352
 
98
- if ext in PYTHON_EXTENSIONS:
99
- return lint_python(file_path)
353
+ try:
354
+ issues = json.loads(result.stdout)
355
+ except json.JSONDecodeError:
356
+ return ""
357
+
358
+ if not issues:
359
+ return ""
360
+
361
+ severity_map = {
362
+ "error": "error",
363
+ "warning": "warning",
364
+ "info": "info",
365
+ "style": "info",
366
+ }
367
+ parsed = [
368
+ {
369
+ "severity": severity_map.get(issue.get("level", "info"), "info"),
370
+ "line": issue.get("line", 0),
371
+ "message": f"[{issue.get('code', '?')}] {issue.get('message', '')}",
372
+ }
373
+ for issue in issues
374
+ ]
375
+ return _format_issues(Path(file_path).name, parsed)
100
376
 
101
- # No linter available for this file type
102
- return True, ""
377
+ except subprocess.TimeoutExpired:
378
+ return f" {Path(file_path).name}: hadolint timed out"
379
+ except Exception:
380
+ return ""
103
381
 
104
382
 
105
- def main():
383
+ def lint_clippy(file_path: str) -> str:
384
+ """Run clippy on a Rust file (conditional — only if cargo is in PATH)."""
385
+ cargo = _which("cargo")
386
+ if not cargo:
387
+ return ""
388
+
106
389
  try:
107
- input_data = json.load(sys.stdin)
108
- tool_input = input_data.get("tool_input", {})
109
- file_path = tool_input.get("file_path", "")
390
+ result = subprocess.run(
391
+ [cargo, "clippy", "--message-format=json", "--", "-W", "clippy::all"],
392
+ capture_output=True,
393
+ text=True,
394
+ timeout=SUBPROCESS_TIMEOUT,
395
+ cwd=str(Path(file_path).parent),
396
+ )
397
+
398
+ lines = result.stdout.strip().splitlines()
399
+ parsed = []
400
+ target_name = Path(file_path).name
401
+
402
+ for line in lines:
403
+ try:
404
+ msg = json.loads(line)
405
+ except json.JSONDecodeError:
406
+ continue
407
+
408
+ if msg.get("reason") != "compiler-message":
409
+ continue
410
+ inner = msg.get("message", {})
411
+ level = inner.get("level", "")
412
+ if level not in ("warning", "error"):
413
+ continue
414
+
415
+ # Match diagnostics to the target file
416
+ spans = inner.get("spans", [])
417
+ line_num = 0
418
+ for span in spans:
419
+ if span.get("is_primary") and target_name in span.get("file_name", ""):
420
+ line_num = span.get("line_start", 0)
421
+ break
422
+
423
+ if line_num or not spans:
424
+ parsed.append(
425
+ {
426
+ "severity": level,
427
+ "line": line_num,
428
+ "message": inner.get("message", ""),
429
+ }
430
+ )
431
+
432
+ return _format_issues(Path(file_path).name, parsed)
433
+
434
+ except subprocess.TimeoutExpired:
435
+ return f" {Path(file_path).name}: clippy timed out"
436
+ except Exception:
437
+ return ""
438
+
110
439
 
111
- if not file_path:
112
- sys.exit(0)
440
+ # ── Main ────────────────────────────────────────────────────────────
113
441
 
114
- # Check if file exists
115
- if not os.path.exists(file_path):
116
- sys.exit(0)
117
442
 
118
- _, message = lint_file(file_path)
443
+ def main():
444
+ try:
445
+ input_data = json.load(sys.stdin)
446
+ except (json.JSONDecodeError, ValueError):
447
+ sys.exit(0)
119
448
 
120
- if message:
121
- # Output context for Claude
122
- print(json.dumps({"additionalContext": message}))
449
+ if input_data.get("stop_hook_active"):
450
+ sys.exit(0)
123
451
 
452
+ session_id = input_data.get("session_id", "")
453
+ if not session_id:
124
454
  sys.exit(0)
125
455
 
126
- except json.JSONDecodeError:
456
+ tmp_path = f"/tmp/claude-lint-files-{session_id}"
457
+
458
+ try:
459
+ with open(tmp_path) as f:
460
+ raw_paths = f.read().splitlines()
461
+ except FileNotFoundError:
127
462
  sys.exit(0)
128
- except Exception as e:
129
- print(f"Hook error: {e}", file=sys.stderr)
463
+ except OSError:
130
464
  sys.exit(0)
465
+ finally:
466
+ try:
467
+ os.unlink(tmp_path)
468
+ except OSError:
469
+ pass
470
+
471
+ # Deduplicate, filter to existing files (no longer Python-only)
472
+ seen: set[str] = set()
473
+ paths: list[str] = []
474
+ for p in raw_paths:
475
+ p = p.strip()
476
+ if p and p not in seen and os.path.isfile(p):
477
+ seen.add(p)
478
+ paths.append(p)
479
+
480
+ if not paths:
481
+ sys.exit(0)
482
+
483
+ # Collect results grouped by linter
484
+ all_results: dict[str, list[str]] = {}
485
+
486
+ for path in paths:
487
+ ext = Path(path).suffix.lower()
488
+ name = Path(path).name
489
+
490
+ if ext in PYTHON_EXTS:
491
+ msg = lint_python_pyright(path)
492
+ if msg:
493
+ all_results.setdefault("Pyright", []).append(msg)
494
+ msg = lint_python_ruff(path)
495
+ if msg:
496
+ all_results.setdefault("Ruff", []).append(msg)
497
+
498
+ elif ext in BIOME_EXTS:
499
+ msg = lint_biome(path)
500
+ if msg:
501
+ all_results.setdefault("Biome", []).append(msg)
502
+
503
+ elif ext in SHELL_EXTS:
504
+ msg = lint_shellcheck(path)
505
+ if msg:
506
+ all_results.setdefault("ShellCheck", []).append(msg)
507
+
508
+ elif ext in GO_EXTS:
509
+ msg = lint_go_vet(path)
510
+ if msg:
511
+ all_results.setdefault("go vet", []).append(msg)
512
+
513
+ elif name == "Dockerfile" or ext == ".dockerfile":
514
+ msg = lint_hadolint(path)
515
+ if msg:
516
+ all_results.setdefault("hadolint", []).append(msg)
517
+
518
+ elif ext in RUST_EXTS:
519
+ msg = lint_clippy(path)
520
+ if msg:
521
+ all_results.setdefault("clippy", []).append(msg)
522
+
523
+ if all_results:
524
+ sections = []
525
+ for linter_name, results in all_results.items():
526
+ sections.append(
527
+ f"[Auto-linter] {linter_name} results:\n" + "\n".join(results)
528
+ )
529
+ output = "\n\n".join(sections)
530
+ print(json.dumps({"additionalContext": output}))
531
+
532
+ sys.exit(0)
131
533
 
132
534
 
133
535
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-directive",
3
- "description": "Coding standards and skills for the CodeDirective project",
3
+ "description": "17 custom agents, 16 coding skills, agent redirection, syntax validation, and skill auto-suggestion",
4
4
  "version": "1.0.0",
5
5
  "author": {
6
6
  "name": "AnExiledDev"