xtrm-tools 2.1.13 → 2.1.16

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/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-cli",
3
- "version": "2.1.13",
3
+ "version": "2.1.16",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
package/config/hooks.json CHANGED
@@ -4,6 +4,10 @@
4
4
  {
5
5
  "script": "skill-suggestion.py",
6
6
  "timeout": 5000
7
+ },
8
+ {
9
+ "script": "gitnexus-impact-reminder.py",
10
+ "timeout": 5000
7
11
  }
8
12
  ],
9
13
  "SessionStart": [
@@ -35,11 +39,6 @@
35
39
  "matcher": "Edit|Write|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
36
40
  "script": "type-safety-enforcement.py"
37
41
  },
38
- {
39
- "matcher": "Grep|Glob|Bash",
40
- "script": "gitnexus/gitnexus-hook.cjs",
41
- "timeout": 8000
42
- },
43
42
  {
44
43
  "matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
45
44
  "script": "beads-edit-gate.mjs",
@@ -56,6 +55,11 @@
56
55
  "matcher": "Bash",
57
56
  "script": "main-guard-post-push.mjs",
58
57
  "timeout": 5000
58
+ },
59
+ {
60
+ "matcher": "Read|Grep|Glob|Bash|mcp__serena__find_symbol|mcp__serena__get_symbols_overview|mcp__serena__search_for_pattern|mcp__serena__find_referencing_symbols|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol|mcp__serena__rename_symbol",
61
+ "script": "gitnexus/gitnexus-hook.cjs",
62
+ "timeout": 10000
59
63
  }
60
64
  ],
61
65
  "Stop": [
@@ -41,6 +41,17 @@
41
41
  "deepwiki": {
42
42
  "type": "http",
43
43
  "url": "https://mcp.deepwiki.com/mcp"
44
+ },
45
+ "gitnexus": {
46
+ "type": "stdio",
47
+ "command": "npx",
48
+ "args": ["-y", "gitnexus", "mcp"],
49
+ "env": {},
50
+ "_notes": {
51
+ "description": "Code intelligence MCP - provides graph queries for impact analysis, execution flows, symbol context",
52
+ "prerequisite": "Run 'gitnexus analyze' in the project first to build the index",
53
+ "auto_index": "xtrm project init runs gitnexus analyze automatically"
54
+ }
44
55
  }
45
56
  }
46
57
  }
package/hooks/README.md CHANGED
@@ -276,3 +276,75 @@ Use `xtrm install` to deploy all hooks automatically. For manual setup:
276
276
  3. Merge hook entries into `~/.claude/settings.json`.
277
277
 
278
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
336
+
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) |
343
+
344
+ ### Exit Semantics
345
+
346
+ All hooks follow strict exit semantics:
347
+ - `exit 0` — Allow the operation
348
+ - `exit 2` — Block with message shown to Claude
349
+
350
+ The core functions are pure and never call `process.exit()`. Entrypoints are responsible for side effects.
@@ -7,58 +7,32 @@
7
7
  //
8
8
  // Installed by: xtrm install
9
9
 
10
- import { readFileSync } from 'node:fs';
11
10
  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
-
11
+ readHookInput,
12
+ resolveSessionContext,
13
+ resolveClaimAndWorkState,
14
+ decideCommitGate,
15
+ } from './beads-gate-core.mjs';
16
+ import { withSafeBdContext } from './beads-gate-utils.mjs';
17
+ import { commitBlockMessage } from './beads-gate-messages.mjs';
18
+
19
+ const input = readHookInput();
20
+ if (!input) process.exit(0);
21
+
22
+ // Only intercept git commit commands
23
23
  if ((input.tool_name ?? '') !== 'Bash') process.exit(0);
24
24
  if (!/\bgit\s+commit\b/.test(input.tool_input?.command ?? '')) process.exit(0);
25
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
26
  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);
27
+ const ctx = resolveSessionContext(input);
28
+ if (!ctx || !ctx.isBeadsProject) process.exit(0);
43
29
 
