codeforge-dev 1.13.0 → 1.14.2

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 (148) hide show
  1. package/.devcontainer/CHANGELOG.md +146 -4
  2. package/.devcontainer/CLAUDE.md +61 -276
  3. package/.devcontainer/README.md +1 -1
  4. package/.devcontainer/config/defaults/ccstatusline-settings.json +147 -0
  5. package/.devcontainer/config/defaults/main-system-prompt.md +6 -1
  6. package/.devcontainer/config/defaults/rules/spec-workflow.md +1 -55
  7. package/.devcontainer/config/file-manifest.json +14 -0
  8. package/.devcontainer/devcontainer.json +19 -1
  9. package/.devcontainer/docs/optional-features.md +0 -65
  10. package/.devcontainer/docs/plugins.md +38 -23
  11. package/.devcontainer/features/ast-grep/devcontainer-feature.json +0 -1
  12. package/.devcontainer/features/biome/install.sh +13 -0
  13. package/.devcontainer/features/ccburn/devcontainer-feature.json +0 -6
  14. package/.devcontainer/features/ccms/devcontainer-feature.json +0 -1
  15. package/.devcontainer/features/ccms/install.sh +1 -1
  16. package/.devcontainer/features/ccstatusline/devcontainer-feature.json +0 -1
  17. package/.devcontainer/features/ccstatusline/install.sh +17 -115
  18. package/.devcontainer/features/ccusage/devcontainer-feature.json +0 -6
  19. package/.devcontainer/features/chromaterm/README.md +42 -0
  20. package/.devcontainer/features/chromaterm/chromaterm.yml +35 -0
  21. package/.devcontainer/features/chromaterm/devcontainer-feature.json +22 -0
  22. package/.devcontainer/features/chromaterm/install.sh +113 -0
  23. package/.devcontainer/features/claude-monitor/devcontainer-feature.json +0 -6
  24. package/.devcontainer/features/claude-session-dashboard/README.md +2 -2
  25. package/.devcontainer/features/claude-session-dashboard/devcontainer-feature.json +2 -4
  26. package/.devcontainer/features/claude-session-dashboard/install.sh +2 -2
  27. package/.devcontainer/features/kitty-terminfo/README.md +32 -0
  28. package/.devcontainer/features/kitty-terminfo/devcontainer-feature.json +13 -0
  29. package/.devcontainer/features/kitty-terminfo/install.sh +72 -0
  30. package/.devcontainer/features/lsp-servers/devcontainer-feature.json +0 -1
  31. package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +0 -7
  32. package/.devcontainer/features/shellcheck/install.sh +6 -2
  33. package/.devcontainer/features/tree-sitter/devcontainer-feature.json +0 -7
  34. package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +37 -69
  35. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/.claude-plugin/plugin.json +0 -1
  36. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/README.md +197 -0
  37. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/architect.md +3 -1
  38. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/bash-exec.md +3 -0
  39. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/claude-guide.md +4 -1
  40. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/debug-logs.md +6 -1
  41. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/dependency-analyst.md +5 -1
  42. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/doc-writer.md +4 -1
  43. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/explorer.md +3 -1
  44. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/generalist.md +9 -1
  45. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/git-archaeologist.md +3 -0
  46. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/migrator.md +4 -1
  47. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/perf-profiler.md +4 -1
  48. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/refactorer.md +5 -1
  49. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/researcher.md +5 -1
  50. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/security-auditor.md +4 -1
  51. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/spec-writer.md +3 -1
  52. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/statusline-config.md +4 -1
  53. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/test-writer.md +4 -1
  54. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/hooks/hooks.json +23 -1
  55. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/guard-readonly-bash.py +2 -2
  56. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/inject-cwd.py +7 -4
  57. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/task-completed-check.py +166 -0
  58. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/teammate-idle-check.py +81 -0
  59. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/verify-no-regression.py +14 -10
  60. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/verify-tests-pass.py +2 -14
  61. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/README.md +17 -31
  62. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/hooks/hooks.json +5 -1
  63. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/advisory-test-runner.py +9 -8
  64. package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/README.md +28 -0
  65. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/README.md +28 -0
  66. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +2 -2
  67. package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/README.md +28 -0
  68. package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +0 -1
  69. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/README.md +28 -0
  70. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected-bash.py +1 -1
  71. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +2 -2
  72. package/.devcontainer/plugins/devs-marketplace/plugins/session-context/.claude-plugin/plugin.json +0 -1
  73. package/.devcontainer/plugins/devs-marketplace/plugins/session-context/README.md +140 -0
  74. package/.devcontainer/plugins/devs-marketplace/plugins/session-context/hooks/hooks.json +0 -1
  75. package/.devcontainer/plugins/devs-marketplace/plugins/session-context/scripts/commit-reminder.py +3 -2
  76. package/.devcontainer/plugins/devs-marketplace/plugins/session-context/scripts/git-state-injector.py +18 -2
  77. package/.devcontainer/plugins/devs-marketplace/plugins/session-context/scripts/todo-harvester.py +9 -1
  78. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/.claude-plugin/plugin.json +0 -1
  79. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/README.md +158 -0
  80. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/hooks/hooks.json +1 -14
  81. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/scripts/skill-suggester.py +189 -100
  82. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/api-design/SKILL.md +9 -6
  83. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/ast-grep-patterns/SKILL.md +7 -6
  84. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/claude-agent-sdk/SKILL.md +8 -8
  85. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/claude-code-headless/SKILL.md +8 -9
  86. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/debugging/SKILL.md +11 -7
  87. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/dependency-management/SKILL.md +10 -6
  88. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/docker/SKILL.md +8 -8
  89. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/docker-py/SKILL.md +9 -7
  90. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/documentation-patterns/SKILL.md +7 -6
  91. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/fastapi/SKILL.md +9 -8
  92. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/git-forensics/SKILL.md +11 -9
  93. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/migration-patterns/SKILL.md +7 -6
  94. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/performance-profiling/SKILL.md +10 -8
  95. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/pydantic-ai/SKILL.md +8 -7
  96. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/refactoring-patterns/SKILL.md +9 -8
  97. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/security-checklist/SKILL.md +9 -8
  98. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/skill-building/SKILL.md +7 -7
  99. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/sqlite/SKILL.md +9 -7
  100. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/svelte5/SKILL.md +7 -8
  101. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/team/SKILL.md +71 -5
  102. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/testing/SKILL.md +10 -7
  103. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/.claude-plugin/plugin.json +0 -1
  104. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/README.md +192 -0
  105. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/hooks/hooks.json +0 -1
  106. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/scripts/spec-reminder.py +3 -2
  107. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-build/SKILL.md +9 -6
  108. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-check/SKILL.md +10 -5
  109. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-init/SKILL.md +8 -4
  110. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-new/SKILL.md +8 -4
  111. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-refine/SKILL.md +10 -7
  112. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-review/SKILL.md +10 -6
  113. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-update/SKILL.md +10 -5
  114. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/specification-writing/SKILL.md +9 -9
  115. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +1 -2
  116. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/README.md +28 -0
  117. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/hooks/hooks.json +0 -1
  118. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/scripts/ticket-linker.py +9 -1
  119. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/README.md +104 -32
  120. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/hooks/hooks.json +49 -3
  121. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/guard-workspace-scope.py +269 -56
  122. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/inject-workspace-cwd.py +44 -0
  123. package/.devcontainer/scripts/setup-aliases.sh +13 -5
  124. package/.devcontainer/scripts/setup-config.sh +1 -0
  125. package/README.md +5 -5
  126. package/package.json +6 -2
  127. package/setup.js +3 -2
  128. package/.devcontainer/.env +0 -33
  129. package/.devcontainer/features/README.md +0 -126
  130. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +0 -7
  131. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/README.md +0 -81
  132. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +0 -17
  133. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/__pycache__/format-on-stop.cpython-314.pyc +0 -0
  134. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-on-stop.py +0 -297
  135. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +0 -7
  136. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/README.md +0 -92
  137. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +0 -17
  138. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/__pycache__/lint-file.cpython-314.pyc +0 -0
  139. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +0 -536
  140. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/__pycache__/block-dangerous.cpython-314.pyc +0 -0
  141. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/__pycache__/guard-protected.cpython-314.pyc +0 -0
  142. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +0 -184
  143. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/__pycache__/guard-workspace-scope.cpython-314.pyc +0 -0
  144. /package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/{.claude-plugin/commands/debug.md → skills/debug/SKILL.md} +0 -0
  145. /package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/{.claude-plugin/commands/ticket/357/200/272create-pr.md" → skills/ticketcreate-pr/SKILL.md} +0 -0
  146. /package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/{.claude-plugin/commands/ticket/357/200/272new.md" → skills/ticketnew/SKILL.md} +0 -0
  147. /package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/{.claude-plugin/commands/ticket/357/200/272review-commit.md" → skills/ticketreview-commit/SKILL.md} +0 -0
  148. /package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/{.claude-plugin/commands/ticket/357/200/272work.md" → skills/ticketwork/SKILL.md} +0 -0
@@ -1,536 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
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)
15
-
16
- Outputs JSON with additionalContext containing lint warnings.
17
- Always cleans up the temp file. Always exits 0.
18
- """
19
-
20
- import json
21
- import os
22
- import subprocess
23
- import sys
24
- from pathlib import Path
25
-
26
- # ── Extension sets ──────────────────────────────────────────────────
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"}
45
-
46
- SUBPROCESS_TIMEOUT = 10
47
-
48
-
49
- # ── Tool resolution ─────────────────────────────────────────────────
50
-
51
-
52
- def _which(name: str) -> str | None:
53
- """Check if a tool is available in PATH."""
54
- try:
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 ""
125
-
126
- try:
127
- result = subprocess.run(
128
- [pyright, "--outputjson", file_path],
129
- capture_output=True,
130
- text=True,
131
- timeout=SUBPROCESS_TIMEOUT,
132
- )
133
-
134
- try:
135
- output = json.loads(result.stdout)
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)
152
-
153
- except subprocess.TimeoutExpired:
154
- return f" {Path(file_path).name}: pyright timed out"
155
- except Exception:
156
- return ""
157
-
158
-
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 ""
240
-
241
-
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 ""
247
-
248
- try:
249
- result = subprocess.run(
250
- [shellcheck, "--format=json", file_path],
251
- capture_output=True,
252
- text=True,
253
- timeout=SUBPROCESS_TIMEOUT,
254
- )
255
-
256
- try:
257
- issues = json.loads(result.stdout)
258
- except json.JSONDecodeError:
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
- }
321
- )
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)
332
-
333
- except subprocess.TimeoutExpired:
334
- return f" {Path(file_path).name}: go vet timed out"
335
- except Exception:
336
- return ""
337
-
338
-
339
- def lint_hadolint(file_path: str) -> str:
340
- """Run hadolint on a Dockerfile."""
341
- hadolint = _which("hadolint")
342
- if not hadolint:
343
- return ""
344
-
345
- try:
346
- result = subprocess.run(
347
- [hadolint, "--format", "json", file_path],
348
- capture_output=True,
349
- text=True,
350
- timeout=SUBPROCESS_TIMEOUT,
351
- )
352
-
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)
376
-
377
- except subprocess.TimeoutExpired:
378
- return f" {Path(file_path).name}: hadolint timed out"
379
- except Exception:
380
- return ""
381
-
382
-
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
-
389
- try:
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
-
439
-
440
- # ── Main ────────────────────────────────────────────────────────────
441
-
442
-
443
- def main():
444
- try:
445
- input_data = json.load(sys.stdin)
446
- except (json.JSONDecodeError, ValueError):
447
- sys.exit(0)
448
-
449
- if input_data.get("stop_hook_active"):
450
- sys.exit(0)
451
-
452
- session_id = input_data.get("session_id", "")
453
- if not session_id:
454
- sys.exit(0)
455
-
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:
462
- sys.exit(0)
463
- except OSError:
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)
533
-
534
-
535
- if __name__ == "__main__":
536
- main()