claude-devkit-cli 1.0.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.
@@ -0,0 +1,99 @@
1
+ EXECUTE, not EXPLORE. Write tests, run them, make them pass. Don't read unrelated files.
2
+
3
+ ## Phase 0: Build Context
4
+
5
+ 1. **Find what changed:**
6
+ ```
7
+ BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||') || BASE="main"
8
+ git diff --name-only "$BASE"...HEAD
9
+ ```
10
+ If `$ARGUMENTS` provided → scope to that file or feature only.
11
+ If no changes → "No source changes found. Specify a file or feature."
12
+
13
+ 2. **Read the test plan** in `docs/test-plans/` if it exists — this is your roadmap.
14
+ 3. **Read the spec** in `docs/specs/` if it exists — understand the INTENT behind the code.
15
+ 4. **Read existing tests** for the changed files — find patterns, fixtures, naming conventions. Don't duplicate.
16
+
17
+ ---
18
+
19
+ ## Phase 1: Decide What to Test
20
+
21
+ Test behavior, not implementation. If the internals change but behavior stays the same, tests should still pass.
22
+
23
+ **What NOT to test:**
24
+ - Private/internal methods (test through public API)
25
+ - Framework behavior (test YOUR handler, not that Express routes work)
26
+ - Trivial getters/setters (unless they have validation)
27
+ - Implementation details (HOW it works — test WHAT it does)
28
+
29
+ **Quality check for each test:**
30
+ - Does it test one concept? If it fails, do you know exactly what broke?
31
+ - Is it independent? No test depends on another running first.
32
+ - Is it deterministic? No random, no time-dependent, no external service calls.
33
+ - Does the name describe the scenario? (`returns_error_when_input_is_empty`)
34
+
35
+ ---
36
+
37
+ ## Phase 2: Write Tests
38
+
39
+ Follow the project's existing test patterns. If using `$ARGUMENTS` as a filter, use `--filter` when running:
40
+ ```
41
+ bash scripts/build-test.sh --filter "$ARGUMENTS"
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Phase 3: Build and Run
47
+
48
+ Compile/typecheck first (tsc --noEmit, cargo check, go vet, swift build, etc.).
49
+
50
+ Then run tests:
51
+ ```
52
+ bash scripts/build-test.sh
53
+ ```
54
+
55
+ If `scripts/build-test.sh` doesn't exist, detect and run directly:
56
+ | Marker | Command |
57
+ |--------|---------|
58
+ | vitest config / vitest in package.json | `npx vitest run` |
59
+ | jest config / jest in package.json | `npx jest --no-cache` |
60
+ | pyproject.toml / pytest.ini | `python3 -m pytest -x` |
61
+ | Cargo.toml | `cargo test` |
62
+ | go.mod | `go test ./...` |
63
+ | build.gradle | `./gradlew test` |
64
+ | *.sln | `dotnet test` |
65
+ | Package.swift | `swift test` |
66
+ | Gemfile | `bundle exec rspec` |
67
+
68
+ ---
69
+
70
+ ## Phase 4: Fix Loop
71
+
72
+ If tests fail:
73
+ 1. Read error output. Is the test wrong or the production code wrong?
74
+ 2. If production code seems wrong → **ASK the user:** "Test expects X but code does Y. Fix production code or adjust test?"
75
+ 3. Fix test code only. Re-run. Max 3 attempts, then stop and report.
76
+
77
+ **NEVER:**
78
+ - Fix production code without asking
79
+ - Delete or weaken existing tests
80
+ - Add `skip`/`xit`/`@disabled` to hide failures
81
+ - Use mocks solely to avoid a real failure
82
+
83
+ ---
84
+
85
+ ## Phase 5: Summary
86
+
87
+ ```
88
+ Tests: X added, Y modified, Z unchanged
89
+ Result: All passing ✓
90
+ Coverage: [critical uncovered paths if any]
91
+ Files: [test files touched]
92
+ Plan: [TC-001 ✓, TC-002 ✓, TC-005 new]
93
+ ```
94
+
95
+ If behavior changed: "Consider updating the spec in docs/specs/."
96
+
97
+ ## Rules
98
+ 1. **Behavior over implementation.** Test what code DOES, not how.
99
+ 2. **Independent tests.** Each test sets up its own state, cleans up after.
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ // comment-guard.js — PreToolUse hook for Claude Code
3
+ //
4
+ // Detects when an Edit would replace real code with placeholder comments like
5
+ // "// ... existing code ..." or "// rest of implementation". This is a
6
+ // common LLM failure mode where the model gets lazy and drops code.
7
+ //
8
+ // Blocking: Yes — exits 2 to reject the edit BEFORE it is applied.
9
+ // Event: PreToolUse on Edit|MultiEdit
10
+
11
+ "use strict";
12
+
13
+ const fs = require("fs");
14
+
15
+ // Patterns that indicate lazy placeholder comments (case-insensitive)
16
+ const PLACEHOLDER_PATTERNS = [
17
+ /\/\/\s*\.{2,}\s*(existing|remaining|rest|previous|other|same|original)/i,
18
+ /\/\/\s*\.{2,}\s*(code|implementation|logic|methods|functions|properties)/i,
19
+ /\/\/\s*\[.*(?:remains?|unchanged|omitted|removed|truncated|collapsed).*\]/i,
20
+ /\/\/\s*(?:unchanged|omitted|keep|stays?)\s*(?:as\s*(?:is|before))?/i,
21
+ /\/\*\s*\.{2,}\s*\*\//, // /* ... */
22
+ /#\s*\.{2,}\s*(existing|remaining|rest|previous)/i, // Python: # ... existing
23
+ /\/\/\s*TODO:?\s*implement/i, // // TODO: implement
24
+ /\/\/\s*(?:add|put|insert)\s+.*\s+here/i, // // add code here
25
+ /\/\/\s*<\s*(?:your|actual)\s+/i, // // <your code>
26
+ /pass\s*#\s*(?:TODO|placeholder|implement)/i, // Python: pass # TODO
27
+ ];
28
+
29
+ function isCommentLine(line) {
30
+ const trimmed = line.trim();
31
+ if (trimmed === "") return true; // blank lines are neutral
32
+ if (trimmed.startsWith("//")) return true;
33
+ if (trimmed.startsWith("#") && !trimmed.startsWith("#!")) return true;
34
+ if (trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.endsWith("*/")) return true;
35
+ if (trimmed.startsWith("<!--")) return true;
36
+ if (trimmed === "pass") return true; // Python pass statement as placeholder
37
+ return false;
38
+ }
39
+
40
+ function getCodeLineCount(text) {
41
+ if (!text) return 0;
42
+ return text.split("\n").filter((line) => !isCommentLine(line)).length;
43
+ }
44
+
45
+ function hasPlaceholderPattern(text) {
46
+ return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(text));
47
+ }
48
+
49
+ function main() {
50
+ let input;
51
+ try {
52
+ input = fs.readFileSync(0, "utf-8").trim();
53
+ } catch {
54
+ process.exit(0);
55
+ }
56
+
57
+ if (!input) process.exit(0);
58
+
59
+ let payload;
60
+ try {
61
+ payload = JSON.parse(input);
62
+ } catch {
63
+ process.exit(0);
64
+ }
65
+
66
+ const oldStr = payload.tool_input?.old_string;
67
+ const newStr = payload.tool_input?.new_string;
68
+
69
+ // Only applies to Edit (not Write — Write creates new content)
70
+ if (!oldStr || !newStr) process.exit(0);
71
+
72
+ // If old content was already all comments, this is just editing comments — allow
73
+ const oldCodeLines = getCodeLineCount(oldStr);
74
+ if (oldCodeLines === 0) process.exit(0);
75
+
76
+ // If new content has real code, allow (even if it also has comments)
77
+ const newCodeLines = getCodeLineCount(newStr);
78
+ if (newCodeLines > 0) process.exit(0);
79
+
80
+ // At this point: old had code, new is all comments/blanks
81
+ // Check if the new content contains placeholder patterns
82
+ if (hasPlaceholderPattern(newStr)) {
83
+ process.stderr.write(
84
+ "Blocked: real code was replaced with placeholder comments. " +
85
+ "Preserve the original code and make targeted changes instead.\n"
86
+ );
87
+ process.exit(2);
88
+ }
89
+
90
+ // New is all comments but no placeholder pattern — could be intentional
91
+ // (e.g., replacing a code block with documentation comments)
92
+ // Allow but only if the replacement is not drastically shorter
93
+ const oldLines = oldStr.split("\n").length;
94
+ const newLines = newStr.split("\n").length;
95
+
96
+ if (newLines < oldLines * 0.3) {
97
+ // Suspiciously shorter and all comments — likely lazy replacement
98
+ process.stderr.write(
99
+ "Blocked: code block was replaced with a much shorter comment-only block. " +
100
+ "This looks like an accidental truncation. Preserve the original code.\n"
101
+ );
102
+ process.exit(2);
103
+ }
104
+
105
+ // Seems intentional
106
+ process.exit(0);
107
+ }
108
+
109
+ try {
110
+ main();
111
+ } catch {
112
+ // Never crash — allow on error
113
+ process.exit(0);
114
+ }
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ // file-guard.js — PostToolUse hook for Claude Code
3
+ //
4
+ // Warns when a Write/Edit operation produces a file exceeding a line threshold.
5
+ // Non-blocking: always exits 0 and injects advisory context.
6
+ //
7
+ // Environment:
8
+ // FILE_GUARD_THRESHOLD — max lines before warning (default: 200)
9
+ // FILE_GUARD_EXCLUDE — comma-separated globs to skip (e.g. "*.generated.swift,*.pb.go")
10
+
11
+ "use strict";
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+
16
+ const THRESHOLD = parseInt(process.env.FILE_GUARD_THRESHOLD, 10) || 200;
17
+ const EXCLUDE = (process.env.FILE_GUARD_EXCLUDE || "")
18
+ .split(",")
19
+ .map((g) => g.trim())
20
+ .filter(Boolean);
21
+
22
+ function matchesExclude(filePath) {
23
+ const name = path.basename(filePath);
24
+ return EXCLUDE.some((pattern) => {
25
+ // Simple glob: *.ext or exact match
26
+ if (pattern.startsWith("*")) {
27
+ return name.endsWith(pattern.slice(1));
28
+ }
29
+ return name === pattern;
30
+ });
31
+ }
32
+
33
+ function isBinary(buf) {
34
+ // Check first 512 bytes for null bytes (common binary indicator)
35
+ const check = buf.slice(0, 512);
36
+ for (let i = 0; i < check.length; i++) {
37
+ if (check[i] === 0) return true;
38
+ }
39
+ return false;
40
+ }
41
+
42
+ function main() {
43
+ let input;
44
+ try {
45
+ input = fs.readFileSync(0, "utf-8").trim();
46
+ } catch {
47
+ process.exit(0);
48
+ }
49
+
50
+ if (!input) process.exit(0);
51
+
52
+ let payload;
53
+ try {
54
+ payload = JSON.parse(input);
55
+ } catch {
56
+ process.exit(0);
57
+ }
58
+
59
+ const filePath = payload.tool_input?.file_path;
60
+ if (!filePath) process.exit(0);
61
+
62
+ // Skip excluded patterns
63
+ if (matchesExclude(filePath)) process.exit(0);
64
+
65
+ // Skip if file doesn't exist (deleted?) or is a symlink to outside project
66
+ try {
67
+ const stat = fs.lstatSync(filePath);
68
+ if (stat.isSymbolicLink() || !stat.isFile()) process.exit(0);
69
+ } catch {
70
+ process.exit(0);
71
+ }
72
+
73
+ // Cap read at 1MB to avoid OOM on huge files
74
+ const MAX_BYTES = 1024 * 1024;
75
+ let content;
76
+ try {
77
+ const stat = fs.statSync(filePath);
78
+ if (stat.size > MAX_BYTES) {
79
+ // >1MB = definitely over threshold, warn without exact count
80
+ const rel = path.relative(process.cwd(), filePath);
81
+ process.stdout.write(JSON.stringify({
82
+ continue: true,
83
+ hookSpecificOutput: {
84
+ hookEventName: "PostToolUse",
85
+ additionalContext: `Warning: ${rel} is ${Math.round(stat.size / 1024)}KB. Consider splitting into smaller modules.`,
86
+ },
87
+ }));
88
+ process.exit(0);
89
+ }
90
+ const buf = fs.readFileSync(filePath);
91
+ if (isBinary(buf)) process.exit(0);
92
+ content = buf.toString("utf-8");
93
+ } catch {
94
+ process.exit(0);
95
+ }
96
+
97
+ const lineCount = content.split("\n").length;
98
+ if (lineCount <= THRESHOLD) process.exit(0);
99
+
100
+ // Inject non-blocking warning
101
+ const rel = path.relative(process.cwd(), filePath);
102
+ const warning = `Warning: ${rel} has ${lineCount} lines (threshold: ${THRESHOLD}). Consider splitting into smaller, focused modules.`;
103
+
104
+ process.stdout.write(
105
+ JSON.stringify({
106
+ continue: true,
107
+ hookSpecificOutput: {
108
+ hookEventName: "PostToolUse",
109
+ additionalContext: warning,
110
+ },
111
+ })
112
+ );
113
+ }
114
+
115
+ try {
116
+ main();
117
+ } catch {
118
+ // Never block on error
119
+ }
120
+ process.exit(0);
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ // glob-guard.js — PreToolUse hook for Claude Code
3
+ //
4
+ // Blocks overly broad glob patterns (e.g. **/*.ts at project root) that would
5
+ // return thousands of files and fill the context window. Suggests scoped
6
+ // alternatives instead.
7
+ //
8
+ // Blocking: Yes — exits 2 when a broad pattern at a high-level path is detected.
9
+ // Event: PreToolUse on Glob
10
+
11
+ "use strict";
12
+
13
+ const fs = require("fs");
14
+
15
+ // Patterns that match too many files when run at project root
16
+ const BROAD_PATTERNS = [
17
+ /^\*\*$/, // **
18
+ /^\*$/, // *
19
+ /^\*\*\/\*$/, // **/*
20
+ /^\*\.\w+$/, // *.ts, *.js
21
+ /^\*\.\{[^}]+\}$/, // *.{ts,js}
22
+ /^\*\*\/\*\.\w+$/, // **/*.ts
23
+ /^\*\*\/\*\.\{[^}]+\}$/, // **/*.{ts,tsx}
24
+ /^\*\*\/\.\*$/, // **/.* (all dotfiles)
25
+ ];
26
+
27
+ // Directories that indicate an intentional, scoped search
28
+ const SCOPED_DIRS = [
29
+ "src", "lib", "app", "apps", "packages", "components", "pages",
30
+ "api", "server", "client", "web", "mobile", "shared", "common",
31
+ "utils", "helpers", "services", "hooks", "store", "routes",
32
+ "models", "controllers", "views", "tests", "__tests__", "spec",
33
+ "Sources", "Tests", "cmd", "pkg", "internal",
34
+ ];
35
+
36
+ function isBroadPattern(pattern) {
37
+ if (!pattern) return false;
38
+ return BROAD_PATTERNS.some((re) => re.test(pattern.trim()));
39
+ }
40
+
41
+ function startsWithScopedDir(pattern) {
42
+ if (!pattern) return false;
43
+ // Only allow dirs explicitly in SCOPED_DIRS — not arbitrary dirs like node_modules/
44
+ return SCOPED_DIRS.some(
45
+ (d) => pattern.startsWith(d + "/") || pattern.startsWith("./" + d + "/")
46
+ );
47
+ }
48
+
49
+ function isRootLevel(basePath) {
50
+ if (!basePath) return true;
51
+ const norm = basePath.replace(/\\/g, "/").replace(/\/+$/, "");
52
+ if (!norm || norm === ".") return true;
53
+ const segments = norm.split("/").filter((s) => s && s !== ".");
54
+ if (segments.length === 0) return true;
55
+ if (segments.length === 1 && !SCOPED_DIRS.includes(segments[0])) return true;
56
+ return false;
57
+ }
58
+
59
+ function suggest(pattern) {
60
+ let ext = "";
61
+ const m = pattern.match(/\*\.(\{[^}]+\}|\w+)$/);
62
+ if (m) ext = m[1];
63
+ const dirs = ["src", "lib", "app", "tests"];
64
+ return ext
65
+ ? dirs.map((d) => `${d}/**/*.${ext}`).slice(0, 3)
66
+ : dirs.map((d) => `${d}/**/*`).slice(0, 3);
67
+ }
68
+
69
+ function main() {
70
+ let input;
71
+ try { input = fs.readFileSync(0, "utf-8").trim(); } catch { process.exit(0); }
72
+ if (!input) process.exit(0);
73
+
74
+ let payload;
75
+ try { payload = JSON.parse(input); } catch { process.exit(0); }
76
+
77
+ const pattern = payload.tool_input?.pattern;
78
+ const basePath = payload.tool_input?.path;
79
+
80
+ if (!pattern) process.exit(0);
81
+ if (startsWithScopedDir(pattern)) process.exit(0);
82
+ if (!isBroadPattern(pattern)) process.exit(0);
83
+ if (!isRootLevel(basePath)) process.exit(0);
84
+
85
+ const alt = suggest(pattern);
86
+ process.stderr.write(
87
+ [
88
+ `Blocked: '${pattern}' is too broad for ${basePath || "project root"} — would fill the context window.`,
89
+ "Use a scoped pattern instead:",
90
+ ...alt.map((s) => ` - ${s}`),
91
+ ].join("\n") + "\n"
92
+ );
93
+ process.exit(2);
94
+ }
95
+
96
+ try { main(); } catch { process.exit(0); }
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # path-guard.sh — PreToolUse hook for Claude Code
3
+ #
4
+ # Blocks Bash commands that target directories known to be large and wasteful
5
+ # to explore (node_modules, build artifacts, .git internals, etc.).
6
+ #
7
+ # Exit codes:
8
+ # 0 — command allowed
9
+ # 2 — command blocked (policy)
10
+ #
11
+ # Environment:
12
+ # PATH_GUARD_EXTRA — additional pipe-separated patterns to block
13
+ # e.g. "\.terraform|\.vagrant"
14
+
15
+ set -euo pipefail
16
+
17
+ # ─── Read hook payload from stdin ───────────────────────────────────
18
+
19
+ INPUT=$(cat)
20
+ [[ -z "$INPUT" ]] && exit 0
21
+
22
+ # Check Node.js availability
23
+ if ! command -v node &>/dev/null; then
24
+ echo "WARNING: path-guard disabled — Node.js not found." >&2
25
+ exit 0
26
+ fi
27
+
28
+ # Parse JSON with inline Node.js (avoids jq dependency)
29
+ COMMAND=$(printf '%s' "$INPUT" | node -e "
30
+ try {
31
+ const d = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
32
+ const cmd = d.tool_input?.command;
33
+ if (typeof cmd === 'string') process.stdout.write(cmd);
34
+ else process.exit(0);
35
+ } catch { process.exit(0); }
36
+ " 2>/dev/null) || exit 0
37
+
38
+ [[ -z "$COMMAND" ]] && exit 0
39
+
40
+ # ─── Blocked directory patterns ─────────────────────────────────────
41
+
42
+ BLOCKED="node_modules"
43
+ BLOCKED+="|__pycache__"
44
+ BLOCKED+="|\.git/objects"
45
+ BLOCKED+="|\.git/refs"
46
+ BLOCKED+="|dist/"
47
+ BLOCKED+="|build/"
48
+ BLOCKED+="|\.next/"
49
+ BLOCKED+="|vendor/"
50
+ BLOCKED+="|Pods/"
51
+ BLOCKED+="|\.build/"
52
+ BLOCKED+="|DerivedData"
53
+ BLOCKED+="|\.gradle/"
54
+ BLOCKED+="|target/debug"
55
+ BLOCKED+="|target/release"
56
+ BLOCKED+="|\.nuget"
57
+ BLOCKED+="|\.cache"
58
+
59
+ # Append project-specific patterns from env
60
+ if [[ -n "${PATH_GUARD_EXTRA:-}" ]]; then
61
+ BLOCKED+="|$PATH_GUARD_EXTRA"
62
+ fi
63
+
64
+ # ─── Match and block ────────────────────────────────────────────────
65
+
66
+ if printf '%s\n' "$COMMAND" | grep -qE "$BLOCKED"; then
67
+ # Extract which pattern matched for a useful error message
68
+ MATCHED=$(printf '%s\n' "$COMMAND" | grep -oE "$BLOCKED" | head -1)
69
+ echo "Blocked: command references '$MATCHED' — this directory is typically large and exploring it wastes tokens. Use Glob or Grep tools instead." >&2
70
+ exit 2
71
+ fi
72
+
73
+ exit 0
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # self-review.sh — Stop hook for Claude Code
3
+ #
4
+ # Injects a self-review checklist when Claude is about to finish.
5
+ # Non-blocking: always exits 0, just adds context for Claude to consider.
6
+ #
7
+ # Environment:
8
+ # SELF_REVIEW_ENABLED — set to "false" to disable (default: true)
9
+
10
+ set -euo pipefail
11
+
12
+ # Check if disabled
13
+ if [[ "${SELF_REVIEW_ENABLED:-true}" == "false" ]]; then
14
+ exit 0
15
+ fi
16
+
17
+ # Read stdin (Stop event payload — may be empty or minimal)
18
+ cat > /dev/null 2>&1 || true
19
+
20
+ # Inject self-review checklist as context
21
+ cat <<'REVIEW_JSON'
22
+ {
23
+ "continue": true,
24
+ "hookSpecificOutput": {
25
+ "hookEventName": "Stop",
26
+ "additionalContext": "Self-review before finishing:\n1. Did you leave any TODO/FIXME comments that should be resolved now?\n2. Did you create mock or fake implementations just to pass tests?\n3. Did you replace real code with placeholder comments like '// ... existing code'?\n4. Do all changed files compile and typecheck cleanly?\n5. Did you run the full test suite, not just the new tests?\n6. Are there any files you modified but forgot to include in the summary?"
27
+ }
28
+ }
29
+ REVIEW_JSON