44
- const ip = getInProgress(cwd);
45
- const summary = ip?.summary ?? ` Claimed: ${claimed}`;
30
+ const state = resolveClaimAndWorkState(ctx);
31
+ const decision = decideCommitGate(ctx, state);
46
32
 
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);
33
+ if (decision.allow) process.exit(0);
56
34
 
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
- }
35
+ // Block with message
36
+ process.stderr.write(commitBlockMessage(decision.summary, decision.claimed));
37
+ process.exit(2);
64
38
  });
@@ -7,66 +7,32 @@
7
7
  //
8
8
  // Installed by: xtrm install
9
9
 
10
- import { readFileSync } from 'node:fs';
11
10
  import {
12
- resolveCwd, isBeadsProject, getSessionClaim,
13
- getTotalWork, getInProgress, withSafeBdContext,
14
- } from './beads-gate-utils.mjs';
11
+ readHookInput,
12
+ resolveSessionContext,
13
+ resolveClaimAndWorkState,
14
+ decideEditGate,
15
+ } from './beads-gate-core.mjs';
16
+ import { withSafeBdContext } from './beads-gate-utils.mjs';
17
+ import { editBlockMessage, editBlockFallbackMessage } from './beads-gate-messages.mjs';
15
18
 
16
- let input;
17
- try {
18
- input = JSON.parse(readFileSync(0, 'utf8'));
19
- } catch {
20
- process.exit(0);
21
- }
19
+ const input = readHookInput();
20
+ if (!input) process.exit(0);
22
21
 
23
22
  withSafeBdContext(() => {
24
- const cwd = resolveCwd(input);
25
- if (!isBeadsProject(cwd)) process.exit(0);
23
+ const ctx = resolveSessionContext(input);
24
+ if (!ctx || !ctx.isBeadsProject) process.exit(0);
26
25
 
27
- const sessionId = input.session_id;
26
+ const state = resolveClaimAndWorkState(ctx);
27
+ const decision = decideEditGate(ctx, state);
28
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
29
+ if (decision.allow) process.exit(0);
33
30
 
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);
31
+ // Block with appropriate message
32
+ if (decision.reason === 'no_claim_with_work') {
33
+ process.stderr.write(editBlockMessage(decision.sessionId));
48
34
  } 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);
35
+ process.stderr.write(editBlockFallbackMessage());
71
36
  }
