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
@@ -21,6 +21,11 @@
21
21
  "type": "command",
22
22
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/skill-suggester.py",
23
23
  "timeout": 3
24
+ },
25
+ {
26
+ "type": "command",
27
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/ticket-linker.py",
28
+ "timeout": 12
24
29
  }
25
30
  ]
26
31
  }
@@ -37,6 +42,40 @@
37
42
  ]
38
43
  }
39
44
  ],
45
+ "Stop": [
46
+ {
47
+ "matcher": "",
48
+ "hooks": [
49
+ {
50
+ "type": "command",
51
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/advisory-test-runner.py",
52
+ "timeout": 65
53
+ },
54
+ {
55
+ "type": "command",
56
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/commit-reminder.py",
57
+ "timeout": 8
58
+ }
59
+ ]
60
+ }
61
+ ],
62
+ "SessionStart": [
63
+ {
64
+ "matcher": "",
65
+ "hooks": [
66
+ {
67
+ "type": "command",
68
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/git-state-injector.py",
69
+ "timeout": 10
70
+ },
71
+ {
72
+ "type": "command",
73
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/todo-harvester.py",
74
+ "timeout": 8
75
+ }
76
+ ]
77
+ }
78
+ ],
40
79
  "PostToolUse": [
41
80
  {
42
81
  "matcher": "Edit|Write",
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Advisory test runner — Stop hook that injects test results as context.
4
+
5
+ Detects the project's test framework and runs the test suite. Results are
6
+ returned as additionalContext so Claude sees pass/fail info without being
7
+ blocked. If tests fail, Claude's next response will naturally address them.
8
+
9
+ Reads hook input from stdin (JSON). Returns JSON on stdout.
10
+ Always exits 0 (advisory, never blocking).
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import subprocess
16
+ import sys
17
+
18
+
19
+ def detect_test_framework(cwd: str) -> tuple[str, list[str]]:
20
+ """Detect which test framework is available in the project.
21
+
22
+ Checks for: pytest, vitest, jest, mocha, go test, cargo test.
23
+ Falls back to npm test if a test script is defined.
24
+
25
+ Returns:
26
+ Tuple of (framework_name, command_list) or ("", []) if none found.
27
+ """
28
+ try:
29
+ entries = set(os.listdir(cwd))
30
+ except OSError:
31
+ return ("", [])
32
+
33
+ # --- Python: pytest ---
34
+ if "pytest.ini" in entries or "conftest.py" in entries:
35
+ return ("pytest", ["python3", "-m", "pytest", "--tb=short", "-q"])
36
+
37
+ for cfg_name in ("pyproject.toml", "setup.cfg", "tox.ini"):
38
+ cfg_path = os.path.join(cwd, cfg_name)
39
+ if os.path.isfile(cfg_path):
40
+ try:
41
+ with open(cfg_path, "r", encoding="utf-8") as f:
42
+ content = f.read()
43
+ if (
44
+ "[tool.pytest" in content
45
+ or "[pytest]" in content
46
+ or "[tool:pytest]" in content
47
+ ):
48
+ return ("pytest", ["python3", "-m", "pytest", "--tb=short", "-q"])
49
+ except OSError:
50
+ pass
51
+
52
+ if "tests" in entries and os.path.isdir(os.path.join(cwd, "tests")):
53
+ return ("pytest", ["python3", "-m", "pytest", "--tb=short", "-q"])
54
+
55
+ for entry in entries:
56
+ if entry.startswith("test_") and entry.endswith(".py"):
57
+ return ("pytest", ["python3", "-m", "pytest", "--tb=short", "-q"])
58
+
59
+ # --- JavaScript: vitest ---
60
+ for name in entries:
61
+ if name.startswith("vitest.config"):
62
+ return ("vitest", ["npx", "vitest", "run", "--reporter=verbose"])
63
+
64
+ for vite_cfg in ("vite.config.ts", "vite.config.js"):
65
+ cfg_path = os.path.join(cwd, vite_cfg)
66
+ if os.path.isfile(cfg_path):
67
+ try:
68
+ with open(cfg_path, "r", encoding="utf-8") as f:
69
+ if "test" in f.read():
70
+ return (
71
+ "vitest",
72
+ ["npx", "vitest", "run", "--reporter=verbose"],
73
+ )
74
+ except OSError:
75
+ pass
76
+
77
+ # --- JavaScript: jest ---
78
+ for name in entries:
79
+ if name.startswith("jest.config"):
80
+ return ("jest", ["npx", "jest", "--verbose"])
81
+
82
+ pkg_json = os.path.join(cwd, "package.json")
83
+ if os.path.isfile(pkg_json):
84
+ try:
85
+ with open(pkg_json, "r", encoding="utf-8") as f:
86
+ pkg = json.loads(f.read())
87
+
88
+ if "jest" in pkg:
89
+ return ("jest", ["npx", "jest", "--verbose"])
90
+
91
+ dev_deps = pkg.get("devDependencies", {})
92
+ deps = pkg.get("dependencies", {})
93
+
94
+ if "mocha" in dev_deps or "mocha" in deps:
95
+ return ("mocha", ["npx", "mocha", "--reporter", "spec"])
96
+
97
+ test_script = pkg.get("scripts", {}).get("test", "")
98
+ if test_script and "no test specified" not in test_script:
99
+ return ("npm-test", ["npm", "test"])
100
+ except (OSError, json.JSONDecodeError):
101
+ pass
102
+
103
+ # --- Go ---
104
+ if "go.mod" in entries:
105
+ return ("go", ["go", "test", "./...", "-count=1"])
106
+
107
+ # --- Rust ---
108
+ if "Cargo.toml" in entries:
109
+ return ("cargo", ["cargo", "test"])
110
+
111
+ return ("", [])
112
+
113
+
114
+ def main():
115
+ try:
116
+ input_data = json.load(sys.stdin)
117
+ except (json.JSONDecodeError, ValueError):
118
+ sys.exit(0)
119
+
120
+ # Skip if another Stop hook is already blocking
121
+ if input_data.get("stop_hook_active"):
122
+ sys.exit(0)
123
+
124
+ cwd = os.getcwd()
125
+ framework, cmd = detect_test_framework(cwd)
126
+
127
+ if not framework:
128
+ sys.exit(0)
129
+
130
+ try:
131
+ result = subprocess.run(
132
+ cmd,
133
+ cwd=cwd,
134
+ capture_output=True,
135
+ text=True,
136
+ timeout=60,
137
+ )
138
+ except subprocess.TimeoutExpired:
139
+ json.dump(
140
+ {"additionalContext": f"[Tests] {framework} timed out after 60s"},
141
+ sys.stdout,
142
+ )
143
+ sys.exit(0)
144
+ except (FileNotFoundError, OSError):
145
+ # Test runner not installed or not accessible
146
+ sys.exit(0)
147
+
148
+ output = (result.stdout + "\n" + result.stderr).strip()
149
+
150
+ if result.returncode == 0:
151
+ # Extract test count from output if possible
152
+ json.dump(
153
+ {"additionalContext": f"[Tests] All tests passed ({framework})"},
154
+ sys.stdout,
155
+ )
156
+ sys.exit(0)
157
+
158
+ # Tests failed — truncate to last 30 lines
159
+ if not output:
160
+ output = "(no test output)"
161
+
162
+ lines = output.splitlines()
163
+ if len(lines) > 30:
164
+ output = "...(truncated)\n" + "\n".join(lines[-30:])
165
+
166
+ json.dump(
167
+ {"additionalContext": (f"[Tests] Some tests FAILED ({framework}):\n{output}")},
168
+ sys.stdout,
169
+ )
170
+ sys.exit(0)
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()
@@ -30,12 +30,14 @@ def main():
30
30
  if not os.path.isfile(file_path):
31
31
  sys.exit(0)
32
32
 
33
- tmp_path = f"/tmp/claude-edited-files-{session_id}"
34
- try:
35
- with open(tmp_path, "a") as f:
36
- f.write(file_path + "\n")
37
- except OSError:
38
- pass # non-critical, don't block Claude
33
+ # Write to both formatter and linter temp files (independent pipelines)
34
+ for prefix in ("claude-edited-files", "claude-lint-files"):
35
+ tmp_path = f"/tmp/{prefix}-{session_id}"
36
+ try:
37
+ with open(tmp_path, "a") as f:
38
+ f.write(file_path + "\n")
39
+ except OSError:
40
+ pass # non-critical, don't block Claude
39
41
 
40
42
  sys.exit(0)
41
43
 
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Commit reminder — Stop hook that advises about uncommitted changes.
4
+
5
+ On Stop, checks for uncommitted changes (staged + unstaged) and injects
6
+ an advisory reminder as additionalContext. Claude sees it and can
7
+ naturally ask the user if they want to commit.
8
+
9
+ Reads hook input from stdin (JSON). Returns JSON on stdout.
10
+ Always exits 0 (advisory, never blocking).
11
+ """
12
+
13
+ import json
14
+ import subprocess
15
+ import sys
16
+
17
+ GIT_CMD_TIMEOUT = 5
18
+
19
+
20
+ def _run_git(args: list[str]) -> str | None:
21
+ """Run a git command and return stdout, or None on any failure."""
22
+ try:
23
+ result = subprocess.run(
24
+ ["git"] + args,
25
+ capture_output=True,
26
+ text=True,
27
+ timeout=GIT_CMD_TIMEOUT,
28
+ )
29
+ if result.returncode == 0:
30
+ return result.stdout.strip()
31
+ except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
32
+ pass
33
+ return None
34
+
35
+
36
+ def main():
37
+ try:
38
+ input_data = json.load(sys.stdin)
39
+ except (json.JSONDecodeError, ValueError):
40
+ sys.exit(0)
41
+
42
+ # Skip if another Stop hook is already blocking
43
+ if input_data.get("stop_hook_active"):
44
+ sys.exit(0)
45
+
46
+ # Check if there are any changes at all
47
+ porcelain = _run_git(["status", "--porcelain"])
48
+ if porcelain is None:
49
+ # Not a git repo or git not available
50
+ sys.exit(0)
51
+ if not porcelain.strip():
52
+ # Working tree clean
53
+ sys.exit(0)
54
+
55
+ lines = porcelain.strip().splitlines()
56
+ total = len(lines)
57
+
58
+ # Count staged vs unstaged
59
+ staged = 0
60
+ unstaged = 0
61
+ for line in lines:
62
+ index_status = line[0:1] if len(line) > 0 else " "
63
+ worktree_status = line[1:2] if len(line) > 1 else " "
64
+
65
+ if index_status not in (" ", "?"):
66
+ staged += 1
67
+ if worktree_status not in (" ", "?"):
68
+ unstaged += 1
69
+ if line[0:2] == "??":
70
+ unstaged += 1
71
+
72
+ parts = []
73
+ if staged:
74
+ parts.append(f"{staged} staged")
75
+ if unstaged:
76
+ parts.append(f"{unstaged} unstaged")
77
+
78
+ summary = ", ".join(parts) if parts else f"{total} changed"
79
+
80
+ message = (
81
+ f"[Uncommitted Changes] {total} files with changes ({summary}).\n"
82
+ "Consider asking the user if they'd like to commit before finishing."
83
+ )
84
+
85
+ json.dump({"additionalContext": message}, sys.stdout)
86
+ sys.exit(0)
87
+
88
+
89
+ if __name__ == "__main__":
90
+ main()
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Git state injector — SessionStart hook that injects repo state as context.
4
+
5
+ Runs git commands to gather branch, status, recent commits, and uncommitted
6
+ changes. Injects the results as additionalContext so Claude starts every
7
+ session knowing the current git state.
8
+
9
+ Reads hook input from stdin (JSON). Returns JSON on stdout.
10
+ Always exits 0 (advisory, never blocking).
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import subprocess
16
+ import sys
17
+
18
+ GIT_CMD_TIMEOUT = 5
19
+ STATUS_LINE_CAP = 20
20
+ DIFF_STAT_LINE_CAP = 15
21
+ TOTAL_OUTPUT_CAP = 2000
22
+
23
+
24
+ def _run_git(args: list[str], cwd: str) -> str | None:
25
+ """Run a git command and return stdout, or None on any failure."""
26
+ try:
27
+ result = subprocess.run(
28
+ ["git"] + args,
29
+ cwd=cwd,
30
+ capture_output=True,
31
+ text=True,
32
+ timeout=GIT_CMD_TIMEOUT,
33
+ )
34
+ if result.returncode == 0:
35
+ return result.stdout.strip()
36
+ except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
37
+ pass
38
+ return None
39
+
40
+
41
+ def _cap_lines(text: str, limit: int) -> str:
42
+ """Truncate text to a maximum number of lines."""
43
+ lines = text.splitlines()
44
+ if len(lines) <= limit:
45
+ return text
46
+ return "\n".join(lines[:limit]) + f"\n...({len(lines) - limit} more lines)"
47
+
48
+
49
+ def main():
50
+ try:
51
+ json.load(sys.stdin)
52
+ except (json.JSONDecodeError, ValueError):
53
+ pass
54
+
55
+ cwd = os.getcwd()
56
+
57
+ # Check if we're in a git repo at all
58
+ branch = _run_git(["branch", "--show-current"], cwd)
59
+ if branch is None:
60
+ # Not a git repo or git not available
61
+ sys.exit(0)
62
+
63
+ sections = []
64
+ sections.append(f"Branch: {branch or '(detached HEAD)'}")
65
+
66
+ # Git status
67
+ status = _run_git(["status", "--short"], cwd)
68
+ if status:
69
+ status_lines = status.splitlines()
70
+ modified = sum(1 for l in status_lines if l.strip() and l[0:1] == "M")
71
+ added = sum(1 for l in status_lines if l[0:1] == "A")
72
+ deleted = sum(1 for l in status_lines if l[0:1] == "D")
73
+ untracked = sum(1 for l in status_lines if l[0:2] == "??")
74
+
75
+ counts = []
76
+ if modified:
77
+ counts.append(f"{modified} modified")
78
+ if added:
79
+ counts.append(f"{added} added")
80
+ if deleted:
81
+ counts.append(f"{deleted} deleted")
82
+ if untracked:
83
+ counts.append(f"{untracked} untracked")
84
+
85
+ summary = ", ".join(counts) if counts else f"{len(status_lines)} changed"
86
+ sections.append(f"Status: {summary}")
87
+ sections.append(_cap_lines(status, STATUS_LINE_CAP))
88
+ else:
89
+ sections.append("Status: clean")
90
+
91
+ # Recent commits
92
+ log = _run_git(["log", "--oneline", "-5"], cwd)
93
+ if log:
94
+ sections.append(f"Recent commits:\n{log}")
95
+
96
+ # Uncommitted diff stats
97
+ diff_stat = _run_git(["diff", "--stat"], cwd)
98
+ if diff_stat:
99
+ sections.append(
100
+ f"Uncommitted changes:\n{_cap_lines(diff_stat, DIFF_STAT_LINE_CAP)}"
101
+ )
102
+
103
+ output = "[Git State]\n" + "\n".join(sections)
104
+
105
+ # Cap total output to avoid context bloat
106
+ if len(output) > TOTAL_OUTPUT_CAP:
107
+ output = output[:TOTAL_OUTPUT_CAP] + "\n...(truncated)"
108
+
109
+ json.dump({"additionalContext": output}, sys.stdout)
110
+ sys.exit(0)
111
+
112
+
113
+ if __name__ == "__main__":
114
+ main()
@@ -235,6 +235,67 @@ SKILLS = {
235
235
  "throughput",
236
236
  ],
237
237
  },
238
+ "api-design": {
239
+ "phrases": [
240
+ "api design",
241
+ "rest api design",
242
+ "design an api",
243
+ "design a rest",
244
+ "api convention",
245
+ "endpoint design",
246
+ "api versioning",
247
+ "pagination pattern",
248
+ "error response format",
249
+ ],
250
+ "terms": ["openapi", "swagger", "rfc7807"],
251
+ },
252
+ "ast-grep-patterns": {
253
+ "phrases": [
254
+ "ast-grep",
255
+ "ast grep",
256
+ "structural search",
257
+ "structural code search",
258
+ "syntax-aware search",
259
+ "tree-sitter",
260
+ ],
261
+ "terms": ["sg run", "ast-grep", "tree-sitter"],
262
+ },
263
+ "dependency-management": {
264
+ "phrases": [
265
+ "check dependencies",
266
+ "audit dependencies",
267
+ "outdated packages",
268
+ "dependency health",
269
+ "license check",
270
+ "unused dependencies",
271
+ "vulnerability scan",
272
+ ],
273
+ "terms": ["pip-audit", "npm audit", "cargo audit", "govulncheck"],
274
+ },
275
+ "documentation-patterns": {
276
+ "phrases": [
277
+ "write a readme",
278
+ "write documentation",
279
+ "add docstrings",
280
+ "add jsdoc",
281
+ "document the api",
282
+ "documentation template",
283
+ "update the docs",
284
+ ],
285
+ "terms": ["docstring", "jsdoc", "tsdoc", "godoc", "rustdoc"],
286
+ },
287
+ "migration-patterns": {
288
+ "phrases": [
289
+ "migrate from",
290
+ "upgrade to",
291
+ "version upgrade",
292
+ "framework migration",
293
+ "bump python",
294
+ "upgrade pydantic",
295
+ "migrate express",
296
+ ],
297
+ "terms": ["migrate", "migration", "upgrade"],
298
+ },
238
299
  }
239
300
 
240
301
  # Pre-compile term patterns for whole-word matching
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Ticket linker — UserPromptSubmit hook that auto-fetches GitHub issues/PRs.
4
+
5
+ When the user's prompt contains #123 or a full GitHub issue/PR URL,
6
+ fetches the ticket body via `gh` and injects it as additionalContext
7
+ so Claude has the full ticket context without the user copy-pasting.
8
+
9
+ Reads hook input from stdin (JSON). Returns JSON on stdout.
10
+ Always exits 0 (advisory, never blocking).
11
+ """
12
+
13
+ import json
14
+ import re
15
+ import subprocess
16
+ import sys
17
+
18
+ GH_CMD_TIMEOUT = 5
19
+ MAX_REFS = 3
20
+ BODY_CHAR_CAP = 1500
21
+ TOTAL_OUTPUT_CAP = 3000
22
+
23
+ # Short refs: #123 (but not inside URLs or hex colors)
24
+ SHORT_REF_RE = re.compile(r"(?<![/\w])#(\d+)\b")
25
+
26
+ # Full GitHub URLs: github.com/owner/repo/issues/123 or .../pull/123
27
+ URL_REF_RE = re.compile(r"github\.com/[^/\s]+/[^/\s]+/(?:issues|pull)/(\d+)")
28
+
29
+
30
+ def extract_refs(prompt: str) -> list[int]:
31
+ """Extract deduplicated issue/PR numbers from the prompt."""
32
+ seen: set[int] = set()
33
+ refs: list[int] = []
34
+
35
+ for match in SHORT_REF_RE.finditer(prompt):
36
+ num = int(match.group(1))
37
+ if num not in seen and num > 0:
38
+ seen.add(num)
39
+ refs.append(num)
40
+
41
+ for match in URL_REF_RE.finditer(prompt):
42
+ num = int(match.group(1))
43
+ if num not in seen and num > 0:
44
+ seen.add(num)
45
+ refs.append(num)
46
+
47
+ return refs[:MAX_REFS]
48
+
49
+
50
+ def fetch_ticket(number: int) -> str | None:
51
+ """Fetch a GitHub issue/PR via gh CLI. Returns formatted string or None."""
52
+ try:
53
+ result = subprocess.run(
54
+ [
55
+ "gh",
56
+ "issue",
57
+ "view",
58
+ str(number),
59
+ "--json",
60
+ "number,title,body,state,labels",
61
+ ],
62
+ capture_output=True,
63
+ text=True,
64
+ timeout=GH_CMD_TIMEOUT,
65
+ )
66
+ except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
67
+ return None
68
+
69
+ if result.returncode != 0:
70
+ return None
71
+
72
+ try:
73
+ data = json.loads(result.stdout)
74
+ except (json.JSONDecodeError, ValueError):
75
+ return None
76
+
77
+ title = data.get("title", "(no title)")
78
+ state = data.get("state", "UNKNOWN")
79
+ body = data.get("body", "") or ""
80
+ labels = data.get("labels", [])
81
+
82
+ label_names = [lb.get("name", "") for lb in labels if lb.get("name")]
83
+ label_str = ", ".join(label_names) if label_names else "none"
84
+
85
+ # Truncate body
86
+ if len(body) > BODY_CHAR_CAP:
87
+ body = body[:BODY_CHAR_CAP] + "\n...(truncated)"
88
+
89
+ parts = [
90
+ f"[Ticket #{number}] {title}",
91
+ f"State: {state} | Labels: {label_str}",
92
+ ]
93
+ if body.strip():
94
+ parts.append(body.strip())
95
+
96
+ return "\n".join(parts)
97
+
98
+
99
+ def main():
100
+ raw = sys.stdin.read().strip()
101
+ if not raw:
102
+ sys.exit(0)
103
+
104
+ try:
105
+ data = json.loads(raw)
106
+ except (json.JSONDecodeError, ValueError):
107
+ sys.exit(0)
108
+
109
+ prompt = data.get("prompt", "")
110
+ if not prompt:
111
+ sys.exit(0)
112
+
113
+ refs = extract_refs(prompt)
114
+ if not refs:
115
+ sys.exit(0)
116
+
117
+ tickets: list[str] = []
118
+ for number in refs:
119
+ ticket = fetch_ticket(number)
120
+ if ticket:
121
+ tickets.append(ticket)
122
+
123
+ if not tickets:
124
+ sys.exit(0)
125
+
126
+ output = "\n\n---\n\n".join(tickets)
127
+
128
+ # Cap total output
129
+ if len(output) > TOTAL_OUTPUT_CAP:
130
+ output = output[:TOTAL_OUTPUT_CAP] + "\n...(truncated)"
131
+
132
+ json.dump({"additionalContext": output}, sys.stdout)
133
+ sys.exit(0)
134
+
135
+
136
+ if __name__ == "__main__":
137
+ main()