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.
- package/README.md +0 -2
- package/cli/dist/index.cjs +19 -19
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/hooks.json +33 -0
- package/hooks/README.md +108 -5
- package/hooks/beads-close-memory-prompt.mjs +49 -0
- package/hooks/beads-commit-gate.mjs +64 -0
- package/hooks/beads-edit-gate.mjs +72 -0
- package/hooks/beads-gate-utils.mjs +121 -0
- package/hooks/beads-stop-gate.mjs +61 -0
- package/hooks/main-guard.mjs +149 -0
- package/package.json +3 -2
- package/project-skills/py-quality-gate/.claude/settings.json +1 -1
- package/project-skills/service-skills-set/.claude/skills/creating-service-skills/SKILL.md +12 -12
- package/project-skills/service-skills-set/.claude/skills/creating-service-skills/references/script_quality_standards.md +13 -0
- package/project-skills/service-skills-set/.claude/skills/creating-service-skills/references/service_skill_system_guide.md +14 -0
- package/project-skills/service-skills-set/.claude/skills/creating-service-skills/scripts/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/project-skills/service-skills-set/.claude/skills/scoping-service-skills/SKILL.md +6 -6
- package/project-skills/service-skills-set/.claude/skills/updating-service-skills/SKILL.md +1 -1
- package/project-skills/service-skills-set/.claude/skills/using-service-skills/SKILL.md +2 -2
- package/project-skills/service-skills-set/.claude/skills/using-service-skills/scripts/__pycache__/skill_activator.cpython-314.pyc +0 -0
- 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
- package/project-skills/service-skills-set/.claude/skills/using-service-skills/scripts/skill_activator.py +2 -2
- package/project-skills/service-skills-set/.claude/skills/using-service-skills/scripts/test_skill_activator.py +58 -0
- package/project-skills/ts-quality-gate/.claude/settings.json +1 -1
- package/project-skills/main-guard/.claude/hooks/main-guard.cjs +0 -188
- package/project-skills/main-guard/.claude/settings.json +0 -16
- package/project-skills/main-guard/.claude/skills/using-main-guard/SKILL.md +0 -135
- 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
|
|
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
|
}
|
|
@@ -20,7 +20,7 @@ immediately useful to any agent working on the service.
|
|
|
20
20
|
|
|
21
21
|
## Mandatory Two-Phase Workflow
|
|
22
22
|
|
|
23
|
-
**
|
|
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
|
|
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 (
|
|
121
|
+
### Phase 2 Script Writing (Complete Implementation)
|
|
122
122
|
|
|
123
|
-
After research is complete, replace
|
|
124
|
-
|
|
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
|
|
210
|
+
#### `scripts/Makefile` (required)
|
|
211
211
|
|
|
212
|
-
The scaffolder creates a stub `Makefile` in Phase 1. In Phase 2
|
|
213
|
-
correct and complete
|
|
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. **
|
|
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. **
|
|
267
|
-
4. **
|
|
268
|
-
5. **
|
|
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.
|
|
Binary file
|
|
@@ -24,7 +24,7 @@ in the conversation.
|
|
|
24
24
|
|
|
25
25
|
### Step 1 — Read the Registry
|
|
26
26
|
|
|
27
|
-
Run this
|
|
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
|
|
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
|
-
|
|
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,
|
|
131
|
-
regression-test phase
|
|
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,
|
|
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
|
-
|
|
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
|
|
82
|
-
|
|
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
|
{
|
|
Binary file
|
|
Binary file
|
|
@@ -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()
|
|
@@ -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
|
-
}
|