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/dist/index.cjs +40 -0
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/hooks.json +9 -5
- package/config/mcp_servers.json +11 -0
- package/hooks/README.md +72 -0
- package/hooks/beads-commit-gate.mjs +20 -46
- package/hooks/beads-edit-gate.mjs +19 -53
- package/hooks/beads-gate-core.mjs +209 -0
- package/hooks/beads-gate-messages.mjs +92 -0
- package/hooks/beads-memory-gate.mjs +9 -18
- package/hooks/beads-stop-gate.mjs +17 -46
- package/hooks/gitnexus/gitnexus-hook.cjs +222 -133
- package/hooks/main-guard.mjs +20 -0
- package/package.json +1 -1
- package/skills/using-xtrm/SKILL.md +271 -0
package/cli/package.json
CHANGED
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": [
|
package/config/mcp_servers.json
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
35
|
-
if (!isBeadsProject
|
|
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
|
-
|
|
45
|
-
|
|
30
|
+
const state = resolveClaimAndWorkState(ctx);
|
|
31
|
+
const decision = decideCommitGate(ctx, state);
|
|
46
32
|
|
|
47
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
25
|
-
if (!isBeadsProject
|
|
23
|
+
const ctx = resolveSessionContext(input);
|
|
24
|
+
if (!ctx || !ctx.isBeadsProject) process.exit(0);
|
|
26
25
|
|
|
27
|
-
const
|
|
26
|
+
const state = resolveClaimAndWorkState(ctx);
|
|
27
|
+
const decision = decideEditGate(ctx, state);
|
|
28
28
|
|
|
29
|
-
if (
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
22
|
-
if (!
|
|
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);
|