xtrm-tools 2.1.17 → 2.1.21

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,67 @@
1
+ import type { ExtensionAPI, ToolResultEvent } from "@mariozechner/pi-coding-agent";
2
+ import { SubprocessRunner, EventAdapter, Logger } from "./core";
3
+ import * as path from "node:path";
4
+ import * as fs from "node:fs";
5
+
6
+ const logger = new Logger({ namespace: "quality-gates" });
7
+
8
+ export default function (pi: ExtensionAPI) {
9
+ pi.on("tool_result", async (event, ctx) => {
10
+ if (!EventAdapter.isMutatingFileTool(event)) return undefined;
11
+
12
+ const cwd = ctx.cwd || process.cwd();
13
+ const filePath = EventAdapter.extractPathFromToolInput(event, cwd);
14
+ if (!filePath) return undefined;
15
+
16
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
17
+ const ext = path.extname(fullPath);
18
+
19
+ let scriptPath: string | null = null;
20
+ let runner: string = "node";
21
+
22
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
23
+ scriptPath = path.join(cwd, ".claude", "hooks", "quality-check.cjs");
24
+ runner = "node";
25
+ } else if (ext === ".py") {
26
+ scriptPath = path.join(cwd, ".claude", "hooks", "quality-check.py");
27
+ runner = "python3";
28
+ }
29
+
30
+ if (!scriptPath || !fs.existsSync(scriptPath)) return undefined;
31
+
32
+ const hookInput = JSON.stringify({
33
+ tool_name: event.toolName,
34
+ tool_input: event.input,
35
+ cwd: cwd,
36
+ });
37
+
38
+ const result = await SubprocessRunner.run(runner, [scriptPath], {
39
+ cwd,
40
+ input: hookInput,
41
+ env: { ...process.env, CLAUDE_PROJECT_DIR: cwd },
42
+ timeoutMs: 30000,
43
+ });
44
+
45
+ if (result.code === 0) {
46
+ if (result.stderr && result.stderr.trim()) {
47
+ const newContent = [...event.content];
48
+ newContent.push({ type: "text", text: `\n\n**Quality Gate**: ${result.stderr.trim()}` });
49
+ return { content: newContent };
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ if (result.code === 2) {
55
+ const newContent = [...event.content];
56
+ newContent.push({ type: "text", text: `\n\n**Quality Gate FAILED**:\n${result.stderr || result.stdout || "Unknown error"}` });
57
+
58
+ if (ctx.hasUI) {
59
+ ctx.ui.notify(`Quality Gate failed for ${path.basename(fullPath)}`, "error");
60
+ }
61
+
62
+ return { isError: true, content: newContent };
63
+ }
64
+
65
+ return undefined;
66
+ });
67
+ }
@@ -0,0 +1,88 @@
1
+ import type { ExtensionAPI, ToolCallEvent, ToolResultEvent } from "@mariozechner/pi-coding-agent";
2
+ import { isToolCallEventType, isBashToolResult } from "@mariozechner/pi-coding-agent";
3
+ import { SubprocessRunner, Logger } from "./core";
4
+ import * as path from "node:path";
5
+ import * as fs from "node:fs";
6
+
7
+ const logger = new Logger({ namespace: "service-skills" });
8
+
9
+ export default function (pi: ExtensionAPI) {
10
+ const getCwd = (ctx: any) => ctx.cwd || process.cwd();
11
+
12
+ // 1. Catalog Injection
13
+ pi.on("before_agent_start", async (event, ctx) => {
14
+ const cwd = getCwd(ctx);
15
+ const catalogerPath = path.join(cwd, ".claude", "skills", "using-service-skills", "scripts", "cataloger.py");
16
+ if (!fs.existsSync(catalogerPath)) return undefined;
17
+
18
+ const result = await SubprocessRunner.run("python3", [catalogerPath], {
19
+ cwd,
20
+ env: { ...process.env, CLAUDE_PROJECT_DIR: cwd }
21
+ });
22
+
23
+ if (result.code === 0 && result.stdout.trim()) {
24
+ return { systemPrompt: event.systemPrompt + "\n\n" + result.stdout.trim() };
25
+ }
26
+ return undefined;
27
+ });
28
+
29
+ // 2. Territory Activation
30
+ pi.on("tool_call", async (event, ctx) => {
31
+ const cwd = getCwd(ctx);
32
+ const activatorPath = path.join(cwd, ".claude", "skills", "using-service-skills", "scripts", "skill_activator.py");
33
+ if (!fs.existsSync(activatorPath)) return undefined;
34
+
35
+ const hookInput = JSON.stringify({
36
+ tool_name: event.toolName === "bash" ? "Bash" : event.toolName,
37
+ tool_input: event.input,
38
+ cwd: cwd
39
+ });
40
+
41
+ const result = await SubprocessRunner.run("python3", [activatorPath], {
42
+ cwd,
43
+ input: hookInput,
44
+ env: { ...process.env, CLAUDE_PROJECT_DIR: cwd },
45
+ timeoutMs: 5000
46
+ });
47
+
48
+ if (result.code === 0 && result.stdout.trim()) {
49
+ try {
50
+ const parsed = JSON.parse(result.stdout.trim());
51
+ const context = parsed.hookSpecificOutput?.additionalContext;
52
+ if (context && ctx.hasUI) {
53
+ ctx.ui.notify(context, "info");
54
+ }
55
+ } catch (e) {
56
+ logger.error("Failed to parse skill_activator output", e);
57
+ }
58
+ }
59
+ return undefined;
60
+ });
61
+
62
+ // 3. Drift Detection
63
+ pi.on("tool_result", async (event, ctx) => {
64
+ const cwd = getCwd(ctx);
65
+ const driftDetectorPath = path.join(cwd, ".claude", "skills", "updating-service-skills", "scripts", "drift_detector.py");
66
+ if (!fs.existsSync(driftDetectorPath)) return undefined;
67
+
68
+ const hookInput = JSON.stringify({
69
+ tool_name: event.toolName === "bash" ? "Bash" : event.toolName,
70
+ tool_input: event.input,
71
+ cwd: cwd
72
+ });
73
+
74
+ const result = await SubprocessRunner.run("python3", [driftDetectorPath], {
75
+ cwd,
76
+ input: hookInput,
77
+ env: { ...process.env, CLAUDE_PROJECT_DIR: cwd },
78
+ timeoutMs: 10000
79
+ });
80
+
81
+ if (result.code === 0 && result.stdout.trim()) {
82
+ const newContent = [...event.content];
83
+ newContent.push({ type: "text", text: "\n\n" + result.stdout.trim() });
84
+ return { content: newContent };
85
+ }
86
+ return undefined;
87
+ });
88
+ }
@@ -0,0 +1,89 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { Logger } from "./core";
5
+
6
+ const logger = new Logger({ namespace: "xtrm-loader" });
7
+
8
+ /**
9
+ * Recursively find markdown files in a directory.
10
+ */
11
+ function findMarkdownFiles(dir: string, basePath: string = ""): string[] {
12
+ const results: string[] = [];
13
+ if (!fs.existsSync(dir)) return results;
14
+
15
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
16
+ for (const entry of entries) {
17
+ const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
18
+ if (entry.isDirectory()) {
19
+ results.push(...findMarkdownFiles(path.join(dir, entry.name), relativePath));
20
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
21
+ results.push(relativePath);
22
+ }
23
+ }
24
+ return results;
25
+ }
26
+
27
+ export default function (pi: ExtensionAPI) {
28
+ let projectContext: string = "";
29
+
30
+ pi.on("session_start", async (_event, ctx) => {
31
+ const cwd = ctx.cwd;
32
+ const contextParts: string[] = [];
33
+
34
+ // 1. Architecture & Roadmap
35
+ const roadmapPaths = [
36
+ path.join(cwd, "architecture", "project_roadmap.md"),
37
+ path.join(cwd, "ROADMAP.md"),
38
+ path.join(cwd, "architecture", "index.md")
39
+ ];
40
+
41
+ for (const p of roadmapPaths) {
42
+ if (fs.existsSync(p)) {
43
+ const content = fs.readFileSync(p, "utf8");
44
+ contextParts.push(`## Project Roadmap & Architecture (${path.relative(cwd, p)})\n\n${content}`);
45
+ break; // Only load the first one found
46
+ }
47
+ }
48
+
49
+ // 2. Project Rules (.claude/rules)
50
+ const rulesDir = path.join(cwd, ".claude", "rules");
51
+ if (fs.existsSync(rulesDir)) {
52
+ const ruleFiles = findMarkdownFiles(rulesDir);
53
+ if (ruleFiles.length > 0) {
54
+ const rulesContent = ruleFiles.map(f => {
55
+ const content = fs.readFileSync(path.join(rulesDir, f), "utf8");
56
+ return `### Rule: ${f}\n${content}`;
57
+ }).join("\n\n");
58
+ contextParts.push(`## Project Rules\n\n${rulesContent}`);
59
+ }
60
+ }
61
+
62
+ // 3. Project Skills (.claude/skills)
63
+ const skillsDir = path.join(cwd, ".claude", "skills");
64
+ if (fs.existsSync(skillsDir)) {
65
+ const skillFiles = findMarkdownFiles(skillsDir);
66
+ if (skillFiles.length > 0) {
67
+ const skillsContent = skillFiles.map(f => {
68
+ // We only want to list the paths/names so the agent knows what it can read
69
+ return `- ${f} (Path: .claude/skills/${f})`;
70
+ }).join("\n");
71
+ contextParts.push(`## Available Project Skills\n\nExisting service skills and workflows found in .claude/skills/:\n\n${skillsContent}\n\nUse the read tool to load any of these skills if relevant to the current task.`);
72
+ }
73
+ }
74
+
75
+ projectContext = contextParts.join("\n\n---\n\n");
76
+
77
+ if (projectContext && ctx.hasUI) {
78
+ ctx.ui.notify("XTRM-Loader: Project context and skills indexed", "info");
79
+ }
80
+ });
81
+
82
+ pi.on("before_agent_start", async (event) => {
83
+ if (!projectContext) return undefined;
84
+
85
+ return {
86
+ systemPrompt: event.systemPrompt + "\n\n# Project Intelligence Context\n\n" + projectContext
87
+ };
88
+ });
89
+ }
package/hooks/README.md CHANGED
@@ -1,350 +1,75 @@
1
1
  # Hooks
2
2
 
3
- Claude Code hooks that extend agent behavior with automated checks, suggestions, and workflow enhancements.
3
+ Claude Code hooks that extend agent behavior with automated checks, workflow enhancements, and safety guardrails.
4
4
 
5
5
  ## Overview
6
6
 
7
- Hooks intercept specific events in the Claude Code lifecycle to provide:
8
- - Proactive skill suggestions
9
- - Safety guardrails (venv enforcement, type checking)
10
- - Workflow reminders
11
- - Status information
7
+ Hooks intercept specific events in the Claude Code lifecycle. Following architecture decisions in v2.0.0+, the hook ecosystem is designed exclusively for Claude Code.
12
8
 
13
- ## Skill-Associated Hooks
9
+ *Note: In v2.1.15+, several older hooks (`skill-suggestion.py`, `skill-discovery.py`, `gitnexus-impact-reminder.py`, and `type-safety-enforcement.py`) were removed or superseded by native capabilities, CLI commands, and consolidated quality gates.*
14
10
 
15
- ### skill-suggestion.py
11
+ ## Project Hooks
16
12
 
17
- **Purpose**: Proactively suggests `/prompt-improving` or `/delegating` based on prompt analysis.
13
+ ### main-guard.mjs
18
14
 
19
- **Trigger**: UserPromptSubmit
15
+ **Purpose**: Enforces PR-only merge workflow with full git protection. Blocks direct commits and dangerous `git checkout`, `git reset`, and file writes via Bash on protected branches (`main`/`master`).
20
16
 
21
- **Skills**:
22
- - `prompt-improving` - Suggested for short/generic prompts
23
- - `delegating` - Suggested for simple tasks or explicit delegation requests
17
+ **Trigger**: PreToolUse (Write|Edit|MultiEdit|Serena edit tools|Bash)
24
18
 
25
- **Configuration**:
26
- ```json
27
- {
28
- "hooks": {
29
- "UserPromptSubmit": [{
30
- "hooks": [{
31
- "type": "command",
32
- "command": "/home/user/.claude/hooks/skill-suggestion.py",
33
- "timeout": 5000 // Claude: seconds (5000s), Gemini: milliseconds (5s)
34
- }]
35
- }]
36
- },
37
- "skillSuggestions": {
38
- "enabled": true
39
- }
40
- }
41
- ```
19
+ **Configuration**: Installed automatically to protect the main branch from unreviewed changes.
42
20
 
43
- ### skill-discovery.py
21
+ ### main-guard-post-push.mjs
44
22
 
45
- **Purpose**: Scans the `@skills/` directory for `SKILL.md` files and injects a summarized list of all available local skills into the agent's context at the start of a session.
23
+ **Purpose**: Workflow enforcement. After pushing a feature branch, reminds to open a PR, merge using `gh pr merge --squash`, and sync local via `git reset --hard origin/main`.
46
24
 
47
- **Trigger**: SessionStart
25
+ **Trigger**: PostToolUse (Bash: git push)
48
26
 
49
- **Skills**: All skills found in the repository's `skills/` directory.
27
+ ### gitnexus-hook.cjs
50
28
 
51
- **Configuration**:
52
- ```json
53
- {
54
- "hooks": {
55
- "SessionStart": [{
56
- "hooks": [{
57
- "type": "command",
58
- "command": "/home/user/.claude/hooks/skill-discovery.py",
59
- "timeout": 5000
60
- }]
61
- }]
62
- }
63
- }
64
- ```
29
+ **Purpose**: Enriches tool calls with knowledge graph context via `gitnexus augment`. Now supports Serena tools and uses a deduplication cache for efficiency.
65
30
 
66
- ### serena-workflow-reminder.py
31
+ **Trigger**: PostToolUse (Grep|Glob|Bash|Serena edit tools)
67
32
 
68
- **Purpose**: Enforces semantic workflow using "Using Serena LSP".
33
+ ## Beads Issue Tracking Gates
69
34
 
70
- **Triggers**:
71
- - `SessionStart`: Injects skill context.
72
- - `PreToolUse` (Read|Edit): Blocks inefficient usage.
35
+ The beads gate hooks integrate the `bd` (beads) issue tracker directly into Claude's workflow, ensuring no code changes happen without an active ticket.
73
36
 
74
- **Skill**: `using-serena-lsp`
37
+ **Installation**: Installed with `xtrm install all` or included when `beads`+`dolt` is available.
75
38
 
76
- **Configuration**:
77
- ```json
78
- {
79
- "hooks": {
80
- "SessionStart": [{
81
- "hooks": [{ "type": "command", "command": "/home/user/.claude/hooks/serena-workflow-reminder.py" }]
82
- }],
83
- "PreToolUse": [{
84
- "matcher": "Read|Edit",
85
- "hooks": [{ "type": "command", "command": "/home/user/.claude/hooks/serena-workflow-reminder.py" }]
86
- }]
87
- }
88
- }
89
- ```
39
+ ### Core Gates
40
+ - **`beads-edit-gate.mjs`** (PreToolUse) — Blocks writes/edits without an active issue claim.
41
+ - **`beads-commit-gate.mjs`** (PreToolUse) — Blocks commits with an unresolved session claim.
42
+ - **`beads-stop-gate.mjs`** (Stop) — Blocks session stop while a claim remains open.
43
+ - **`beads-close-memory-prompt.mjs`** (PostToolUse) — Prompts memory handoff after `bd close`.
90
44
 
91
- ## Standalone Hooks
45
+ ### Compaction & State Preservation (v2.1.18+)
46
+ - **`beads-pre-compact.mjs`** (PreCompact) — Saves the currently `in_progress` beads state before Claude clears context.
47
+ - **`beads-session-start.mjs`** (SessionStart) — Restores the `in_progress` state when the session restarts after compaction.
92
48
 
93
- ### pip-venv-guard.py
49
+ *Note: As of v2.1.18+, hook blocking messages are quieted and compacted to save tokens.*
94
50
 
95
- **Purpose**: Prevents accidental `pip install` outside virtual environments.
51
+ ## Hook Timeouts
96
52
 
97
- **Trigger**: PreToolUse (Bash)
53
+ Adjust hook execution timeouts in `settings.json` if commands take longer than expected:
98
54
 
99
- **Configuration**:
100
55
  ```json
101
56
  {
102
57
  "hooks": {
103
- "PreToolUse": [{
104
- "matcher": "Bash",
58
+ "PostToolUse": [{
105
59
  "hooks": [{
106
- "type": "command",
107
- "command": "/home/user/.claude/hooks/pip-venv-guard.py",
108
- "timeout": 3000 // 3 seconds in milliseconds (both Claude & Gemini)
60
+ "timeout": 5000 // Timeout in milliseconds (5000ms = 5 seconds)
109
61
  }]
110
62
  }]
111
63
  }
112
64
  }
113
65
  ```
114
66
 
115
- ### type-safety-enforcement.py
116
-
117
- **Purpose**: Enforces type safety checks in Python code before execution.
118
-
119
- **Trigger**: PreToolUse (Bash, Edit, Write)
120
-
121
- **Configuration**:
122
- ```json
123
- {
124
- "hooks": {
125
- "PreToolUse": [{
126
- "matcher": "Bash|Edit|Write",
127
- "hooks": [{
128
- "type": "command",
129
- "command": "/home/user/.claude/hooks/type-safety-enforcement.py",
130
- "timeout": 10000 // 10 seconds in milliseconds (both Claude & Gemini)
131
- }]
132
- }]
133
- }
134
- }
135
- ```
136
-
137
- ### statusline.js
138
-
139
- **Purpose**: Displays custom status line information.
140
- **Trigger**: StatusLine
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|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol|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** (global Claude config):
158
- ```json
159
- {
160
- "hooks": {
161
- "PreToolUse": [{
162
- "matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol|Bash",
163
- "hooks": [{ "type": "command", "command": "node \"~/.claude/hooks/main-guard.mjs\"", "timeout": 5000 }]
164
- }]
165
- }
166
- }
167
- ```
168
-
169
- ---
170
-
171
- ### main-guard-post-push.mjs
172
-
173
- **Purpose**: After a successful `git push` from a non-protected branch, injects a reminder for the next PR workflow steps.
174
-
175
- **Trigger**: PostToolUse (`Bash`) — only fires when command matches `git push` and appears successful.
176
-
177
- **Behavior**:
178
- - No blocking (informational only)
179
- - Skips protected branches (`main`/`master`)
180
- - Skips explicit pushes targeting protected branches
181
-
182
- **Guidance injected**:
183
- - `gh pr create --fill`
184
- - `gh pr merge --squash`
185
- - `git checkout main && git pull --ff-only`
186
- - Reminder to keep beads issue state updated
187
-
188
- ---
189
-
190
- ### beads-gate-utils.mjs
191
-
192
- **Purpose**: Shared utility module imported by all beads gate hooks. Not registered as a hook itself.
193
-
194
- **Exports**: `resolveCwd`, `isBeadsProject`, `getSessionClaim`, `getTotalWork`, `getInProgress`, `clearSessionClaim`, `withSafeBdContext`
195
-
196
- **Requires**: `bd` (beads CLI), `dolt`
197
-
198
- ---
199
-
200
- ### beads-edit-gate.mjs
201
-
202
- **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.
203
-
204
- **Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol`)
205
-
206
- **Behavior**:
207
- - Session has claim (`bd kv get "claimed:<session_id>"`) → allow
208
- - No claim + no trackable work → allow (clean-start state)
209
- - No claim + open/in_progress issues exist → block
210
- - Falls back to global in_progress check when `session_id` is absent
211
-
212
- **Requires**: `bd`, `dolt`
213
-
214
- ---
215
-
216
- ### beads-commit-gate.mjs
217
-
218
- **Purpose**: Blocks `git commit` when the current session still has an unclosed beads claim.
219
-
220
- **Trigger**: PreToolUse (`Bash`) — only fires when command matches `git commit`
221
-
222
- **Requires**: `bd`, `dolt`
223
-
224
- ---
225
-
226
- ### beads-stop-gate.mjs
227
-
228
- **Purpose**: Blocks the agent from stopping when the current session has an unclosed beads claim.
229
-
230
- **Trigger**: Stop
231
-
232
- **Requires**: `bd`, `dolt`
233
-
234
- ---
235
-
236
- ### beads-close-memory-prompt.mjs
237
-
238
- **Purpose**: After `bd close`, clears the session's kv claim and injects a reminder to capture knowledge before moving on.
239
-
240
- **Trigger**: PostToolUse (`Bash`) — only fires when command matches `bd close`
241
-
242
- **Requires**: `bd`, `dolt`
243
-
244
- ---
245
-
246
- ## Beads claim workflow
247
-
248
- ```bash
249
- # Claim an issue before editing
250
- bd update <id> --status=in_progress
251
- bd kv set "claimed:<session_id>" "<id>"
252
-
253
- # Edit files freely
254
- # ...
255
-
256
- # Close when done — hook auto-clears the claim
257
- bd close <id>
258
- ```
259
-
260
- ---
261
-
262
- ## Installation
263
-
264
- Use `xtrm install` to deploy all hooks automatically. For manual setup:
265
-
266
- 1. Copy hooks to the global Claude Code directory:
267
- ```bash
268
- cp hooks/*.mjs hooks/*.py ~/.claude/hooks/
269
- ```
270
-
271
- 2. Make scripts executable:
272
- ```bash
273
- chmod +x ~/.claude/hooks/*.mjs ~/.claude/hooks/*.py
274
- ```
275
-
276
- 3. Merge hook entries into `~/.claude/settings.json`.
277
-
278
- 4. Restart Claude Code.
279
-
280
- ---
281
-
282
- ## Beads Hooks Architecture
283
-
284
- The beads gate hooks are organized into three layers:
285
-
286
- ```
287
- ┌─────────────────────────────────────────────────────────────────┐
288
- │ HOOK ENTRYPOINTS │
289
- │ (thin wrappers - just parse, call core, emit, exit) │
290
- ├──────────────────┬──────────────────┬──────────────────────────┤
291
- │ beads-edit-gate │ beads-commit-gate│ beads-stop-gate │
292
- │ beads-memory-gate│ │ │
293
- └────────┬─────────┴────────┬─────────┴─────────────┬────────────┘
294
- │ │ │
295
- ▼ ▼ ▼
296
- ┌─────────────────────────────────────────────────────────────────┐
297
- │ beads-gate-core.mjs │
298
- │ Pure decision functions - no I/O, return {allow, reason} │
299
- ├─────────────────────────────────────────────────────────────────┤
300
- │ • readHookInput() → parsed input or null │
301
- │ • resolveSessionContext() → {cwd, sessionId, isBeadsProject} │
302
- │ • resolveClaimAndWorkState() → {claimed, claimId, work} │
303
- │ • decideEditGate() → {allow: bool, reason?: string} │
304
- │ • decideCommitGate() → {allow: bool, reason?: string} │
305
- │ • decideStopGate() → {allow: bool, reason?: string} │
306
- └─────────────────────────────────────────────────────────────────┘
307
-
308
-
309
- ┌─────────────────────────────────────────────────────────────────┐
310
- │ beads-gate-messages.mjs │
311
- │ Centralized message templates │
312
- ├─────────────────────────────────────────────────────────────────┤
313
- │ • WORKFLOW_STEPS - full 7-step workflow │
314
- │ • SESSION_CLOSE_PROTOCOL - stop gate steps │
315
- │ • editBlockMessage(sessionId) │
316
- │ • commitBlockMessage(summary, claimed) │
317
- │ • stopBlockMessage(summary, claimed) │
318
- │ • memoryPromptMessage() │
319
- └─────────────────────────────────────────────────────────────────┘
320
-
321
-
322
- ┌─────────────────────────────────────────────────────────────────┐
323
- │ beads-gate-utils.mjs │
324
- │ Low-level adapters - bd CLI, shell, fs operations │
325
- ├─────────────────────────────────────────────────────────────────┤
326
- │ • resolveCwd(input) │
327
- │ • isBeadsProject(cwd) │
328
- │ • getSessionClaim(sessionId, cwd) │
329
- │ • getTotalWork(cwd), getInProgress(cwd) │
330
- │ • clearSessionClaim(sessionId, cwd) │
331
- │ • withSafeBdContext(fn) │
332
- └─────────────────────────────────────────────────────────────────┘
333
- ```
334
-
335
- ### Where to Make Changes
67
+ ## Creating Custom Hooks
336
68
 
337
- | Change type | File to edit |
338
- |-------------|--------------|
339
- | Policy logic (when to block/allow) | `beads-gate-core.mjs` |
340
- | User-facing messages | `beads-gate-messages.mjs` |
341
- | bd CLI integration | `beads-gate-utils.mjs` |
342
- | Hook registration/wiring | Entrypoints (rarely needed) |
69
+ To create new project-specific hooks, use the `hook-development` global skill. Follow the canonical structure defined in the `xtrm-tools` core libraries.
343
70
 
344
- ### Exit Semantics
71
+ For debugging orphaned hooks, use `xtrm clean`.
345
72
 
346
- All hooks follow strict exit semantics:
347
- - `exit 0` — Allow the operation
348
- - `exit 2` — Block with message shown to Claude
73
+ ## Pi Extensions Migration
349
74
 
350
- The core functions are pure and never call `process.exit()`. Entrypoints are responsible for side effects.
75
+ Core workflow hooks have been migrated to native Pi Extensions for better performance and integration. See the [Pi Extensions Migration Guide](../docs/pi-extensions-migration.md) for details.