xtrm-tools 2.0.2 → 2.1.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 (30) hide show
  1. package/README.md +0 -2
  2. package/cli/dist/index.cjs +19 -19
  3. package/cli/dist/index.cjs.map +1 -1
  4. package/cli/package.json +1 -1
  5. package/config/hooks.json +33 -0
  6. package/hooks/README.md +108 -5
  7. package/hooks/beads-close-memory-prompt.mjs +49 -0
  8. package/hooks/beads-commit-gate.mjs +64 -0
  9. package/hooks/beads-edit-gate.mjs +72 -0
  10. package/hooks/beads-gate-utils.mjs +121 -0
  11. package/hooks/beads-stop-gate.mjs +61 -0
  12. package/hooks/main-guard.mjs +149 -0
  13. package/package.json +3 -2
  14. package/project-skills/py-quality-gate/.claude/settings.json +1 -1
  15. package/project-skills/service-skills-set/.claude/skills/creating-service-skills/SKILL.md +12 -12
  16. package/project-skills/service-skills-set/.claude/skills/creating-service-skills/references/script_quality_standards.md +13 -0
  17. package/project-skills/service-skills-set/.claude/skills/creating-service-skills/references/service_skill_system_guide.md +14 -0
  18. package/project-skills/service-skills-set/.claude/skills/creating-service-skills/scripts/__pycache__/bootstrap.cpython-314.pyc +0 -0
  19. package/project-skills/service-skills-set/.claude/skills/scoping-service-skills/SKILL.md +6 -6
  20. package/project-skills/service-skills-set/.claude/skills/updating-service-skills/SKILL.md +1 -1
  21. package/project-skills/service-skills-set/.claude/skills/using-service-skills/SKILL.md +2 -2
  22. package/project-skills/service-skills-set/.claude/skills/using-service-skills/scripts/__pycache__/skill_activator.cpython-314.pyc +0 -0
  23. package/project-skills/service-skills-set/.claude/skills/using-service-skills/scripts/__pycache__/test_skill_activator.cpython-314-pytest-9.0.2.pyc +0 -0
  24. package/project-skills/service-skills-set/.claude/skills/using-service-skills/scripts/skill_activator.py +2 -2
  25. package/project-skills/service-skills-set/.claude/skills/using-service-skills/scripts/test_skill_activator.py +58 -0
  26. package/project-skills/ts-quality-gate/.claude/settings.json +1 -1
  27. package/project-skills/main-guard/.claude/hooks/main-guard.cjs +0 -188
  28. package/project-skills/main-guard/.claude/settings.json +0 -16
  29. package/project-skills/main-guard/.claude/skills/using-main-guard/SKILL.md +0 -135
  30. package/project-skills/main-guard/README.md +0 -163
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code PreToolUse hook — block writes and direct master pushes
3
+ // Exit 0: allow | Exit 2: block (message shown to user)
4
+ //
5
+ // Installed by: xtrm install
6
+
7
+ import { execSync } from 'node:child_process';
8
+ import { readFileSync } from 'node:fs';
9
+
10
+ let branch = '';
11
+ try {
12
+ branch = execSync('git branch --show-current', {
13
+ encoding: 'utf8',
14
+ stdio: ['pipe', 'pipe', 'pipe'],
15
+ }).trim();
16
+ } catch {}
17
+
18
+ // Determine protected branches — env var override for tests and custom setups
19
+ const protectedBranches = process.env.MAIN_GUARD_PROTECTED_BRANCHES
20
+ ? process.env.MAIN_GUARD_PROTECTED_BRANCHES.split(',').map(b => b.trim()).filter(Boolean)
21
+ : ['main', 'master'];
22
+
23
+ // Not in a git repo or not on a protected branch — allow
24
+ if (!branch || !protectedBranches.includes(branch)) {
25
+ process.exit(0);
26
+ }
27
+
28
+ let input;
29
+ try {
30
+ input = JSON.parse(readFileSync(0, 'utf8'));
31
+ } catch {
32
+ process.exit(0);
33
+ }
34
+
35
+ const tool = input.tool_name ?? '';
36
+ const hookEventName = input.hook_event_name ?? 'PreToolUse';
37
+
38
+ function deny(reason) {
39
+ process.stdout.write(JSON.stringify({
40
+ systemMessage: reason,
41
+ hookSpecificOutput: {
42
+ hookEventName,
43
+ permissionDecision: 'deny',
44
+ permissionDecisionReason: reason,
45
+ },
46
+ }));
47
+ process.stdout.write('\n');
48
+ process.exit(0);
49
+ }
50
+
51
+ const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
52
+
53
+ if (WRITE_TOOLS.has(tool)) {
54
+ deny(
55
+ `⛔ You are on '${branch}' — never edit files directly on master.\n\n` +
56
+ 'Full workflow:\n' +
57
+ ' 1. git checkout -b feature/<name> ← start here\n' +
58
+ ' 2. bd create + bd update in_progress track your work\n' +
59
+ ' 3. Edit files / write code\n' +
60
+ ' 4. bd close <id> && git add && git commit\n' +
61
+ ' 5. git push -u origin feature/<name>\n' +
62
+ ' 6. gh pr create --fill && gh pr merge --squash\n' +
63
+ ' 7. git checkout master && git reset --hard origin/master\n'
64
+ );
65
+ }
66
+
67
+ const WORKFLOW =
68
+ 'Full workflow:\n' +
69
+ ' 1. git checkout -b feature/<name> \u2190 start here\n' +
70
+ ' 2. bd create + bd update in_progress track your work\n' +
71
+ ' 3. Edit files / write code\n' +
72
+ ' 4. bd close <id> && git add && git commit\n' +
73
+ ' 5. git push -u origin feature/<name>\n' +
74
+ ' 6. gh pr create --fill && gh pr merge --squash\n' +
75
+ ' 7. git checkout master && git reset --hard origin/master\n';
76
+
77
+ if (tool === 'Bash') {
78
+ const cmd = (input.tool_input?.command ?? '').trim().replace(/\s+/g, ' ');
79
+
80
+ // Emergency override — escape hatch for power users
81
+ if (process.env.MAIN_GUARD_ALLOW_BASH === '1') {
82
+ process.exit(0);
83
+ }
84
+
85
+ // Safe allowlist — non-mutating commands + explicit branch-exit paths.
86
+ // Important: do not allow generic checkout/switch forms, which include
87
+ // mutating variants such as `git checkout -- <path>`.
88
+ const SAFE_BASH_PATTERNS = [
89
+ /^git\s+(status|log|diff|branch|show|describe|fetch|remote|config)\b/,
90
+ /^git\s+pull\b/,
91
+ /^git\s+stash\b/,
92
+ /^git\s+worktree\b/,
93
+ /^git\s+checkout\s+-b\s+\S+/,
94
+ /^git\s+switch\s+-c\s+\S+/,
95
+ /^gh\s+/,
96
+ /^bd\s+/,
97
+ ];
98
+
99
+ if (SAFE_BASH_PATTERNS.some(p => p.test(cmd))) {
100
+ process.exit(0);
101
+ }
102
+
103
+ // Specific messages for common blocked operations
104
+ if (/^git\s+commit\b/.test(cmd)) {
105
+ deny(
106
+ `\u26D4 Don't commit directly to '${branch}' \u2014 use a feature branch.\n\n` +
107
+ WORKFLOW
108
+ );
109
+ }
110
+
111
+ if (/^git\s+push\b/.test(cmd)) {
112
+ const tokens = cmd.split(' ');
113
+ const lastToken = tokens[tokens.length - 1];
114
+ const explicitProtected = protectedBranches.some(b => lastToken === b || lastToken.endsWith(`:${b}`));
115
+ const impliedProtected = tokens.length <= 3 && protectedBranches.includes(branch);
116
+ if (explicitProtected || impliedProtected) {
117
+ deny(
118
+ `\u26D4 Don't push directly to '${branch}' \u2014 use the PR workflow.\n\n` +
119
+ 'Next steps:\n' +
120
+ ' 5. git push -u origin <feature-branch> \u2190 push your branch\n' +
121
+ ' 6. gh pr create --fill create PR\n' +
122
+ ' gh pr merge --squash merge it\n' +
123
+ ' 7. git checkout master sync master\n' +
124
+ ' git reset --hard origin/master\n\n' +
125
+ "If you're not on a feature branch yet:\n" +
126
+ ' git checkout -b feature/<name> (then re-commit and push)\n'
127
+ );
128
+ }
129
+ // Pushing to a feature branch — allow
130
+ process.exit(0);
131
+ }
132
+
133
+ // Default deny — block everything else on protected branches
134
+ deny(
135
+ `\u26D4 Bash is restricted on '${branch}' \u2014 use a feature branch for file writes and script execution.\n\n` +
136
+ 'Allowed on protected branches:\n' +
137
+ ' git status / log / diff / branch / fetch / pull / stash\n' +
138
+ ' git checkout -b <name> (create feature branch \u2014 the exit path)\n' +
139
+ ' git switch -c <name> (same)\n' +
140
+ ' git worktree / config\n' +
141
+ ' gh <any> (GitHub CLI)\n' +
142
+ ' bd <any> (beads issue tracking)\n\n' +
143
+ 'To run arbitrary commands:\n' +
144
+ ' 1. git checkout -b feature/<name> \u2190 move to a feature branch, or\n' +
145
+ ' 2. MAIN_GUARD_ALLOW_BASH=1 <command> (escape hatch, use sparingly)\n'
146
+ );
147
+ }
148
+
149
+ process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,6 +43,7 @@
43
43
  "dotenv": "^17.3.1",
