xtrm-tools 2.0.2 → 2.0.3
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/hooks/README.md +107 -4
- 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 +139 -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
package/cli/package.json
CHANGED
package/hooks/README.md
CHANGED
|
@@ -139,18 +139,121 @@ Hooks intercept specific events in the Claude Code lifecycle to provide:
|
|
|
139
139
|
**Purpose**: Displays custom status line information.
|
|
140
140
|
**Trigger**: StatusLine
|
|
141
141
|
|
|
142
|
+
## Workflow Enforcement Hooks (JavaScript)
|
|
143
|
+
|
|
144
|
+
Installed globally to `~/.claude/hooks/` by `xtrm install`. Require Node.js.
|
|
145
|
+
|
|
146
|
+
### main-guard.mjs
|
|
147
|
+
|
|
148
|
+
**Purpose**: Blocks direct file edits and dangerous git operations on protected branches (`main`/`master`). Enforces the feature-branch → PR workflow.
|
|
149
|
+
|
|
150
|
+
**Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|Bash`)
|
|
151
|
+
|
|
152
|
+
**Blocks**:
|
|
153
|
+
- Write/Edit/MultiEdit/NotebookEdit on protected branches
|
|
154
|
+
- `git commit` directly on protected branches
|
|
155
|
+
- `git push` to protected branches
|
|
156
|
+
|
|
157
|
+
**Configuration** (injected into `~/.claude/settings.json`):
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"hooks": {
|
|
161
|
+
"PreToolUse": [{
|
|
162
|
+
"matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash",
|
|
163
|
+
"hooks": [{ "type": "command", "command": "~/.claude/hooks/main-guard.mjs", "timeout": 5000 }]
|
|
164
|
+
}]
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### beads-gate-utils.mjs
|
|
172
|
+
|
|
173
|
+
**Purpose**: Shared utility module imported by all beads gate hooks. Not registered as a hook itself.
|
|
174
|
+
|
|
175
|
+
**Exports**: `resolveCwd`, `isBeadsProject`, `getSessionClaim`, `getTotalWork`, `getInProgress`, `clearSessionClaim`, `withSafeBdContext`
|
|
176
|
+
|
|
177
|
+
**Requires**: `bd` (beads CLI), `dolt`
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### beads-edit-gate.mjs
|
|
182
|
+
|
|
183
|
+
**Purpose**: Blocks file edits when the current session has not claimed a beads issue via `bd kv`. Prevents free-riding in multi-agent and multi-session scenarios.
|
|
184
|
+
|
|
185
|
+
**Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|mcp__serena__*`)
|
|
186
|
+
|
|
187
|
+
**Behavior**:
|
|
188
|
+
- Session has claim (`bd kv get "claimed:<session_id>"`) → allow
|
|
189
|
+
- No claim + no trackable work → allow (clean-start state)
|
|
190
|
+
- No claim + open/in_progress issues exist → block
|
|
191
|
+
- Falls back to global in_progress check when `session_id` is absent
|
|
192
|
+
|
|
193
|
+
**Requires**: `bd`, `dolt`
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### beads-commit-gate.mjs
|
|
198
|
+
|
|
199
|
+
**Purpose**: Blocks `git commit` when the current session still has an unclosed beads claim.
|
|
200
|
+
|
|
201
|
+
**Trigger**: PreToolUse (`Bash`) — only fires when command matches `git commit`
|
|
202
|
+
|
|
203
|
+
**Requires**: `bd`, `dolt`
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### beads-stop-gate.mjs
|
|
208
|
+
|
|
209
|
+
**Purpose**: Blocks the agent from stopping when the current session has an unclosed beads claim.
|
|
210
|
+
|
|
211
|
+
**Trigger**: Stop
|
|
212
|
+
|
|
213
|
+
**Requires**: `bd`, `dolt`
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### beads-close-memory-prompt.mjs
|
|
218
|
+
|
|
219
|
+
**Purpose**: After `bd close`, clears the session's kv claim and injects a reminder to capture knowledge before moving on.
|
|
220
|
+
|
|
221
|
+
**Trigger**: PostToolUse (`Bash`) — only fires when command matches `bd close`
|
|
222
|
+
|
|
223
|
+
**Requires**: `bd`, `dolt`
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Beads claim workflow
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Claim an issue before editing
|
|
231
|
+
bd update <id> --status=in_progress
|
|
232
|
+
bd kv set "claimed:<session_id>" "<id>"
|
|
233
|
+
|
|
234
|
+
# Edit files freely
|
|
235
|
+
# ...
|
|
236
|
+
|
|
237
|
+
# Close when done — hook auto-clears the claim
|
|
238
|
+
bd close <id>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
142
243
|
## Installation
|
|
143
244
|
|
|
144
|
-
|
|
245
|
+
Use `xtrm install` to deploy all hooks automatically. For manual setup:
|
|
246
|
+
|
|
247
|
+
1. Copy hooks to the global Claude Code directory:
|
|
145
248
|
```bash
|
|
146
|
-
cp hooks
|
|
249
|
+
cp hooks/*.mjs hooks/*.py ~/.claude/hooks/
|
|
147
250
|
```
|
|
148
251
|
|
|
149
252
|
2. Make scripts executable:
|
|
150
253
|
```bash
|
|
151
|
-
chmod +x ~/.claude/hooks/*.
|
|
254
|
+
chmod +x ~/.claude/hooks/*.mjs ~/.claude/hooks/*.py
|
|
152
255
|
```
|
|
153
256
|
|
|
154
|
-
3.
|
|
257
|
+
3. Merge hook entries into `~/.claude/settings.json`.
|
|
155
258
|
|
|
156
259
|
4. Restart Claude Code.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// beads-close-memory-prompt — Claude Code PostToolUse hook
|
|
3
|
+
// After `bd close`: clears session claim from bd kv, then injects a short
|
|
4
|
+
// reminder into Claude's context to capture knowledge and consider underused
|
|
5
|
+
// beads features.
|
|
6
|
+
// Output to stdout is shown to Claude as additional context.
|
|
7
|
+
//
|
|
8
|
+
// Installed by: xtrm install
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import {
|
|
12
|
+
resolveCwd, isBeadsProject, clearSessionClaim, withSafeBdContext,
|
|
13
|
+
} from './beads-gate-utils.mjs';
|
|
14
|
+
|
|
15
|
+
let input;
|
|
16
|
+
try {
|
|
17
|
+
input = JSON.parse(readFileSync(0, 'utf8'));
|
|
18
|
+
} catch {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (input.tool_name !== 'Bash') process.exit(0);
|
|
23
|
+
if (!/\bbd\s+close\b/.test(input.tool_input?.command ?? '')) process.exit(0);
|
|
24
|
+
|
|
25
|
+
withSafeBdContext(() => {
|
|
26
|
+
const cwd = resolveCwd(input);
|
|
27
|
+
if (!isBeadsProject(cwd)) process.exit(0);
|
|
28
|
+
|
|
29
|
+
if (input.session_id) {
|
|
30
|
+
clearSessionClaim(input.session_id, cwd);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
process.stdout.write(
|
|
34
|
+
'\n[beads] Issue(s) closed. Before moving on:\n\n' +
|
|
35
|
+
' Knowledge worth keeping?\n' +
|
|
36
|
+
' bd remember "key insight from this work"\n' +
|
|
37
|
+
' bd memories <keyword> -- search what is already stored\n\n' +
|
|
38
|
+
' Discovered related work while implementing?\n' +
|
|
39
|
+
' bd create --title="..." --deps=discovered-from:<id>\n\n' +
|
|
40
|
+
' Underused features to consider:\n' +
|
|
41
|
+
' bd dep add <a> <b> -- link blocking relationships between issues\n' +
|
|
42
|
+
' bd graph -- visualize issue dependency graph\n' +
|
|
43
|
+
' bd orphans -- issues referenced in commits but still open\n' +
|
|
44
|
+
' bd preflight -- PR readiness checklist before gh pr create\n' +
|
|
45
|
+
' bd stale -- issues not touched recently\n'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
process.exit(0);
|
|
49
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// beads-commit-gate — Claude Code PreToolUse hook
|
|
3
|
+
// Blocks `git commit` when this session still has an unclosed claim in bd kv.
|
|
4
|
+
// Falls back to global in_progress check when session_id is unavailable.
|
|
5
|
+
// Forces: close issues first, THEN commit.
|
|
6
|
+
// Exit 0: allow | Exit 2: block (stderr shown to Claude)
|
|
7
|
+
//
|
|
8
|
+
// Installed by: xtrm install
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import {
|
|
12
|
+
resolveCwd, isBeadsProject, getSessionClaim,
|
|
13
|
+
getInProgress, withSafeBdContext,
|
|
14
|
+
} from './beads-gate-utils.mjs';
|
|
15
|
+
|
|
16
|
+
let input;
|
|
17
|
+
try {
|
|
18
|
+
input = JSON.parse(readFileSync(0, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if ((input.tool_name ?? '') !== 'Bash') process.exit(0);
|
|
24
|
+
if (!/\bgit\s+commit\b/.test(input.tool_input?.command ?? '')) process.exit(0);
|
|
25
|
+
|
|
26
|
+
const NEXT_STEPS =
|
|
27
|
+
' 3. bd close <id1> <id2> ... ← you are here\n' +
|
|
28
|
+
' 4. git add <files> && git commit -m "..."\n' +
|
|
29
|
+
' 5. git push -u origin <feature-branch>\n' +
|
|
30
|
+
' 6. gh pr create --fill && gh pr merge --squash\n' +
|
|
31
|
+
' 7. git checkout master && git reset --hard origin/master\n';
|
|
32
|
+
|
|
33
|
+
withSafeBdContext(() => {
|
|
34
|
+
const cwd = resolveCwd(input);
|
|
35
|
+
if (!isBeadsProject(cwd)) process.exit(0);
|
|
36
|
+
|
|
37
|
+
const sessionId = input.session_id;
|
|
38
|
+
|
|
39
|
+
if (sessionId) {
|
|
40
|
+
const claimed = getSessionClaim(sessionId, cwd);
|
|
41
|
+
if (claimed === null) process.exit(0);
|
|
42
|
+
if (!claimed) process.exit(0);
|
|
43
|
+
|
|
44
|
+
const ip = getInProgress(cwd);
|
|
45
|
+
const summary = ip?.summary ?? ` Claimed: ${claimed}`;
|
|
46
|
+
|
|
47
|
+
process.stderr.write(
|
|
48
|
+
'🚫 BEADS GATE: Close open issues before committing.\n\n' +
|
|
49
|
+
`Open issues:\n${summary}\n\n` +
|
|
50
|
+
'Next steps:\n' + NEXT_STEPS
|
|
51
|
+
);
|
|
52
|
+
process.exit(2);
|
|
53
|
+
} else {
|
|
54
|
+
const ip = getInProgress(cwd);
|
|
55
|
+
if (ip === null || ip.count === 0) process.exit(0);
|
|
56
|
+
|
|
57
|
+
process.stderr.write(
|
|
58
|
+
'🚫 BEADS GATE: Close open issues before committing.\n\n' +
|
|
59
|
+
`Open issues:\n${ip.summary}\n\n` +
|
|
60
|
+
'Next steps:\n' + NEXT_STEPS
|
|
61
|
+
);
|
|
62
|
+
process.exit(2);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// beads-edit-gate — Claude Code PreToolUse hook
|
|
3
|
+
// Blocks file edits when this session has not claimed a beads issue via bd kv.
|
|
4
|
+
// Falls back to global in_progress check when session_id is unavailable.
|
|
5
|
+
// Only active in projects with a .beads/ directory.
|
|
6
|
+
// Exit 0: allow | Exit 2: block (stderr shown to Claude)
|
|
7
|
+
//
|
|
8
|
+
// Installed by: xtrm install
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import {
|
|
12
|
+
resolveCwd, isBeadsProject, getSessionClaim,
|
|
13
|
+
getTotalWork, getInProgress, withSafeBdContext,
|
|
14
|
+
} from './beads-gate-utils.mjs';
|
|
15
|
+
|
|
16
|
+
let input;
|
|
17
|
+
try {
|
|
18
|
+
input = JSON.parse(readFileSync(0, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
withSafeBdContext(() => {
|
|
24
|
+
const cwd = resolveCwd(input);
|
|
25
|
+
if (!isBeadsProject(cwd)) process.exit(0);
|
|
26
|
+
|
|
27
|
+
const sessionId = input.session_id;
|
|
28
|
+
|
|
29
|
+
if (sessionId) {
|
|
30
|
+
const claimed = getSessionClaim(sessionId, cwd);
|
|
31
|
+
if (claimed === null) process.exit(0); // bd kv unavailable — fail open
|
|
32
|
+
if (claimed) process.exit(0); // this session has an active claim
|
|
33
|
+
|
|
34
|
+
const totalWork = getTotalWork(cwd);
|
|
35
|
+
if (totalWork === null) process.exit(0); // can't determine — fail open
|
|
36
|
+
if (totalWork === 0) process.exit(0); // nothing to track — clean-start state
|
|
37
|
+
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
'🚫 BEADS GATE: This session has no active claim — claim an issue before editing files.\n\n' +
|
|
40
|
+
' bd update <id> --status=in_progress\n' +
|
|
41
|
+
` bd kv set "claimed:${sessionId}" "<id>"\n\n` +
|
|
42
|
+
'Or create a new issue:\n' +
|
|
43
|
+
' bd create --title="<what you\'re doing>" --type=task --priority=2\n' +
|
|
44
|
+
' bd update <id> --status=in_progress\n' +
|
|
45
|
+
` bd kv set "claimed:${sessionId}" "<id>"\n`
|
|
46
|
+
);
|
|
47
|
+
process.exit(2);
|
|
48
|
+
} else {
|
|
49
|
+
// Fallback: global in_progress check (non-Claude environments / no session_id).
|
|
50
|
+
const ip = getInProgress(cwd);
|
|
51
|
+
if (ip === null) process.exit(0);
|
|
52
|
+
if (ip.count > 0) process.exit(0);
|
|
53
|
+
|
|
54
|
+
const totalWork = getTotalWork(cwd);
|
|
55
|
+
if (totalWork === null || totalWork === 0) process.exit(0);
|
|
56
|
+
|
|
57
|
+
process.stderr.write(
|
|
58
|
+
'🚫 BEADS GATE: No active issue — create one before editing files.\n\n' +
|
|
59
|
+
' bd create --title="<what you\'re doing>" --type=task --priority=2\n' +
|
|
60
|
+
' bd update <id> --status=in_progress\n\n' +
|
|
61
|
+
'Full workflow (do this every session):\n' +
|
|
62
|
+
' 1. bd create + bd update in_progress ← you are here\n' +
|
|
63
|
+
' 2. Edit files / write code\n' +
|
|
64
|
+
' 3. bd close <id> close when done\n' +
|
|
65
|
+
' 4. git add <files> && git commit\n' +
|
|
66
|
+
' 5. git push -u origin <feature-branch>\n' +
|
|
67
|
+
' 6. gh pr create --fill && gh pr merge --squash\n' +
|
|
68
|
+
' 7. git checkout master && git reset --hard origin/master\n'
|
|
69
|
+
);
|
|
70
|
+
process.exit(2);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// beads-gate-utils.mjs — shared infrastructure for beads gate hooks
|
|
3
|
+
// Import from sibling hooks using: import { ... } from './beads-gate-utils.mjs';
|
|
4
|
+
// Static ES module imports resolve relative to the importing file's location,
|
|
5
|
+
// not CWD, so this works regardless of the project directory.
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
/** Resolve project cwd from hook input JSON. */
|
|
12
|
+
export function resolveCwd(input) {
|
|
13
|
+
return input.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Return true if the directory contains a .beads project. */
|
|
17
|
+
export function isBeadsProject(cwd) {
|
|
18
|
+
return existsSync(join(cwd, '.beads'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the claimed issue ID for a session from bd kv.
|
|
23
|
+
* Returns: issue ID string if claimed, '' if not set, null if bd kv unavailable.
|
|
24
|
+
* Note: bd kv get exits 1 for missing keys — execSync throws, so we check err.status.
|
|
25
|
+
*/
|
|
26
|
+
export function getSessionClaim(sessionId, cwd) {
|
|
27
|
+
try {
|
|
28
|
+
return execSync(`bd kv get "claimed:${sessionId}"`, {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
cwd,
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
32
|
+
timeout: 5000,
|
|
33
|
+
}).trim();
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (err.status === 1) return ''; // key not found — no claim
|
|
36
|
+
return null; // command failed — bd kv unavailable
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse work counts from a bd list output string.
|
|
42
|
+
* Reads the "Total: N issues (X open, Y in progress)" summary line.
|
|
43
|
+
* Returns { open, inProgress } or null if the line is absent.
|
|
44
|
+
*
|
|
45
|
+
* This is more reliable than counting symbols or tokens: the Total line is
|
|
46
|
+
* a structured summary that doesn't depend on status-legend text or box-drawing
|
|
47
|
+
* characters, and it's present in all non-empty bd list outputs.
|
|
48
|
+
*/
|
|
49
|
+
function parseCounts(output) {
|
|
50
|
+
const m = output.match(/Total:\s*\d+\s+issues?\s*\((\d+)\s+open,\s*(\d+)\s+in progress\)/);
|
|
51
|
+
if (!m) return null;
|
|
52
|
+
return { open: parseInt(m[1], 10), inProgress: parseInt(m[2], 10) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get in_progress issues as { count, summary }.
|
|
57
|
+
* Returns null if bd is unavailable.
|
|
58
|
+
*/
|
|
59
|
+
export function getInProgress(cwd) {
|
|
60
|
+
try {
|
|
61
|
+
const output = execSync('bd list --status=in_progress', {
|
|
62
|
+
encoding: 'utf8', cwd, stdio: ['pipe', 'pipe', 'pipe'], timeout: 8000,
|
|
63
|
+
});
|
|
64
|
+
const counts = parseCounts(output);
|
|
65
|
+
return {
|
|
66
|
+
count: counts?.inProgress ?? 0,
|
|
67
|
+
summary: output.trim(),
|
|
68
|
+
};
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Count total trackable work (open + in_progress issues) using a single bd list call.
|
|
76
|
+
* Returns the count, or null if bd is unavailable.
|
|
77
|
+
*/
|
|
78
|
+
export function getTotalWork(cwd) {
|
|
79
|
+
try {
|
|
80
|
+
// Use default status filter (non-closed) and parse Total summary.
|
|
81
|
+
// Repeating --status is not additive in bd CLI and can collapse to one status.
|
|
82
|
+
const output = execSync('bd list', {
|
|
83
|
+
encoding: 'utf8', cwd, stdio: ['pipe', 'pipe', 'pipe'], timeout: 8000,
|
|
84
|
+
});
|
|
85
|
+
const counts = parseCounts(output);
|
|
86
|
+
if (!counts) return 0; // "No issues found." — nothing to track
|
|
87
|
+
return counts.open + counts.inProgress;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Clear the session claim key from bd kv. Non-fatal — best-effort cleanup.
|
|
95
|
+
*/
|
|
96
|
+
export function clearSessionClaim(sessionId, cwd) {
|
|
97
|
+
try {
|
|
98
|
+
execSync(`bd kv clear "claimed:${sessionId}"`, {
|
|
99
|
+
encoding: 'utf8', cwd, stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000,
|
|
100
|
+
});
|
|
101
|
+
} catch {
|
|
102
|
+
// non-fatal
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Option C: wrap hook body with uniform fail-open error handling.
|
|
108
|
+
* Any unexpected top-level throw exits 0 (allow) rather than crashing visibly.
|
|
109
|
+
*
|
|
110
|
+
* Usage:
|
|
111
|
+
* withSafeBdContext(() => {
|
|
112
|
+
* // hook logic here — call process.exit() to set exit code
|
|
113
|
+
* });
|
|
114
|
+
*/
|
|
115
|
+
export function withSafeBdContext(fn) {
|
|
116
|
+
try {
|
|
117
|
+
fn();
|
|
118
|
+
} catch {
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// beads-stop-gate — Claude Code Stop hook
|
|
3
|
+
// Blocks the agent from stopping when this session has an unclosed claim in bd kv.
|
|
4
|
+
// Falls back to global in_progress check when session_id is unavailable.
|
|
5
|
+
// Exit 0: allow stop | Exit 2: block stop (stderr shown to Claude)
|
|
6
|
+
//
|
|
7
|
+
// Installed by: xtrm install
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import {
|
|
11
|
+
resolveCwd, isBeadsProject, getSessionClaim,
|
|
12
|
+
getInProgress, withSafeBdContext,
|
|
13
|
+
} from './beads-gate-utils.mjs';
|
|
14
|
+
|
|
15
|
+
let input;
|
|
16
|
+
try {
|
|
17
|
+
input = JSON.parse(readFileSync(0, 'utf8'));
|
|
18
|
+
} catch {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CLOSE_PROTOCOL =
|
|
23
|
+
' 3. bd close <id1> <id2> ... close all in_progress issues\n' +
|
|
24
|
+
' 4. git add <files> && git commit -m "..." commit your changes\n' +
|
|
25
|
+
' 5. git push -u origin <feature-branch> push feature branch\n' +
|
|
26
|
+
' 6. gh pr create --fill create PR\n' +
|
|
27
|
+
' 7. gh pr merge --squash merge PR\n' +
|
|
28
|
+
' 8. git checkout master && git reset --hard origin/master\n';
|
|
29
|
+
|
|
30
|
+
withSafeBdContext(() => {
|
|
31
|
+
const cwd = resolveCwd(input);
|
|
32
|
+
if (!isBeadsProject(cwd)) process.exit(0);
|
|
33
|
+
|
|
34
|
+
const sessionId = input.session_id;
|
|
35
|
+
|
|
36
|
+
if (sessionId) {
|
|
37
|
+
const claimed = getSessionClaim(sessionId, cwd);
|
|
38
|
+
if (claimed === null) process.exit(0); // bd kv unavailable — fail open
|
|
39
|
+
if (!claimed) process.exit(0); // no active claim for this session
|
|
40
|
+
|
|
41
|
+
const ip = getInProgress(cwd);
|
|
42
|
+
const summary = ip?.summary ?? ` Claimed: ${claimed}`;
|
|
43
|
+
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
'🚫 BEADS STOP GATE: Unresolved issues — complete the session close protocol.\n\n' +
|
|
46
|
+
`Open issues:\n${summary}\n\n` +
|
|
47
|
+
'Session close protocol:\n' + CLOSE_PROTOCOL
|
|
48
|
+
);
|
|
49
|
+
process.exit(2);
|
|
50
|
+
} else {
|
|
51
|
+
const ip = getInProgress(cwd);
|
|
52
|
+
if (ip === null || ip.count === 0) process.exit(0);
|
|
53
|
+
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
'🚫 BEADS STOP GATE: Unresolved issues — complete the session close protocol.\n\n' +
|
|
56
|
+
`Open issues:\n${ip.summary}\n\n` +
|
|
57
|
+
'Session close protocol:\n' + CLOSE_PROTOCOL
|
|
58
|
+
);
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
|
|
37
|
+
const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
38
|
+
|
|
39
|
+
if (WRITE_TOOLS.has(tool)) {
|
|
40
|
+
process.stderr.write(
|
|
41
|
+
`⛔ You are on '${branch}' — never edit files directly on master.\n\n` +
|
|
42
|
+
'Full workflow:\n' +
|
|
43
|
+
' 1. git checkout -b feature/<name> ← start here\n' +
|
|
44
|
+
' 2. bd create + bd update in_progress track your work\n' +
|
|
45
|
+
' 3. Edit files / write code\n' +
|
|
46
|
+
' 4. bd close <id> && git add && git commit\n' +
|
|
47
|
+
' 5. git push -u origin feature/<name>\n' +
|
|
48
|
+
' 6. gh pr create --fill && gh pr merge --squash\n' +
|
|
49
|
+
' 7. git checkout master && git reset --hard origin/master\n'
|
|
50
|
+
);
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const WORKFLOW =
|
|
55
|
+
'Full workflow:\n' +
|
|
56
|
+
' 1. git checkout -b feature/<name> \u2190 start here\n' +
|
|
57
|
+
' 2. bd create + bd update in_progress track your work\n' +
|
|
58
|
+
' 3. Edit files / write code\n' +
|
|
59
|
+
' 4. bd close <id> && git add && git commit\n' +
|
|
60
|
+
' 5. git push -u origin feature/<name>\n' +
|
|
61
|
+
' 6. gh pr create --fill && gh pr merge --squash\n' +
|
|
62
|
+
' 7. git checkout master && git reset --hard origin/master\n';
|
|
63
|
+
|
|
64
|
+
if (tool === 'Bash') {
|
|
65
|
+
const cmd = (input.tool_input?.command ?? '').trim().replace(/\s+/g, ' ');
|
|
66
|
+
|
|
67
|
+
// Emergency override — escape hatch for power users
|
|
68
|
+
if (process.env.MAIN_GUARD_ALLOW_BASH === '1') {
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Safe allowlist — non-mutating commands + explicit branch-exit paths.
|
|
73
|
+
// Important: do not allow generic checkout/switch forms, which include
|
|
74
|
+
// mutating variants such as `git checkout -- <path>`.
|
|
75
|
+
const SAFE_BASH_PATTERNS = [
|
|
76
|
+
/^git\s+(status|log|diff|branch|show|describe|fetch|remote|config)\b/,
|
|
77
|
+
/^git\s+pull\b/,
|
|
78
|
+
/^git\s+stash\b/,
|
|
79
|
+
/^git\s+worktree\b/,
|
|
80
|
+
/^git\s+checkout\s+-b\s+\S+/,
|
|
81
|
+
/^git\s+switch\s+-c\s+\S+/,
|
|
82
|
+
/^gh\s+/,
|
|
83
|
+
/^bd\s+/,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
if (SAFE_BASH_PATTERNS.some(p => p.test(cmd))) {
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Specific messages for common blocked operations
|
|
91
|
+
if (/^git\s+commit\b/.test(cmd)) {
|
|
92
|
+
process.stderr.write(
|
|
93
|
+
`\u26D4 Don't commit directly to '${branch}' \u2014 use a feature branch.\n\n` +
|
|
94
|
+
WORKFLOW
|
|
95
|
+
);
|
|
96
|
+
process.exit(2);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (/^git\s+push\b/.test(cmd)) {
|
|
100
|
+
const tokens = cmd.split(' ');
|
|
101
|
+
const lastToken = tokens[tokens.length - 1];
|
|
102
|
+
const explicitProtected = protectedBranches.some(b => lastToken === b || lastToken.endsWith(`:${b}`));
|
|
103
|
+
const impliedProtected = tokens.length <= 3 && protectedBranches.includes(branch);
|
|
104
|
+
if (explicitProtected || impliedProtected) {
|
|
105
|
+
process.stderr.write(
|
|
106
|
+
`\u26D4 Don't push directly to '${branch}' \u2014 use the PR workflow.\n\n` +
|
|
107
|
+
'Next steps:\n' +
|
|
108
|
+
' 5. git push -u origin <feature-branch> \u2190 push your branch\n' +
|
|
109
|
+
' 6. gh pr create --fill create PR\n' +
|
|
110
|
+
' gh pr merge --squash merge it\n' +
|
|
111
|
+
' 7. git checkout master sync master\n' +
|
|
112
|
+
' git reset --hard origin/master\n\n' +
|
|
113
|
+
"If you're not on a feature branch yet:\n" +
|
|
114
|
+
' git checkout -b feature/<name> (then re-commit and push)\n'
|
|
115
|
+
);
|
|
116
|
+
process.exit(2);
|
|
117
|
+
}
|
|
118
|
+
// Pushing to a feature branch — allow
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Default deny — block everything else on protected branches
|
|
123
|
+
process.stderr.write(
|
|
124
|
+
`\u26D4 Bash is restricted on '${branch}' \u2014 use a feature branch for file writes and script execution.\n\n` +
|
|
125
|
+
'Allowed on protected branches:\n' +
|
|
126
|
+
' git status / log / diff / branch / fetch / pull / stash\n' +
|
|
127
|
+
' git checkout -b <name> (create feature branch \u2014 the exit path)\n' +
|
|
128
|
+
' git switch -c <name> (same)\n' +
|
|
129
|
+
' git worktree / config\n' +
|
|
130
|
+
' gh <any> (GitHub CLI)\n' +
|
|
131
|
+
' bd <any> (beads issue tracking)\n\n' +
|
|
132
|
+
'To run arbitrary commands:\n' +
|
|
133
|
+
' 1. git checkout -b feature/<name> \u2190 move to a feature branch, or\n' +
|
|
134
|
+
' 2. MAIN_GUARD_ALLOW_BASH=1 <command> (escape hatch, use sparingly)\n'
|
|
135
|
+
);
|
|
136
|
+
process.exit(2);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
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.0.3",
|
|
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
|
}
|