codeforge-dev 1.7.0 → 1.9.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 (158) 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 +181 -0
  6. package/.devcontainer/CLAUDE.md +57 -20
  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/defaults/rules/spec-workflow.md +67 -0
  10. package/.devcontainer/config/defaults/rules/workspace-scope.md +7 -0
  11. package/.devcontainer/config/defaults/settings.json +67 -0
  12. package/.devcontainer/config/file-manifest.json +32 -0
  13. package/.devcontainer/devcontainer.json +20 -0
  14. package/.devcontainer/docs/configuration-reference.md +90 -0
  15. package/.devcontainer/docs/keybindings.md +100 -0
  16. package/.devcontainer/docs/optional-features.md +129 -0
  17. package/.devcontainer/docs/plugins.md +154 -0
  18. package/.devcontainer/docs/troubleshooting.md +128 -0
  19. package/.devcontainer/features/agent-browser/install.sh +6 -0
  20. package/.devcontainer/features/ast-grep/install.sh +6 -0
  21. package/.devcontainer/features/biome/README.md +27 -0
  22. package/.devcontainer/features/biome/install.sh +6 -0
  23. package/.devcontainer/features/ccburn/install.sh +6 -0
  24. package/.devcontainer/features/ccstatusline/devcontainer-feature.json +5 -0
  25. package/.devcontainer/features/ccstatusline/install.sh +7 -0
  26. package/.devcontainer/features/ccusage/install.sh +6 -0
  27. package/.devcontainer/features/claude-monitor/install.sh +6 -0
  28. package/.devcontainer/features/dprint/README.md +30 -0
  29. package/.devcontainer/features/dprint/devcontainer-feature.json +18 -0
  30. package/.devcontainer/features/dprint/install.sh +131 -0
  31. package/.devcontainer/features/hadolint/README.md +35 -0
  32. package/.devcontainer/features/hadolint/devcontainer-feature.json +13 -0
  33. package/.devcontainer/features/hadolint/install.sh +86 -0
  34. package/.devcontainer/features/lsp-servers/devcontainer-feature.json +5 -0
  35. package/.devcontainer/features/lsp-servers/install.sh +7 -0
  36. package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +5 -0
  37. package/.devcontainer/features/mcp-qdrant/install.sh +13 -6
  38. package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +5 -0
  39. package/.devcontainer/features/mcp-reasoner/install.sh +8 -1
  40. package/.devcontainer/features/notify-hook/devcontainer-feature.json +5 -0
  41. package/.devcontainer/features/notify-hook/install.sh +7 -0
  42. package/.devcontainer/features/ruff/README.md +26 -0
  43. package/.devcontainer/features/ruff/devcontainer-feature.json +21 -0
  44. package/.devcontainer/features/ruff/install.sh +74 -0
  45. package/.devcontainer/features/shellcheck/README.md +38 -0
  46. package/.devcontainer/features/shellcheck/devcontainer-feature.json +13 -0
  47. package/.devcontainer/features/shellcheck/install.sh +24 -0
  48. package/.devcontainer/features/shfmt/README.md +37 -0
  49. package/.devcontainer/features/shfmt/devcontainer-feature.json +13 -0
  50. package/.devcontainer/features/shfmt/install.sh +85 -0
  51. package/.devcontainer/features/splitrail/devcontainer-feature.json +5 -0
  52. package/.devcontainer/features/splitrail/install.sh +7 -0
  53. package/.devcontainer/features/tmux/install.sh +8 -0
  54. package/.devcontainer/features/tree-sitter/install.sh +6 -0
  55. package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +104 -104
  56. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/.claude-plugin/plugin.json +7 -0
  57. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/README.md +158 -0
  58. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/hooks/hooks.json +39 -0
  59. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/collect-edited-files.py +47 -0
  60. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/format-on-stop.py +297 -0
  61. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/lint-file.py +536 -0
  62. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/syntax-validator.py +146 -0
  63. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +1 -1
  64. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/__pycache__/format-on-stop.cpython-314.pyc +0 -0
  65. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-on-stop.py +114 -9
  66. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +1 -1
  67. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +4 -5
  68. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/__pycache__/lint-file.cpython-314.pyc +0 -0
  69. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +478 -76
  70. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/.claude-plugin/plugin.json +1 -1
  71. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/AGENT-REDIRECTION.md +226 -0
  72. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/architect.md +94 -1
  73. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/bash-exec.md +4 -4
  74. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/claude-guide.md +14 -23
  75. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/debug-logs.md +20 -0
  76. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/dependency-analyst.md +20 -0
  77. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/doc-writer.md +99 -1
  78. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/explorer.md +20 -0
  79. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/generalist.md +152 -9
  80. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/git-archaeologist.md +18 -0
  81. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/migrator.md +114 -1
  82. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/perf-profiler.md +24 -0
  83. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/refactorer.md +101 -1
  84. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/researcher.md +33 -1
  85. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/security-auditor.md +24 -0
  86. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/spec-writer.md +65 -24
  87. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/statusline-config.md +3 -3
  88. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/test-writer.md +99 -1
  89. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/hooks/hooks.json +100 -56
  90. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/advisory-test-runner.cpython-314.pyc +0 -0
  91. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/collect-edited-files.cpython-314.pyc +0 -0
  92. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/commit-reminder.cpython-314.pyc +0 -0
  93. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/git-state-injector.cpython-314.pyc +0 -0
  94. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/redirect-builtin-agents.cpython-314.pyc +0 -0
  95. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/ticket-linker.cpython-314.pyc +0 -0
  96. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/todo-harvester.cpython-314.pyc +0 -0
  97. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/advisory-test-runner.py +174 -0
  98. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/collect-edited-files.py +8 -6
  99. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/commit-reminder.py +90 -0
  100. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/git-state-injector.py +114 -0
  101. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/skill-suggester.py +61 -0
  102. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/spec-reminder.py +121 -0
  103. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/ticket-linker.py +137 -0
  104. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/todo-harvester.py +130 -0
  105. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/api-design/SKILL.md +224 -0
  106. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/api-design/references/error-handling.md +166 -0
  107. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/api-design/references/rest-conventions.md +215 -0
  108. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/ast-grep-patterns/SKILL.md +211 -0
  109. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/ast-grep-patterns/references/language-patterns.md +327 -0
  110. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/dependency-management/SKILL.md +134 -0
  111. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/dependency-management/references/ecosystem-commands.md +264 -0
  112. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/dependency-management/references/license-compliance.md +80 -0
  113. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/SKILL.md +153 -0
  114. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/references/api-doc-templates.md +221 -0
  115. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/references/docstring-formats.md +296 -0
  116. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/migration-patterns/SKILL.md +150 -0
  117. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/migration-patterns/references/javascript-migrations.md +179 -0
  118. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/migration-patterns/references/python-migrations.md +141 -0
  119. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-check/SKILL.md +86 -0
  120. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/SKILL.md +97 -0
  121. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/references/backlog-template.md +7 -0
  122. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/references/roadmap-template.md +13 -0
  123. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-new/SKILL.md +101 -0
  124. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-new/references/template.md +110 -0
  125. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-update/SKILL.md +124 -0
  126. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/SKILL.md +32 -0
  127. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/__pycache__/block-dangerous.cpython-314.pyc +0 -0
  128. package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +1 -1
  129. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/__pycache__/guard-protected.cpython-314.pyc +0 -0
  130. package/.devcontainer/scripts/check-setup.sh +72 -0
  131. package/.devcontainer/scripts/setup-aliases.sh +43 -3
  132. package/.devcontainer/scripts/setup-auth.sh +74 -0
  133. package/.devcontainer/scripts/setup-config.sh +117 -24
  134. package/.devcontainer/scripts/setup-update-claude.sh +8 -0
  135. package/.devcontainer/scripts/setup.sh +46 -13
  136. package/README.md +23 -190
  137. package/package.json +42 -42
  138. package/setup.js +245 -71
  139. package/.devcontainer/config/settings.json +0 -70
  140. package/.devcontainer/features/claude-code/README.md +0 -498
  141. package/.devcontainer/features/claude-code/config/settings.json +0 -72
  142. package/.devcontainer/features/claude-code/config/system-prompt.md +0 -118
  143. package/.devcontainer/features/claude-code/config/world-building-sp.md +0 -1432
  144. package/.devcontainer/features/claude-code/devcontainer-feature.json +0 -42
  145. package/.devcontainer/features/claude-code/install.sh +0 -466
  146. package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +0 -7
  147. package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +0 -17
  148. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +0 -6
  149. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +0 -14
  150. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +0 -989
  151. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +0 -33
  152. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
  153. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +0 -71
  154. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +0 -68
  155. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +0 -120
  156. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +0 -133
  157. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +0 -253
  158. /package/.devcontainer/config/{keybindings.json → defaults/keybindings.json} +0 -0