44
44
  "fs-extra": "^11.2.0",
45
45
  "kleur": "^4.1.5",
46
- "prompts": "^2.4.2"
46
+ "prompts": "^2.4.2",
47
+ "xtrm-tools": "^2.0.2"
47
48
  }
48
49
  }
@@ -2,7 +2,7 @@
2
2
  "hooks": {
3
3
  "PostToolUse": [
4
4
  {
5
- "matcher": "Write|Edit",
5
+ "matcher": "Write|Edit|MultiEdit",
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
@@ -20,7 +20,7 @@ immediately useful to any agent working on the service.
20
20
 
21
21
  ## Mandatory Two-Phase Workflow
22
22
 
23
- **Both phases are required. Never skip Phase 2.**
23
+ **Use both phases every time.** Phase 1 gives structure; Phase 2 grounds the skill in real service behavior.
24
24
 
25
25
  ---
26
26
 
@@ -65,7 +65,7 @@ python3 "$CLAUDE_PROJECT_DIR/.claude/skills/creating-service-skills/scripts/deep
65
65
 
66
66
  ---
67
67
 
68
- ### Phase 2: Agentic Deep Dive (Non-Negotiable)
68
+ ### Phase 2: Agentic Deep Dive
69
69
 
70
70
  After the skeleton exists, answer every research question by reading the actual
71
71
  source code. Use **Serena LSP tools exclusively** — never read entire files.
@@ -118,10 +118,10 @@ source code. Use **Serena LSP tools exclusively** — never read entire files.
118
118
 
119
119
  ---
120
120
 
121
- ### Phase 2 Script Writing (Required — No Stubs)
121
+ ### Phase 2 Script Writing (Complete Implementation)
122
122
 
123
- After research is complete, replace ALL `[PENDING RESEARCH]` stubs in `scripts/`.
124
- Each script must be fully implemented no TODOs, no placeholder SQL.
123
+ After research is complete, replace all `[PENDING RESEARCH]` stubs in `scripts/`.
124
+ Scripts should be ready to run end-to-end, without TODO markers or placeholder SQL.
125
125
 
126
126
  #### Mandatory DB Connection Pattern (all scripts that touch the DB)
127
127
 
@@ -207,10 +207,10 @@ Required features:
207
207
 
208
208
  See [references/script_quality_standards.md](references/script_quality_standards.md) for complete templates.
209
209
 
210
- #### `scripts/Makefile` (required — always present)
210
+ #### `scripts/Makefile` (required)
211
211
 
212
- The scaffolder creates a stub `Makefile` in Phase 1. In Phase 2 you **must verify it is
213
- correct and complete** it is the primary entry point users and agents use to run diagnostics.
212
+ The scaffolder creates a stub `Makefile` in Phase 1. In Phase 2, verify it is
213
+ correct and complete because it is the primary entry point for diagnostics.
214
214
 
215
215
  **Standard template** (copy verbatim, replace `<service-id>` comment only):
216
216
 
@@ -261,11 +261,11 @@ db:
261
261
 
262
262
  **Rules for the delegated Phase 2 agent:**
263
263
 
264
- 1. **Do not remove or rename the standard targets** they form the stable interface.
264
+ 1. **Keep standard targets stable** avoid removing or renaming them because downstream workflows depend on them.
265
265
  2. **Add service-specific targets** below the standard block if the service needs them (e.g. `make auth`, `make schema`, `make backfill`).
266
- 3. **The `_VENV` auto-detect path (`../../../../venv/bin/python3`) is fixed** — it resolves from `scripts/` → service dir → `skills/` → `.claude/` → project root → `venv/`. Do not change the depth.
267
- 4. **Recipe lines use a real tab character**, not spaces. Makefile syntax requires this.
268
- 5. **Verify with `make help`** after writing the Python path shown must resolve to the project venv, not system python.
266
+ 3. **Keep the `_VENV` auto-detect path (`../../../../venv/bin/python3`) unchanged** — it resolves from `scripts/` → service dir → `skills/` → `.claude/` → project root → `venv/`.
267
+ 4. **Use real tab characters in recipe lines** so Makefile parsing works consistently.
268
+ 5. **Run `make help` after updates** and confirm the Python path resolves to the project venv.
269
269
 
270
270
  ---
271
271
 
@@ -6,6 +6,19 @@
6
6
 
7
7
  ---
8
8
 
9
+ ## Table of Contents
10
+
11
+ - [Mandatory DB Connection Pattern](#mandatory-db-connection-pattern)
12
+ - [Schema Verification Before Writing Any SQL](#schema-verification-before-writing-any-sql)
13
+ - [Makefile Standard](#makefile-standard)
14
+ - [Design Principles](#design-principles)
15
+ - [health_probe.py Standards](#health_probepy-standards)
16
+ - [log_hunter.py Standards](#log_hunterpy-standards)
17
+ - [Specialist Script Standards](#specialist-script-standards)
18
+ - [Common Pitfalls](#common-pitfalls)
19
+
20
+ ---
21
+
9
22
  ## Mandatory DB Connection Pattern
10
23
 
11
24
  **Every script that touches the database MUST use this exact pattern.** No exceptions.
@@ -5,6 +5,20 @@
5
5
 
6
6
  ---
7
7
 
8
+ ## Table of Contents
9
+
10
+ - [1. System Overview](#1-system-overview)
11
+ - [2. System Architecture](#2-system-architecture)
12
+ - [3. Mandatory Two-Phase Workflow](#3-mandatory-two-phase-workflow)
13
+ - [4. Service Type Classification](#4-service-type-classification)
14
+ - [5. Directory Structure](#5-directory-structure)
15
+ - [6. Skill Lifecycle](#6-skill-lifecycle)
16
+ - [7. Quality Gates](#7-quality-gates)
17
+ - [8. Best Practices](#8-best-practices)
18
+ - [9. Anti-Patterns](#9-anti-patterns)
19
+
20
+ ---
21
+
8
22
  ## 1. System Overview
9
23
 
10
24
  The **Service Skill System** transforms an AI agent from a generic assistant into a service-aware operator. Each Docker service in your project gets a dedicated **skill package**: a structured combination of operational documentation and executable diagnostic scripts.
@@ -24,7 +24,7 @@ in the conversation.
24
24
 
25
25
  ### Step 1 — Read the Registry
26
26
 
27
- Run this **immediately**, before any reasoning:
27
+ Run this at the start, before deep task reasoning:
28
28
 
29
29
  ```bash
30
30
  python3 "$CLAUDE_PROJECT_DIR/.claude/skills/scoping-service-skills/scripts/scope.py"
@@ -73,7 +73,7 @@ Using the registry output, reason about which service(s) the task involves. Matc
73
73
 
74
74
  ### Step 4 — Output XML Scope Block
75
75
 
76
- Emit this block **before doing anything else**:
76
+ Emit this block before moving into implementation:
77
77
 
78
78
  ```xml
79
79
  <scope>
@@ -120,15 +120,15 @@ For each `<service>` with `<load>now</load>`, immediately read the skill file:
120
120
  Read: .claude/skills/<service-id>/SKILL.md
121
121
  ```
122
122
 
123
- **Do not proceed with the task until all matched skills are loaded.**
123
+ Load all matched skills before proceeding with the task.
124
124
  Adopt the expert persona, constraints, and diagnostic approach from each loaded skill.
125
125
 
126
126
  ---
127
127
 
128
128
  ### Step 6 — Execute
129
129
 
130
- Follow the workflow phases in order. For `investigation` tasks, **never skip the
131
- regression-test phase**a fix without a test is incomplete.
130
+ Follow the workflow phases in order. For `investigation` tasks, include the
131
+ regression-test phase — it keeps fixes durable.
132
132
 
133
133
  ---
134
134
 
@@ -187,7 +187,7 @@ load-skill → answer
187
187
 
188
188
  ## Regression Test Binding
189
189
 
190
- When `intent = investigation` and a fix has been applied, always write a regression
190
+ When `intent = investigation` and a fix has been applied, write a regression
191
191
  test. Use this decision tree:
192
192
 
193
193
  ```
@@ -109,7 +109,7 @@ Write to:
109
109
  - ✅ `.claude/skills/*/SKILL.md` — skill documentation updates
110
110
  - ✅ `.claude/skills/service-registry.json` — territory and sync timestamp updates
111
111
 
112
- Do not:
112
+ Avoid:
113
113
  - ❌ Modify source code (read-only access to service territories)
114
114
  - ❌ Delete skills or registry entries
115
115
 
@@ -78,8 +78,8 @@ If no registered expert covers the user's need:
78
78
 
79
79
  ## Session Start Hook
80
80
 
81
- The catalog injection is **not** handled by skill frontmatter hooks (SessionStart
82
- is not supported in skill-level hooks). It is configured in `.claude/settings.json`:
81
+ The catalog injection is not handled by skill frontmatter hooks. Configure it in
82
+ `.claude/settings.json` using `SessionStart`:
83
83
 
84
84
  ```json
85
85
  {
@@ -19,7 +19,7 @@ from pathlib import Path
19
19
  BOOTSTRAP_DIR = Path(__file__).parent.parent.parent / "creating-service-skills" / "scripts"
20
20
  sys.path.insert(0, str(BOOTSTRAP_DIR))
21
21
 
22
- from bootstrap import RootResolutionError, get_project_root, load_registry # noqa: E402
22
+ from bootstrap import RootResolutionError, get_project_root, load_registry # noqa: E402 # type: ignore[import-not-found]
23
23
 
24
24
 
25
25
  def match_territory(file_path: str, territory: list[str], project_root: Path) -> bool:
@@ -109,7 +109,7 @@ def main() -> None:
109
109
 
110
110
  try:
111
111
  project_root = Path(get_project_root())
112
- services = load_registry()
112
+ services = load_registry().get("services", {})
113
113
  except (RootResolutionError, Exception):
114
114
  sys.exit(0)
115
115
 
@@ -0,0 +1,58 @@
1
+ """Tests for skill_activator.py — load_registry integration."""
2
+ import io
3
+ import json
4
+ import sys
5
+ import unittest
6
+ from pathlib import Path
7
+ from unittest.mock import patch
8
+
9
+ scripts_dir = Path(__file__).parent
10
+ sys.path.insert(0, str(scripts_dir))
11
+ sys.path.insert(0, str(scripts_dir.parent.parent / "creating-service-skills" / "scripts"))
12
+
13
+ import skill_activator
14
+
15
+
16
+ REGISTRY_WITH_VERSION = {
17
+ "version": "1.0",
18
+ "services": {
19
+ "my-service": {
20
+ "territory": ["src/my-service/**"],
21
+ "name": "My Service",
22
+ "skill_path": ".claude/skills/my-service/SKILL.md",
23
+ }
24
+ },
25
+ }
26
+
27
+ HOOK_INPUT = json.dumps({
28
+ "tool_name": "Write",
29
+ "tool_input": {"file_path": "src/my-service/foo.py"},
30
+ "hook_event_name": "PreToolUse",
31
+ "session_id": "test",
32
+ "cwd": "/fake/project",
33
+ })
34
+
35
+
36
+ class TestMainWithVersionedRegistry(unittest.TestCase):
37
+ def test_main_does_not_crash_when_registry_has_version_key(self):
38
+ """main() must not crash with AttributeError when load_registry returns
39
+ {"version": ..., "services": {...}} — the full registry dict.
40
+ It should output valid JSON context for the matched service.
41
+ """
42
+ with patch("skill_activator.load_registry", return_value=REGISTRY_WITH_VERSION), \
43
+ patch("skill_activator.get_project_root", return_value="/fake/project"), \
44
+ patch("sys.stdin", io.StringIO(HOOK_INPUT)), \
45
+ patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
46
+ try:
47
+ skill_activator.main()
48
+ except SystemExit:
49
+ pass
50
+ output = mock_stdout.getvalue()
51
+
52
+ self.assertTrue(output, "Expected JSON output but got nothing")
53
+ result = json.loads(output)
54
+ self.assertIn("hookSpecificOutput", result)
55
+
56
+
57
+ if __name__ == "__main__":
58
+ unittest.main()
@@ -2,7 +2,7 @@
2
2
  "hooks": {
3
3
  "PostToolUse": [
4
4
  {
5
- "matcher": "Write|Edit",
5
+ "matcher": "Write|Edit|MultiEdit",
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
@@ -1,188 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Main Guard - Git branch protection hook for Claude Code.
4
- * Blocks direct edits to main/master branches and enforces feature branch workflow.
5
- *
6
- * Exit codes:
7
- * 0 - Allowed (not on protected branch, or operation allowed)
8
- * 1 - Fatal error
9
- * 2 - Blocked (attempted edit on protected branch)
10
- */
11
-
12
- const { execSync } = require('child_process');
13
- const path = require('path');
14
- const fs = require('fs');
15
-
16
- // Colors
17
- const colors = {
18
- red: '\x1b[0;31m',
19
- green: '\x1b[0;32m',
20
- yellow: '\x1b[0;33m',
21
- blue: '\x1b[0;34m',
22
- cyan: '\x1b[0;36m',
23
- reset: '\x1b[0m',
24
- };
25
-
26
- function log(msg, color = '') {
27
- console.error(`${color}${msg}${colors.reset}`);
28
- }
29
-
30
- function logInfo(msg) { log(`[INFO] ${msg}`, colors.blue); }
31
- function logError(msg) { log(`[ERROR] ${msg}`, colors.red); }
32
- function logSuccess(msg) { log(`[OK] ${msg}`, colors.green); }
33
- function logWarning(msg) { log(`[WARN] ${msg}`, colors.yellow); }
34
-
35
- /**
36
- * Get current git branch name
37
- */
38
- function getCurrentBranch() {
39
- try {
40
- return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
41
- } catch (e) {
42
- logWarning('Could not determine git branch');
43
- return null;
44
- }
45
- }
46
-
47
- /**
48
- * Check if branch is protected
49
- */
50
- function isProtectedBranch(branch) {
51
- if (!branch) return false;
52
-
53
- // Get protected branches from config or use defaults
54
- const protectedPatterns = process.env.MAIN_GUARD_PROTECTED_BRANCHES
55
- ? process.env.MAIN_GUARD_PROTECTED_BRANCHES.split(',')
56
- : ['main', 'master', 'develop'];
57
-
58
- return protectedPatterns.some(pattern => {
59
- const regex = new RegExp(`^${pattern.replace('*', '.*')}$`);
60
- return regex.test(branch);
61
- });
62
- }
63
-
64
- /**
65
- * Get feature branch name suggestion based on task
66
- */
67
- function suggestBranchName(input) {
68
- const toolName = input.tool_name || '';
69
- const prompt = input.user_prompt || '';
70
-
71
- // Extract potential task identifier
72
- const match = prompt.match(/(?:issue|ticket|task|bug|feat)[#:-]?\s*(\d+|[a-z-]+)/i) ||
73
- prompt.match(/^([a-z-]+)/i);
74
-
75
- if (match) {
76
- const prefix = toolName.includes('Edit') || toolName.includes('Write') ? 'fix' : 'feat';
77
- return `${prefix}/${match[1].toLowerCase().replace(/\s+/g, '-')}`;
78
- }
79
-
80
- return 'feature/task';
81
- }
82
-
83
- /**
84
- * Parse JSON input from stdin
85
- */
86
- function parseInput() {
87
- let inputData = '';
88
-
89
- try {
90
- inputData = fs.readFileSync(0, 'utf8');
91
- } catch (e) {
92
- return null;
93
- }
94
-
95
- if (!inputData.trim()) {
96
- return null;
97
- }
98
-
99
- try {
100
- return JSON.parse(inputData);
101
- } catch (e) {
102
- logWarning('Invalid JSON input');
103
- return null;
104
- }
105
- }
106
-
107
- /**
108
- * Print blocked message with instructions
109
- */
110
- function printBlocked(branch, suggestedBranch) {
111
- log('', colors.red);
112
- log('═══════════════════════════════════════════════════════════', colors.red);
113
- log(' 🛑 BLOCKED: Direct edits to protected branches', colors.red);
114
- log('═══════════════════════════════════════════════════════════', colors.red);
115
- log('', colors.reset);
116
- log(` Current branch: ${colors.yellow}${branch}${colors.reset}`, colors.reset);
117
- log('', colors.reset);
118
- log(' You cannot edit files directly on a protected branch.', colors.reset);
119
- log(' This prevents accidental commits and enforces code review.', colors.reset);
120
- log('', colors.reset);
121
- log(' 📋 To proceed:', colors.cyan);
122
- log(` 1. Create a feature branch: ${colors.green}git checkout -b ${suggestedBranch}${colors.reset}`, colors.reset);
123
- log(` 2. Make your changes on that branch`, colors.reset);
124
- log(` 3. Push and create a pull request`, colors.reset);
125
- log('', colors.reset);
126
- log('═══════════════════════════════════════════════════════════', colors.red);
127
- log('', colors.reset);
128
- }
129
-
130
- /**
131
- * Print success message
132
- */
133
- function printSuccess(branch) {
134
- log('', colors.green);
135
- log(`✅ Git workflow check passed`, colors.green);
136
- log(` Branch: ${branch}`, colors.green);
137
- log('', colors.reset);
138
- }
139
-
140
- /**
141
- * Main entry point
142
- */
143
- function main() {
144
- log('');
145
- log('🔒 Main Guard - Branch Protection Check', colors.blue);
146
- log('─────────────────────────────────────────', colors.blue);
147
-
148
- // Parse input
149
- const input = parseInput();
150
-
151
- // Get current branch
152
- const branch = getCurrentBranch();
153
-
154
- if (!branch) {
155
- logWarning('Not in a git repository - skipping branch protection');
156
- log('', colors.yellow);
157
- log('👉 Not a git repository - continuing without branch protection', colors.yellow);
158
- log('', colors.reset);
159
- process.exit(0);
160
- }
161
-
162
- logInfo(`Current branch: ${branch}`);
163
-
164
- // Check if protected
165
- if (isProtectedBranch(branch)) {
166
- const suggestedBranch = suggestBranchName(input || {});
167
- printBlocked(branch, suggestedBranch);
168
- process.exit(2);
169
- }
170
-
171
- // Not a protected branch - allow
172
- printSuccess(branch);
173
- process.exit(0);
174
- }
175
-
176
- // Handle errors
177
- process.on('unhandledRejection', (error) => {
178
- logError(`Unhandled error: ${error.message}`);
179
- process.exit(1);
180
- });
181
-
182
- // Run
183
- try {
184
- main();
185
- } catch (error) {
186
- logError(`Fatal error: ${error.message}`);
187
- process.exit(1);
188
- }
@@ -1,16 +0,0 @@
1
- {
2
- "hooks": {
3
- "PreToolUse": [
4
- {
5
- "matcher": "Write|Edit|MultiEdit",
6
- "hooks": [
7
- {
8
- "type": "command",
9
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/main-guard.cjs\"",
10
- "timeout": 5
11
- }
12
- ]
13
- }
14
- ]
15
- }
16
- }