xtrm-tools 2.1.30 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/dist/index.cjs +32 -0
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/hooks.json +7 -5
- package/config/pi/extensions/beads.ts +41 -14
- package/config/pi/extensions/core/adapter.ts +19 -0
- package/config/pi/extensions/custom-footer.ts +20 -13
- package/config/pi/extensions/main-guard.ts +3 -3
- package/config/pi/extensions/service-skills.ts +0 -32
- package/config/pi/extensions/xtrm-loader.ts +4 -4
- package/hooks/beads-claim-sync.mjs +39 -22
- package/hooks/beads-commit-gate.mjs +5 -3
- package/hooks/beads-edit-gate.mjs +6 -6
- package/hooks/beads-gate-messages.mjs +21 -17
- package/hooks/branch-state.mjs +51 -0
- package/hooks/main-guard-post-push.mjs +9 -7
- package/hooks/main-guard.mjs +6 -4
- package/package.json +1 -1
- package/project-skills/tdd-guard/.claude/skills/using-tdd-guard/SKILL.md +5 -0
package/cli/package.json
CHANGED
package/config/hooks.json
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
"script": "serena-workflow-reminder.py"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
|
+
"UserPromptSubmit": [
|
|
13
|
+
{
|
|
14
|
+
"script": "branch-state.mjs",
|
|
15
|
+
"timeout": 3000
|
|
16
|
+
}
|
|
17
|
+
],
|
|
12
18
|
"PreToolUse": [
|
|
13
19
|
{
|
|
14
20
|
"matcher": "Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
@@ -20,10 +26,6 @@
|
|
|
20
26
|
"script": "main-guard.mjs",
|
|
21
27
|
"timeout": 5000
|
|
22
28
|
},
|
|
23
|
-
{
|
|
24
|
-
"matcher": "Read|Edit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
25
|
-
"script": "serena-workflow-reminder.py"
|
|
26
|
-
},
|
|
27
29
|
{
|
|
28
30
|
"matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
29
31
|
"script": "beads-edit-gate.mjs",
|
|
@@ -47,7 +49,7 @@
|
|
|
47
49
|
"timeout": 5000
|
|
48
50
|
},
|
|
49
51
|
{
|
|
50
|
-
"matcher": "
|
|
52
|
+
"matcher": "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",
|
|
51
53
|
"script": "gitnexus/gitnexus-hook.cjs",
|
|
52
54
|
"timeout": 10000
|
|
53
55
|
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import type { ExtensionAPI, ToolCallEvent, ToolResultEvent } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { isToolCallEventType, isBashToolResult } from "@mariozechner/pi-coding-agent";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import * as fs from "node:fs";
|
|
5
4
|
import { SubprocessRunner, EventAdapter, Logger } from "./core/lib";
|
|
6
5
|
|
|
7
6
|
const logger = new Logger({ namespace: "beads" });
|
|
8
7
|
|
|
9
8
|
export default function (pi: ExtensionAPI) {
|
|
10
9
|
const getCwd = (ctx: any) => ctx.cwd || process.cwd();
|
|
11
|
-
const isBeadsProject = (cwd: string) => fs.existsSync(path.join(cwd, ".beads"));
|
|
12
10
|
|
|
13
11
|
let cachedSessionId: string | null = null;
|
|
14
12
|
|
|
@@ -28,14 +26,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
28
26
|
};
|
|
29
27
|
|
|
30
28
|
const hasTrackableWork = async (cwd: string): Promise<boolean> => {
|
|
29
|
+
const result = await SubprocessRunner.run("bd", ["list"], { cwd });
|
|
30
|
+
if (result.code === 0) {
|
|
31
|
+
const counts = EventAdapter.parseBdCounts(result.stdout);
|
|
32
|
+
if (counts) return (counts.open + counts.inProgress) > 0;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const hasInProgressWork = async (cwd: string): Promise<boolean> => {
|
|
31
38
|
const result = await SubprocessRunner.run("bd", ["list"], { cwd });
|
|
32
39
|
if (result.code === 0 && result.stdout.includes("Total:")) {
|
|
33
40
|
const m = result.stdout.match(/Total:\s*\d+\s+issues?\s*\((\d+)\s+open,\s*(\d+)\s+in progress\)/);
|
|
34
|
-
if (m)
|
|
35
|
-
const open = parseInt(m[1], 10);
|
|
36
|
-
const inProgress = parseInt(m[2], 10);
|
|
37
|
-
return (open + inProgress) > 0;
|
|
38
|
-
}
|
|
41
|
+
if (m) return parseInt(m[2], 10) > 0;
|
|
39
42
|
}
|
|
40
43
|
return false;
|
|
41
44
|
};
|
|
@@ -47,7 +50,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
47
50
|
|
|
48
51
|
pi.on("tool_call", async (event, ctx) => {
|
|
49
52
|
const cwd = getCwd(ctx);
|
|
50
|
-
if (!isBeadsProject(cwd)) return undefined;
|
|
53
|
+
if (!EventAdapter.isBeadsProject(cwd)) return undefined;
|
|
51
54
|
const sessionId = getSessionId(ctx);
|
|
52
55
|
|
|
53
56
|
if (EventAdapter.isMutatingFileTool(event)) {
|
|
@@ -60,7 +63,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
60
63
|
}
|
|
61
64
|
return {
|
|
62
65
|
block: true,
|
|
63
|
-
reason: `No active
|
|
66
|
+
reason: `No active claim for session ${sessionId}.\n bd update <id> --claim\n`,
|
|
64
67
|
};
|
|
65
68
|
}
|
|
66
69
|
}
|
|
@@ -69,12 +72,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
69
72
|
if (isToolCallEventType("bash", event)) {
|
|
70
73
|
const command = event.input.command;
|
|
71
74
|
if (command && /\bgit\s+commit\b/.test(command)) {
|
|
72
|
-
|
|
75
|
+
const claim = await getSessionClaim(sessionId, cwd);
|
|
73
76
|
if (claim) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
const inProgress = await hasInProgressWork(cwd);
|
|
78
|
+
if (inProgress) {
|
|
79
|
+
return {
|
|
80
|
+
block: true,
|
|
81
|
+
reason: `Active claim [${claim}] — close it first.\n bd close ${claim}\n git push -u origin <feature-branch>\n gh pr create --fill && gh pr merge --squash\n`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
}
|
|
@@ -108,4 +114,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
108
114
|
}
|
|
109
115
|
return undefined;
|
|
110
116
|
});
|
|
117
|
+
|
|
118
|
+
// Dual safety net: notify about unclosed claims when session ends
|
|
119
|
+
const notifySessionEnd = async (ctx: any) => {
|
|
120
|
+
const cwd = getCwd(ctx);
|
|
121
|
+
if (!EventAdapter.isBeadsProject(cwd)) return;
|
|
122
|
+
const sessionId = getSessionId(ctx);
|
|
123
|
+
const claim = await getSessionClaim(sessionId, cwd);
|
|
124
|
+
if (claim && ctx.hasUI) {
|
|
125
|
+
ctx.ui.notify(`Beads: Session ending with active claim [${claim}]`, "warning");
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
130
|
+
await notifySessionEnd(ctx);
|
|
131
|
+
return undefined;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
135
|
+
await notifySessionEnd(ctx);
|
|
136
|
+
return undefined;
|
|
137
|
+
});
|
|
111
138
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as nodePath from "node:path";
|
|
3
|
+
|
|
1
4
|
import type { ExtensionAPI, ToolCallEvent } from "@mariozechner/pi-coding-agent";
|
|
2
5
|
|
|
3
6
|
export class EventAdapter {
|
|
@@ -42,4 +45,20 @@ export class EventAdapter {
|
|
|
42
45
|
static formatBlockReason(prefix: string, details: string): string {
|
|
43
46
|
return `${prefix}: ${details}`;
|
|
44
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns true if the given directory is a beads project (has a .beads directory).
|
|
50
|
+
*/
|
|
51
|
+
static isBeadsProject(cwd: string): boolean {
|
|
52
|
+
return fs.existsSync(nodePath.join(cwd, ".beads"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parses the summary line from `bd list` output.
|
|
57
|
+
* Returns { open, inProgress } or null if the line is absent.
|
|
58
|
+
*/
|
|
59
|
+
static parseBdCounts(output: string): { open: number; inProgress: number } | null {
|
|
60
|
+
const m = output.match(/Total:\s*\d+\s+issues?\s*\((\d+)\s+open,\s*(\d+)\s+in progress\)/);
|
|
61
|
+
if (!m) return null;
|
|
62
|
+
return { open: parseInt(m[1], 10), inProgress: parseInt(m[2], 10) };
|
|
63
|
+
}
|
|
45
64
|
}
|
|
@@ -7,9 +7,7 @@
|
|
|
7
7
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
8
|
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
9
9
|
|
|
10
|
-
import
|
|
11
|
-
import * as fs from "node:fs";
|
|
12
|
-
import { SubprocessRunner } from "./core/lib";
|
|
10
|
+
import { SubprocessRunner, EventAdapter } from "./core/lib";
|
|
13
11
|
|
|
14
12
|
export default function (pi: ExtensionAPI) {
|
|
15
13
|
interface BeadState {
|
|
@@ -26,10 +24,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
26
24
|
blocked: "●",
|
|
27
25
|
closed: "✓",
|
|
28
26
|
};
|
|
27
|
+
// Chip background colours (raw ANSI — theme has no bg() API)
|
|
28
|
+
const CHIP_BG_NEUTRAL = "\x1b[48;5;238m"; // dark gray
|
|
29
|
+
const CHIP_BG_ACTIVE = "\x1b[48;5;39m"; // blue
|
|
30
|
+
const CHIP_BG_BLOCKED = "\x1b[48;5;88m"; // red
|
|
31
|
+
const CHIP_FG = "\x1b[38;5;15m"; // white
|
|
32
|
+
const CHIP_RESET = "\x1b[0m";
|
|
33
|
+
const chip = (text: string, bg = CHIP_BG_NEUTRAL): string =>
|
|
34
|
+
`${bg}${CHIP_FG} ${text} ${CHIP_RESET}`;
|
|
35
|
+
|
|
29
36
|
const STATUS_BG: Record<string, string> = {
|
|
30
|
-
open:
|
|
31
|
-
in_progress:
|
|
32
|
-
blocked:
|
|
37
|
+
open: CHIP_BG_NEUTRAL,
|
|
38
|
+
in_progress: CHIP_BG_ACTIVE,
|
|
39
|
+
blocked: CHIP_BG_BLOCKED,
|
|
33
40
|
};
|
|
34
41
|
|
|
35
42
|
let capturedCtx: any = null;
|
|
@@ -40,13 +47,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
40
47
|
const CACHE_TTL = 5000;
|
|
41
48
|
|
|
42
49
|
const getCwd = () => capturedCtx?.cwd || process.cwd();
|
|
43
|
-
const isBeadsProject = (cwd: string) => fs.existsSync(path.join(cwd, ".beads"));
|
|
44
50
|
const getShortId = (id: string) => id.split("-").pop() ?? id;
|
|
45
51
|
|
|
46
52
|
const refreshBeadState = async () => {
|
|
47
53
|
if (refreshing || Date.now() - beadState.lastFetch < CACHE_TTL) return;
|
|
48
54
|
const cwd = getCwd();
|
|
49
|
-
if (!isBeadsProject(cwd)) return;
|
|
55
|
+
if (!EventAdapter.isBeadsProject(cwd)) return;
|
|
50
56
|
if (!sessionId) return;
|
|
51
57
|
refreshing = true;
|
|
52
58
|
try {
|
|
@@ -84,11 +90,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
84
90
|
const { claimId, shortId, status, openCount } = beadState;
|
|
85
91
|
if (claimId && shortId && status) {
|
|
86
92
|
const icon = STATUS_ICONS[status] ?? "?";
|
|
87
|
-
const bg = STATUS_BG[status] ??
|
|
88
|
-
return
|
|
93
|
+
const bg = STATUS_BG[status] ?? CHIP_BG_NEUTRAL;
|
|
94
|
+
return chip(`bd:${shortId}${icon}`, bg);
|
|
89
95
|
}
|
|
90
96
|
if (openCount > 0) {
|
|
91
|
-
return
|
|
97
|
+
return chip(`bd:${openCount}${STATUS_ICONS.open}`);
|
|
92
98
|
}
|
|
93
99
|
return "";
|
|
94
100
|
};
|
|
@@ -108,7 +114,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
108
114
|
render(width: number): string[] {
|
|
109
115
|
refreshBeadState().catch(() => {});
|
|
110
116
|
|
|
111
|
-
const
|
|
117
|
+
const BOLD = "\x1b[1m", BOLD_OFF = "\x1b[22m";
|
|
118
|
+
const brand = `${BOLD}${theme.fg("accent", "XTRM")}${BOLD_OFF}`;
|
|
112
119
|
|
|
113
120
|
const usage = ctx.getContextUsage();
|
|
114
121
|
const pct = usage?.percent ?? 0;
|
|
@@ -123,7 +130,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
123
130
|
const branchStr = branch ? theme.fg("accent", `⎇ ${branch}`) : "";
|
|
124
131
|
|
|
125
132
|
const modelId = ctx.model?.id || "no-model";
|
|
126
|
-
const modelChip =
|
|
133
|
+
const modelChip = chip(modelId);
|
|
127
134
|
|
|
128
135
|
const sep = theme.fg("dim", " | ");
|
|
129
136
|
|
|
@@ -54,7 +54,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
54
54
|
if (branch && protectedBranches.includes(branch)) {
|
|
55
55
|
// A. Mutating File Tools on Main
|
|
56
56
|
if (EventAdapter.isMutatingFileTool(event)) {
|
|
57
|
-
const reason = `On protected branch '${branch}'
|
|
57
|
+
const reason = `On protected branch '${branch}' — start on a feature branch and claim an issue.\n git checkout -b feature/<name>\n bd update <id> --claim\n`;
|
|
58
58
|
if (ctx.hasUI) {
|
|
59
59
|
ctx.ui.notify(`Main-Guard: Blocked edit on ${branch}`, "error");
|
|
60
60
|
}
|
|
@@ -97,7 +97,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
97
97
|
|
|
98
98
|
// Specific blocks
|
|
99
99
|
if (/\bgit\s+commit\b/.test(cmd)) {
|
|
100
|
-
return { block: true, reason: `No commits on '${branch}' — use a feature branch
|
|
100
|
+
return { block: true, reason: `No commits on '${branch}' — use a feature branch.\n git checkout -b feature/<name>\n bd update <id> --claim\n` };
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
if (/\bgit\s+push\b/.test(cmd)) {
|
|
@@ -113,7 +113,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
// Default deny
|
|
116
|
-
const reason = `Bash restricted on '${branch}'. Allowed: git status/log/diff/pull/stash, gh, bd.\n Exit: git checkout -b feature/<name
|
|
116
|
+
const reason = `Bash restricted on '${branch}'. Allowed: git status/log/diff/pull/stash, gh, bd.\n Exit: git checkout -b feature/<name>\n Then: bd update <id> --claim\n Override: MAIN_GUARD_ALLOW_BASH=1 <cmd>\n`;
|
|
117
117
|
if (ctx.hasUI) {
|
|
118
118
|
ctx.ui.notify("Main-Guard: Command blocked", "error");
|
|
119
119
|
}
|
|
@@ -26,38 +26,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
26
26
|
return undefined;
|
|
27
27
|
});
|
|
28
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
29
|
|
|
62
30
|
// 3. Drift Detection
|
|
63
31
|
pi.on("tool_result", async (event, ctx) => {
|
|
@@ -40,7 +40,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
40
40
|
|
|
41
41
|
for (const p of roadmapPaths) {
|
|
42
42
|
if (fs.existsSync(p)) {
|
|
43
|
-
const content = fs.
|
|
43
|
+
const content = await fs.promises.readFile(p, "utf8");
|
|
44
44
|
contextParts.push(`## Project Roadmap & Architecture (${path.relative(cwd, p)})\n\n${content}`);
|
|
45
45
|
break; // Only load the first one found
|
|
46
46
|
}
|
|
@@ -51,10 +51,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
51
51
|
if (fs.existsSync(rulesDir)) {
|
|
52
52
|
const ruleFiles = findMarkdownFiles(rulesDir);
|
|
53
53
|
if (ruleFiles.length > 0) {
|
|
54
|
-
const rulesContent = ruleFiles.map(f => {
|
|
55
|
-
const content = fs.
|
|
54
|
+
const rulesContent = (await Promise.all(ruleFiles.map(async f => {
|
|
55
|
+
const content = await fs.promises.readFile(path.join(rulesDir, f), "utf8");
|
|
56
56
|
return `### Rule: ${f}\n${content}`;
|
|
57
|
-
}).join("\n\n");
|
|
57
|
+
}))).join("\n\n");
|
|
58
58
|
contextParts.push(`## Project Rules\n\n${rulesContent}`);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// beads-claim-sync — PostToolUse hook
|
|
3
|
-
// Auto-sets
|
|
3
|
+
// Auto-sets kv claim on bd update --claim; auto-clears on bd close.
|
|
4
4
|
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
6
|
import { readFileSync, existsSync } from 'node:fs';
|
|
@@ -39,20 +39,11 @@ function main() {
|
|
|
39
39
|
const input = readInput();
|
|
40
40
|
if (!input || input.hook_event_name !== 'PostToolUse') process.exit(0);
|
|
41
41
|
if (!isShellTool(input.tool_name)) process.exit(0);
|
|
42
|
-
if (!commandSucceeded(input)) process.exit(0);
|
|
43
42
|
|
|
44
43
|
const cwd = input.cwd || process.cwd();
|
|
45
44
|
if (!isBeadsProject(cwd)) process.exit(0);
|
|
46
45
|
|
|
47
46
|
const command = input.tool_input?.command || '';
|
|
48
|
-
if (!/\bbd\s+update\b/.test(command) || !/--claim\b/.test(command)) {
|
|
49
|
-
process.exit(0);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const match = command.match(/\bbd\s+update\s+(\S+)/);
|
|
53
|
-
if (!match) process.exit(0);
|
|
54
|
-
|
|
55
|
-
const issueId = match[1];
|
|
56
47
|
const sessionId = input.session_id ?? input.sessionId;
|
|
57
48
|
|
|
58
49
|
if (!sessionId) {
|
|
@@ -60,22 +51,48 @@ function main() {
|
|
|
60
51
|
process.exit(0);
|
|
61
52
|
}
|
|
62
53
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
54
|
+
// Auto-claim: bd update <id> --claim (fire regardless of exit code — bd returns 1 for "already in_progress")
|
|
55
|
+
if (/\bbd\s+update\b/.test(command) && /--claim\b/.test(command)) {
|
|
56
|
+
const match = command.match(/\bbd\s+update\s+(\S+)/);
|
|
57
|
+
if (match) {
|
|
58
|
+
const issueId = match[1];
|
|
59
|
+
const result = spawnSync('bd', ['kv', 'set', `claimed:${sessionId}`, issueId], {
|
|
60
|
+
cwd,
|
|
61
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
62
|
+
timeout: 5000,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (result.status !== 0) {
|
|
66
|
+
const err = (result.stderr || result.stdout || '').toString().trim();
|
|
67
|
+
if (err) process.stderr.write(`Beads claim sync warning: ${err}\n`);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
process.stdout.write(JSON.stringify({
|
|
72
|
+
additionalContext: `\n✅ **Beads**: Session \`${sessionId}\` claimed issue \`${issueId}\`.`,
|
|
73
|
+
}));
|
|
74
|
+
process.stdout.write('\n');
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
// Auto-clear: bd close <id> — remove the kv claim so commit gate unblocks
|
|
80
|
+
if (/\bbd\s+close\b/.test(command) && commandSucceeded(input)) {
|
|
81
|
+
const result = spawnSync('bd', ['kv', 'clear', `claimed:${sessionId}`], {
|
|
82
|
+
cwd,
|
|
83
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
84
|
+
timeout: 5000,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (result.status === 0) {
|
|
88
|
+
process.stdout.write(JSON.stringify({
|
|
89
|
+
additionalContext: `\n🔓 **Beads**: Session claim cleared. Ready to commit.`,
|
|
90
|
+
}));
|
|
91
|
+
process.stdout.write('\n');
|
|
92
|
+
}
|
|
72
93
|
process.exit(0);
|
|
73
94
|
}
|
|
74
95
|
|
|
75
|
-
process.stdout.write(JSON.stringify({
|
|
76
|
-
additionalContext: `\n✅ **Beads**: Session \`${sessionId}\` claimed issue \`${issueId}\`.`,
|
|
77
|
-
}));
|
|
78
|
-
process.stdout.write('\n');
|
|
79
96
|
process.exit(0);
|
|
80
97
|
}
|
|
81
98
|
|
|
@@ -32,7 +32,9 @@ withSafeBdContext(() => {
|
|
|
32
32
|
|
|
33
33
|
if (decision.allow) process.exit(0);
|
|
34
34
|
|
|
35
|
-
// Block with
|
|
36
|
-
|
|
37
|
-
process.
|
|
35
|
+
// Block with structured decision
|
|
36
|
+
const reason = commitBlockMessage(decision.summary, decision.claimed);
|
|
37
|
+
process.stdout.write(JSON.stringify({ decision: 'block', reason }));
|
|
38
|
+
process.stdout.write('\n');
|
|
39
|
+
process.exit(0);
|
|
38
40
|
});
|
|
@@ -29,10 +29,10 @@ withSafeBdContext(() => {
|
|
|
29
29
|
if (decision.allow) process.exit(0);
|
|
30
30
|
|
|
31
31
|
// Block with appropriate message
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
process.exit(
|
|
32
|
+
const reason = decision.reason === 'no_claim_with_work'
|
|
33
|
+
? editBlockMessage(decision.sessionId)
|
|
34
|
+
: editBlockFallbackMessage();
|
|
35
|
+
process.stdout.write(JSON.stringify({ decision: 'block', reason }));
|
|
36
|
+
process.stdout.write('\n');
|
|
37
|
+
process.exit(0);
|
|
38
38
|
});
|
|
@@ -5,32 +5,36 @@
|
|
|
5
5
|
// All user-facing strings live here. Edit this file to change messaging.
|
|
6
6
|
// Policy logic lives in beads-gate-core.mjs.
|
|
7
7
|
|
|
8
|
-
// ── Shared workflow steps
|
|
8
|
+
// ── Shared workflow steps ────────────────────────────────────────────
|
|
9
9
|
|
|
10
10
|
export const WORKFLOW_STEPS =
|
|
11
11
|
' 1. git checkout -b feature/<name>\n' +
|
|
12
|
-
' 2. bd
|
|
12
|
+
' 2. bd update <id> --claim\n' +
|
|
13
13
|
' 3. Edit / write code\n' +
|
|
14
|
-
' 4. bd close <id
|
|
15
|
-
' 5. git
|
|
16
|
-
' 6.
|
|
17
|
-
' 7.
|
|
14
|
+
' 4. bd close <id>\n' +
|
|
15
|
+
' 5. git add -p && git commit -m "<message>"\n' +
|
|
16
|
+
' 6. git push -u origin feature/<name>\n' +
|
|
17
|
+
' 7. gh pr create --fill && gh pr merge --squash\n' +
|
|
18
|
+
' 8. git checkout master && git reset --hard origin/master\n';
|
|
18
19
|
|
|
19
20
|
export const SESSION_CLOSE_PROTOCOL =
|
|
20
|
-
' bd close <id
|
|
21
|
+
' bd close <id>\n' +
|
|
22
|
+
' git add -p && git commit -m "<message>"\n' +
|
|
23
|
+
' git push -u origin <feature-branch>\n' +
|
|
24
|
+
' gh pr create --fill && gh pr merge --squash\n';
|
|
21
25
|
|
|
22
26
|
export const COMMIT_NEXT_STEPS =
|
|
23
|
-
' bd close <id
|
|
27
|
+
' bd close <id>\n' +
|
|
28
|
+
' git add -p && git commit -m "<message>"\n' +
|
|
24
29
|
' git push -u origin <feature-branch>\n' +
|
|
25
|
-
' gh pr create --fill && gh pr merge --squash\n'
|
|
30
|
+
' gh pr create --fill && gh pr merge --squash\n';
|
|
26
31
|
|
|
27
|
-
// ── Edit gate messages
|
|
32
|
+
// ── Edit gate messages ───────────────────────────────────────────
|
|
28
33
|
|
|
29
34
|
export function editBlockMessage(sessionId) {
|
|
30
35
|
return (
|
|
31
36
|
'🚫 No active claim — claim an issue first.\n' +
|
|
32
|
-
' bd update <id> --
|
|
33
|
-
` bd kv set "claimed:${sessionId}" "<id>"\n`
|
|
37
|
+
' bd update <id> --claim\n'
|
|
34
38
|
);
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -38,11 +42,11 @@ export function editBlockFallbackMessage() {
|
|
|
38
42
|
return (
|
|
39
43
|
'🚫 No active issue — create one before editing.\n' +
|
|
40
44
|
' bd create --title="<task>" --type=task --priority=2\n' +
|
|
41
|
-
' bd update <id> --
|
|
45
|
+
' bd update <id> --claim\n'
|
|
42
46
|
);
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
// ── Commit gate messages
|
|
49
|
+
// ── Commit gate messages ─────────────────────────────────────────
|
|
46
50
|
|
|
47
51
|
export function commitBlockMessage(summary, claimed) {
|
|
48
52
|
const issueSummary = summary ?? ` Claimed: ${claimed}`;
|
|
@@ -53,18 +57,18 @@ export function commitBlockMessage(summary, claimed) {
|
|
|
53
57
|
);
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
// ── Stop gate messages
|
|
60
|
+
// ── Stop gate messages ───────────────────────────────────────────
|
|
57
61
|
|
|
58
62
|
export function stopBlockMessage(summary, claimed) {
|
|
59
63
|
const issueSummary = summary ?? ` Claimed: ${claimed}`;
|
|
60
64
|
return (
|
|
61
65
|
'🚫 Unresolved issues — close before stopping.\n\n' +
|
|
62
66
|
`${issueSummary}\n\n` +
|
|
63
|
-
SESSION_CLOSE_PROTOCOL
|
|
67
|
+
'Next steps:\n' + SESSION_CLOSE_PROTOCOL
|
|
64
68
|
);
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
// ── Memory gate messages
|
|
71
|
+
// ── Memory gate messages ─────────────────────────────────────────
|
|
68
72
|
|
|
69
73
|
export function memoryPromptMessage() {
|
|
70
74
|
return (
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// branch-state.mjs — UserPromptSubmit hook
|
|
3
|
+
// Re-injects current git branch and active beads claim at each prompt.
|
|
4
|
+
// Keeps the agent oriented after /compact or long sessions.
|
|
5
|
+
// Output: { hookSpecificOutput: { additionalSystemPrompt } }
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
function readInput() {
|
|
12
|
+
try { return JSON.parse(readFileSync(0, 'utf-8')); } catch { return null; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getBranch(cwd) {
|
|
16
|
+
try {
|
|
17
|
+
return execSync('git branch --show-current', {
|
|
18
|
+
encoding: 'utf8', cwd,
|
|
19
|
+
stdio: ['pipe', 'pipe', 'pipe'], timeout: 3000,
|
|
20
|
+
}).trim() || null;
|
|
21
|
+
} catch { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getSessionClaim(sessionId, cwd) {
|
|
25
|
+
try {
|
|
26
|
+
const out = execSync(`bd kv get "claimed:${sessionId}"`, {
|
|
27
|
+
encoding: 'utf8', cwd,
|
|
28
|
+
stdio: ['pipe', 'pipe', 'pipe'], timeout: 3000,
|
|
29
|
+
}).trim();
|
|
30
|
+
return out || null;
|
|
31
|
+
} catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const input = readInput();
|
|
35
|
+
if (!input) process.exit(0);
|
|
36
|
+
|
|
37
|
+
const cwd = input.cwd || process.cwd();
|
|
38
|
+
const sessionId = input.session_id ?? input.sessionId;
|
|
39
|
+
const branch = getBranch(cwd);
|
|
40
|
+
const isBeads = existsSync(join(cwd, '.beads'));
|
|
41
|
+
const claim = isBeads && sessionId ? getSessionClaim(sessionId, cwd) : null;
|
|
42
|
+
|
|
43
|
+
if (!branch && !claim) process.exit(0);
|
|
44
|
+
|
|
45
|
+
const context = `[Context: branch=${branch ?? 'unknown'}${claim ? ', claim=' + claim : ''}]`;
|
|
46
|
+
|
|
47
|
+
process.stdout.write(JSON.stringify({
|
|
48
|
+
hookSpecificOutput: { additionalSystemPrompt: context },
|
|
49
|
+
}));
|
|
50
|
+
process.stdout.write('\n');
|
|
51
|
+
process.exit(0);
|
|
@@ -59,11 +59,13 @@ const explicitlyProtectedTarget = protectedBranches
|
|
|
59
59
|
.some((b) => lastToken === b || lastToken.endsWith(`:${b}`));
|
|
60
60
|
if (explicitlyProtectedTarget) process.exit(0);
|
|
61
61
|
|
|
62
|
-
process.stdout.write(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
62
|
+
process.stdout.write(JSON.stringify({
|
|
63
|
+
additionalContext:
|
|
64
|
+
`✅ Pushed '${branch}'. Next steps:\n` +
|
|
65
|
+
' 1. gh pr create --fill\n' +
|
|
66
|
+
' 2. gh pr merge --squash\n' +
|
|
67
|
+
' 3. git checkout main && git reset --hard origin/main\n' +
|
|
68
|
+
'Ensure beads state is updated (e.g. bd close <id>).',
|
|
69
|
+
}));
|
|
70
|
+
process.stdout.write('\n');
|
|
69
71
|
process.exit(0);
|
package/hooks/main-guard.mjs
CHANGED
|
@@ -37,10 +37,11 @@ const hookEventName = input.hook_event_name ?? 'PreToolUse';
|
|
|
37
37
|
|
|
38
38
|
function deny(reason) {
|
|
39
39
|
process.stdout.write(JSON.stringify({
|
|
40
|
-
|
|
40
|
+
decision: 'block',
|
|
41
|
+
reason,
|
|
41
42
|
}));
|
|
42
43
|
process.stdout.write('\n');
|
|
43
|
-
process.exit(
|
|
44
|
+
process.exit(0);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
const WRITE_TOOLS = new Set([
|
|
@@ -55,8 +56,9 @@ const WRITE_TOOLS = new Set([
|
|
|
55
56
|
]);
|
|
56
57
|
|
|
57
58
|
if (WRITE_TOOLS.has(tool)) {
|
|
58
|
-
deny(`⛔ On '${branch}' —
|
|
59
|
-
+ ' git checkout -b feature/<name>\n'
|
|
59
|
+
deny(`⛔ On '${branch}' — start on a feature branch and claim an issue.\n`
|
|
60
|
+
+ ' git checkout -b feature/<name>\n'
|
|
61
|
+
+ ' bd update <id> --claim\n');
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
const WORKFLOW =
|