@@ -0,0 +1,536 @@
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-cq-lint-{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
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()
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Data file syntax validator.
4
+
5
+ Validates JSON, JSONC, YAML, and TOML files after editing.
6
+ Uses Python stdlib only (plus PyYAML if available).
7
+
8
+ Reads tool input from stdin, validates syntax, reports errors.
9
+ Non-blocking: always exits 0.
10
+ """
11
+
12
+ import json
13
+ import os
14
+ import re
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ EXTENSIONS = {".json", ".jsonc", ".yaml", ".yml", ".toml"}
19
+
20
+
21
+ def strip_jsonc_comments(text: str) -> str:
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
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)
32
+ return text
33
+
34
+
35
+ def validate_json(file_path: str, is_jsonc: bool) -> str:
36
+ """Validate JSON/JSONC syntax.
37
+
38
+ Returns:
39
+ Error message string, or empty string if valid.
40
+ """
41
+ with open(file_path, "r", encoding="utf-8") as f:
42
+ content = f.read()
43
+
44
+ if is_jsonc:
45
+ content = strip_jsonc_comments(content)
46
+
47
+ try:
48
+ json.loads(content)
49
+ return ""
50
+ except json.JSONDecodeError as e:
51
+ return f"[Syntax] JSON error at line {e.lineno}, col {e.colno}: {e.msg}"
52
+
53
+
54
+ def validate_yaml(file_path: str) -> str:
55
+ """Validate YAML syntax.
56
+
57
+ Returns:
58
+ Error message string, or empty string if valid.
59
+ """
60
+ try:
61
+ import yaml
62
+ except ImportError:
63
+ return "" # PyYAML not available, skip
64
+
65
+ try:
66
+ with open(file_path, "r", encoding="utf-8") as f:
67
+ yaml.safe_load(f)
68
+ return ""
69
+ except yaml.YAMLError as e:
70
+ if hasattr(e, "problem_mark"):
71
+ mark = e.problem_mark
72
+ return f"[Syntax] YAML error at line {mark.line + 1}, col {mark.column + 1}: {e.problem}"
73
+ return f"[Syntax] YAML error: {e}"
74
+
75
+
76
+ def validate_toml(file_path: str) -> str:
77
+ """Validate TOML syntax.
78
+
79
+ Returns:
80
+ Error message string, or empty string if valid.
81
+ """
82
+ try:
83
+ import tomllib
84
+ except ImportError:
85
+ return "" # Python < 3.11, skip
86
+
87
+ try:
88
+ with open(file_path, "rb") as f:
89
+ tomllib.load(f)
90
+ return ""
91
+ except tomllib.TOMLDecodeError as e:
92
+ return f"[Syntax] TOML error: {e}"
93
+
94
+
95
+ def validate(file_path: str) -> str:
96
+ """Validate file syntax based on extension.
97
+
98
+ Returns:
99
+ Error message string, or empty string if valid.
100
+ """
101
+ ext = Path(file_path).suffix.lower()
102
+
103
+ if ext == ".jsonc":
104
+ return validate_json(file_path, is_jsonc=True)
105
+ elif ext == ".json":
106
+ return validate_json(file_path, is_jsonc=False)
107
+ elif ext in {".yaml", ".yml"}:
108
+ return validate_yaml(file_path)
109
+ elif ext == ".toml":
110
+ return validate_toml(file_path)
111
+
112
+ return ""
113
+
114
+
115
+ def main():
116
+ try:
117
+ input_data = json.load(sys.stdin)
118
+ tool_input = input_data.get("tool_input", {})
119
+ file_path = tool_input.get("file_path", "")
120
+
121
+ if not file_path:
122
+ sys.exit(0)
123
+
124
+ ext = Path(file_path).suffix.lower()
125
+ if ext not in EXTENSIONS:
126
+ sys.exit(0)
127
+
128
+ if not os.path.isfile(file_path):
129
+ sys.exit(0)
130
+
131
+ message = validate(file_path)
132
+
133
+ if message:
134
+ print(json.dumps({"additionalContext": message}))
135
+
136
+ sys.exit(0)
137
+
138
+ except json.JSONDecodeError:
139
+ sys.exit(0)
140
+ except Exception as e:
141
+ print(f"Hook error: {e}", file=sys.stderr)
142
+ sys.exit(0)
143
+
144
+
145
+ if __name__ == "__main__":
146
+ main()
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-formatter",
3
- "description": "Batch-formats edited files at Stop (Black for Python, gofmt for Go, Biome for JS/TS/CSS)",
3
+ "description": "Batch-formats edited files at Stop (Ruff for Python, gofmt for Go, Biome for JS/TS/CSS/JSON/GraphQL/HTML, shfmt for Shell, dprint for Markdown/YAML/TOML/Dockerfile, rustfmt for Rust)",
4
4
  "author": {
5
5
  "name": "AnExiledDev"
6
6
  }