37
+ process.exit(2);
72
38
  });
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ // beads-gate-core.mjs — pure decision functions for beads gate hooks
3
+ // Import from sibling hooks using: import { ... } from './beads-gate-core.mjs';
4
+ //
5
+ // All functions are pure: they return decision objects, never call process.exit()
6
+ // or write to stdout/stderr. Side effects belong in entrypoint wrappers.
7
+ //
8
+ // Dependencies: beads-gate-utils.mjs (adapters)
9
+
10
+ import { readFileSync } from 'node:fs';
11
+ import {
12
+ resolveCwd,
13
+ isBeadsProject,
14
+ getSessionClaim,
15
+ getTotalWork,
16
+ getInProgress,
17
+ } from './beads-gate-utils.mjs';
18
+
19
+ // ── Input parsing ────────────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * Read and parse hook input from stdin. Returns null on parse error.
23
+ * Entrypoints should exit 0 if this returns null (fail-open).
24
+ */
25
+ export function readHookInput() {
26
+ try {
27
+ return JSON.parse(readFileSync(0, 'utf8'));
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ // ── Session context resolution ───────────────────────────────────────────────
34
+
35
+ /**
36
+ * Resolve session context from hook input.
37
+ * Returns { cwd, sessionId, isBeadsProject } or null if cwd can't be resolved.
38
+ */
39
+ export function resolveSessionContext(input) {
40
+ const cwd = resolveCwd(input);
41
+ if (!cwd) return null;
42
+ return {
43
+ cwd,
44
+ sessionId: input.session_id ?? null,
45
+ isBeadsProject: isBeadsProject(cwd),
46
+ };
47
+ }
48
+
49
+ // ── Claim and work state resolution ──────────────────────────────────────────
50
+
51
+ /**
52
+ * Resolve the claim and work state for a session.
53
+ * Returns { claimed, claimId, totalWork, inProgress } or null if bd unavailable.
54
+ */
55
+ export function resolveClaimAndWorkState(ctx) {
56
+ if (!ctx.isBeadsProject) {
57
+ return { claimed: false, claimId: null, totalWork: 0, inProgress: null };
58
+ }
59
+
60
+ const totalWork = getTotalWork(ctx.cwd);
61
+ if (totalWork === null) return null; // bd unavailable
62
+
63
+ const inProgress = getInProgress(ctx.cwd);
64
+
65
+ if (ctx.sessionId) {
66
+ const claimId = getSessionClaim(ctx.sessionId, ctx.cwd);
67
+ if (claimId === null) return null; // bd kv unavailable
68
+ return {
69
+ claimed: !!claimId,
70
+ claimId: claimId || null,
71
+ totalWork,
72
+ inProgress,
73
+ };
74
+ }
75
+
76
+ // No session_id: fallback to global in_progress check
77
+ return {
78
+ claimed: false,
79
+ claimId: null,
80
+ totalWork,
81
+ inProgress,
82
+ };
83
+ }
84
+
85
+ // ── Decision functions ───────────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Decide whether to allow or block an edit operation.
89
+ * Returns { allow: boolean, reason?: string, sessionId?: string }
90
+ */
91
+ export function decideEditGate(ctx, state) {
92
+ // Not a beads project → allow
93
+ if (!ctx.isBeadsProject) {
94
+ return { allow: true };
95
+ }
96
+
97
+ // bd unavailable → fail open
98
+ if (state === null) {
99
+ return { allow: true };
100
+ }
101
+
102
+ // Session has an active claim → allow
103
+ if (state.claimed) {
104
+ return { allow: true };
105
+ }
106
+
107
+ // No trackable work → allow (clean-start state)
108
+ if (state.totalWork === 0) {
109
+ return { allow: true };
110
+ }
111
+
112
+ // Has session_id but no claim + has work → block
113
+ if (ctx.sessionId) {
114
+ return {
115
+ allow: false,
116
+ reason: 'no_claim_with_work',
117
+ sessionId: ctx.sessionId,
118
+ };
119
+ }
120
+
121
+ // Fallback: no session_id, check global in_progress
122
+ if (state.inProgress && state.inProgress.count > 0) {
123
+ return { allow: true };
124
+ }
125
+
126
+ // No session_id, no in_progress, but has open work → block
127
+ if (state.totalWork > 0) {
128
+ return {
129
+ allow: false,
130
+ reason: 'no_claim_fallback',
131
+ };
132
+ }
133
+
134
+ return { allow: true };
135
+ }
136
+
137
+ /**
138
+ * Decide whether to allow or block a git commit operation.
139
+ * Returns { allow: boolean, reason?: string, summary?: string, claimed?: string }
140
+ */
141
+ export function decideCommitGate(ctx, state) {
142
+ // Not a beads project → allow
143
+ if (!ctx.isBeadsProject) {
144
+ return { allow: true };
145
+ }
146
+
147
+ // bd unavailable → fail open
148
+ if (state === null) {
149
+ return { allow: true };
150
+ }
151
+
152
+ // No active claim → allow (nothing to close)
153
+ if (!state.claimed) {
154
+ return { allow: true };
155
+ }
156
+
157
+ // Has claim but check if there are still in_progress issues
158
+ const summary = state.inProgress?.summary ?? null;
159
+
160
+ // Session has claim → block (need to close first)
161
+ return {
162
+ allow: false,
163
+ reason: 'unclosed_claim',
164
+ summary,
165
+ claimed: state.claimId,
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Decide whether to allow or block a stop operation.
171
+ * Returns { allow: boolean, reason?: string, summary?: string, claimed?: string }
172
+ */
173
+ export function decideStopGate(ctx, state) {
174
+ // Not a beads project → allow
175
+ if (!ctx.isBeadsProject) {
176
+ return { allow: true };
177
+ }
178
+
179
+ // bd unavailable → fail open
180
+ if (state === null) {
181
+ return { allow: true };
182
+ }
183
+
184
+ // No active claim → allow
185
+ if (!state.claimed) {
186
+ // But check for global in_progress (no session_id fallback)
187
+ if (!ctx.sessionId && state.inProgress && state.inProgress.count > 0) {
188
+ return {
189
+ allow: false,
190
+ reason: 'global_in_progress',
191
+ summary: state.inProgress.summary,
192
+ };
193
+ }
194
+ return { allow: true };
195
+ }
196
+
197
+ // Has claim but no in_progress issues → allow (stale claim)
198
+ if (!state.inProgress || state.inProgress.count === 0) {
199
+ return { allow: true };
200
+ }
201
+
202
+ // Has claim + in_progress issues → block
203
+ return {
204
+ allow: false,
205
+ reason: 'unclosed_claim',
206
+ summary: state.inProgress.summary,
207
+ claimed: state.claimId,
208
+ };
209
+ }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ // beads-gate-messages.mjs — centralized message templates for beads gate hooks
3
+ // Import from sibling hooks using: import { ... } from './beads-gate-messages.mjs';
4
+ //
5
+ // All user-facing strings live here. Edit this file to change messaging.
6
+ // Policy logic lives in beads-gate-core.mjs.
7
+
8
+ // ── Shared workflow steps ────────────────────────────────────────────────────
9
+
10
+ export const WORKFLOW_STEPS =
11
+ ' 1. git checkout -b feature/<name> ← start here\n' +
12
+ ' 2. bd create + bd update in_progress track your work\n' +
13
+ ' 3. Edit files / write code\n' +
14
+ ' 4. bd close <id> && git add && git commit\n' +
15
+ ' 5. git push -u origin feature/<name>\n' +
16
+ ' 6. gh pr create --fill && gh pr merge --squash\n' +
17
+ ' 7. git checkout master && git reset --hard origin/master\n';
18
+
19
+ export const SESSION_CLOSE_PROTOCOL =
20
+ ' 3. bd close <id1> <id2> ... close all in_progress issues\n' +
21
+ ' 4. git add <files> && git commit -m "..." commit your changes\n' +
22
+ ' 5. git push -u origin <feature-branch> push feature branch\n' +
23
+ ' 6. gh pr create --fill create PR\n' +
24
+ ' 7. gh pr merge --squash merge PR\n' +
25
+ ' 8. git checkout master && git reset --hard origin/master\n';
26
+
27
+ export const COMMIT_NEXT_STEPS =
28
+ ' 3. bd close <id1> <id2> ... ← you are here\n' +
29
+ ' 4. git add <files> && git commit -m "..."\n' +
30
+ ' 5. git push -u origin <feature-branch>\n' +
31
+ ' 6. gh pr create --fill && gh pr merge --squash\n' +
32
+ ' 7. git checkout master && git reset --hard origin/master\n';
33
+
34
+ // ── Edit gate messages ───────────────────────────────────────────────────────
35
+
36
+ export function editBlockMessage(sessionId) {
37
+ return (
38
+ '🚫 BEADS GATE: This session has no active claim — claim an issue before editing files.\n\n' +
39
+ ' bd update <id> --status=in_progress\n' +
40
+ ` bd kv set "claimed:${sessionId}" "<id>"\n\n` +
41
+ 'Or create a new issue:\n' +
42
+ ' bd create --title="<what you\'re doing>" --type=task --priority=2\n' +
43
+ ' bd update <id> --status=in_progress\n' +
44
+ ` bd kv set "claimed:${sessionId}" "<id>"\n`
45
+ );
46
+ }
47
+
48
+ export function editBlockFallbackMessage() {
49
+ return (
50
+ '🚫 BEADS GATE: No active issue — create one before editing files.\n\n' +
51
+ ' bd create --title="<what you\'re doing>" --type=task --priority=2\n' +
52
+ ' bd update <id> --status=in_progress\n\n' +
53
+ 'Full workflow (do this every session):\n' +
54
+ WORKFLOW_STEPS
55
+ );
56
+ }
57
+
58
+ // ── Commit gate messages ─────────────────────────────────────────────────────
59
+
60
+ export function commitBlockMessage(summary, claimed) {
61
+ const issueSummary = summary ?? ` Claimed: ${claimed}`;
62
+ return (
63
+ '🚫 BEADS GATE: Close open issues before committing.\n\n' +
64
+ `Open issues:\n${issueSummary}\n\n` +
65
+ 'Next steps:\n' + COMMIT_NEXT_STEPS
66
+ );
67
+ }
68
+
69
+ // ── Stop gate messages ───────────────────────────────────────────────────────
70
+
71
+ export function stopBlockMessage(summary, claimed) {
72
+ const issueSummary = summary ?? ` Claimed: ${claimed}`;
73
+ return (
74
+ '🚫 BEADS STOP GATE: Unresolved issues — complete the session close protocol.\n\n' +
75
+ `Open issues:\n${issueSummary}\n\n` +
76
+ 'Session close protocol:\n' + SESSION_CLOSE_PROTOCOL
77
+ );
78
+ }
79
+
80
+ // ── Memory gate messages ─────────────────────────────────────────────────────
81
+
82
+ export function memoryPromptMessage() {
83
+ return (
84
+ '🧠 MEMORY GATE: Before ending the session, evaluate this session\'s work.\n\n' +
85
+ 'For each issue you worked on and closed, ask:\n' +
86
+ ' Is this a stable pattern, key decision, or solution I\'ll encounter again?\n\n' +
87
+ ' YES → bd remember "<precise, durable insight>"\n' +
88
+ ' NO → explicitly note "nothing worth persisting" and continue\n\n' +
89
+ 'When done, signal completion and stop again:\n' +
90
+ ' touch .beads/.memory-gate-done\n'
91
+ );
92
+ }
@@ -8,18 +8,17 @@
8
8
  // Installed by: xtrm install
9
9
 
10
10
  import { execSync } from 'node:child_process';
11
- import { readFileSync, existsSync, unlinkSync } from 'node:fs';
11
+ import { existsSync, unlinkSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
+ import { readHookInput } from './beads-gate-core.mjs';
14
+ import { resolveCwd, isBeadsProject } from './beads-gate-utils.mjs';
15
+ import { memoryPromptMessage } from './beads-gate-messages.mjs';
13
16
 
14
- let input;
15
- try {
16
- input = JSON.parse(readFileSync(0, 'utf8'));
17
- } catch {
18
- process.exit(0);
19
- }
17
+ const input = readHookInput();
18
+ if (!input) process.exit(0);
20
19
 
21
- const cwd = input.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
22
- if (!existsSync(join(cwd, '.beads'))) process.exit(0);
20
+ const cwd = resolveCwd(input);
21
+ if (!cwd || !isBeadsProject(cwd)) process.exit(0);
23
22
 
24
23
  // Agent signals evaluation complete by touching this marker, then stops again
25
24
  const marker = join(cwd, '.beads', '.memory-gate-done');
@@ -44,13 +43,5 @@ try {
44
43
 
45
44
  if (!hasClosed) process.exit(0);
46
45
 
47
- process.stderr.write(
48
- '🧠 MEMORY GATE: Before ending the session, evaluate this session\'s work.\n\n' +
49
- 'For each issue you worked on and closed, ask:\n' +
50
- ' Is this a stable pattern, key decision, or solution I\'ll encounter again?\n\n' +
51
- ' YES → bd remember "<precise, durable insight>"\n' +
52
- ' NO → explicitly note "nothing worth persisting" and continue\n\n' +
53
- 'When done, signal completion and stop again:\n' +
54
- ' touch .beads/.memory-gate-done\n'
55
- );
46
+ process.stderr.write(memoryPromptMessage());
56
47
  process.exit(2);