xtrm-tools 0.5.26 → 0.5.28

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": "0.5.26",
3
+ "version": "0.5.28",
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
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "hooks": {
3
3
  "SessionStart": [
4
+ {
5
+ "script": "using-xtrm-reminder.mjs"
6
+ },
4
7
  {
5
8
  "script": "beads-compact-restore.mjs",
6
9
  "timeout": 5000
7
10
  },
8
11
  {
9
- "script": "xtrm-session-logger.mjs",
10
- "timeout": 3000
11
- }
12
- ],
13
- "UserPromptSubmit": [
12
+ "script": "quality-check-env.mjs",
13
+ "timeout": 5000
14
+ },
14
15
  {
15
- "script": "branch-state.mjs",
16
+ "script": "xtrm-session-logger.mjs",
16
17
  "timeout": 3000
17
18
  }
18
19
  ],
@@ -40,17 +41,17 @@
40
41
  "timeout": 5000
41
42
  },
42
43
  {
43
- "matcher": "Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
44
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
44
45
  "script": "quality-check.cjs",
45
46
  "timeout": 30000
46
47
  },
47
48
  {
48
- "matcher": "Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
49
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
49
50
  "script": "quality-check.py",
50
51
  "timeout": 30000
51
52
  },
52
53
  {
53
- "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",
54
+ "matcher": "Bash|Grep|Read|Glob|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",
54
55
  "script": "gitnexus/gitnexus-hook.cjs",
55
56
  "timeout": 10000
56
57
  },
@@ -77,6 +78,6 @@
77
78
  ]
78
79
  },
79
80
  "statusLine": {
80
- "script": "statusline-starship.sh"
81
+ "script": "statusline.mjs"
81
82
  }
82
83
  }
@@ -1,8 +1,8 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { isToolCallEventType, isBashToolResult } from "@mariozechner/pi-coding-agent";
3
- import { SubprocessRunner, EventAdapter, Logger } from "../core/lib";
4
-
5
- const logger = new Logger({ namespace: "beads" });
3
+ import { existsSync, unlinkSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { SubprocessRunner, EventAdapter } from "../core/lib";
6
6
 
7
7
  export default function (pi: ExtensionAPI) {
8
8
  const getCwd = (ctx: any) => ctx.cwd || process.cwd();
@@ -21,8 +21,51 @@ export default function (pi: ExtensionAPI) {
21
21
 
22
22
  const getSessionClaim = async (sessionId: string, cwd: string): Promise<string | null> => {
23
23
  const result = await SubprocessRunner.run("bd", ["kv", "get", `claimed:${sessionId}`], { cwd });
24
- if (result.code === 0) return result.stdout.trim();
25
- return null;
24
+ if (result.code !== 0) return null;
25
+ const claim = result.stdout.trim();
26
+ return claim.length > 0 ? claim : null;
27
+ };
28
+
29
+ const clearClaimMarker = async (sessionId: string, cwd: string) => {
30
+ await SubprocessRunner.run("bd", ["kv", "clear", `claimed:${sessionId}`], { cwd });
31
+ };
32
+
33
+ const isIssueInProgress = async (issueId: string, cwd: string): Promise<boolean | null> => {
34
+ const result = await SubprocessRunner.run("bd", ["show", issueId, "--json"], { cwd });
35
+ if (result.code !== 0 || !result.stdout.trim()) return null;
36
+ try {
37
+ const parsed = JSON.parse(result.stdout);
38
+ const issue = Array.isArray(parsed) ? parsed[0] : parsed;
39
+ if (!issue?.status) return null;
40
+ return issue.status === "in_progress";
41
+ } catch {
42
+ return null;
43
+ }
44
+ };
45
+
46
+ const getActiveClaim = async (sessionId: string, cwd: string): Promise<string | null> => {
47
+ const claim = await getSessionClaim(sessionId, cwd);
48
+ if (!claim) return null;
49
+
50
+ const inProgress = await isIssueInProgress(claim, cwd);
51
+ if (inProgress === false) {
52
+ await clearClaimMarker(sessionId, cwd);
53
+ return null;
54
+ }
55
+
56
+ return claim;
57
+ };
58
+
59
+ const getClosedThisSession = async (sessionId: string, cwd: string): Promise<string | null> => {
60
+ const result = await SubprocessRunner.run("bd", ["kv", "get", `closed-this-session:${sessionId}`], { cwd });
61
+ if (result.code !== 0) return null;
62
+ const issue = result.stdout.trim();
63
+ return issue.length > 0 ? issue : null;
64
+ };
65
+
66
+ const clearSessionMarkers = async (sessionId: string, cwd: string) => {
67
+ await SubprocessRunner.run("bd", ["kv", "clear", `claimed:${sessionId}`], { cwd });
68
+ await SubprocessRunner.run("bd", ["kv", "clear", `closed-this-session:${sessionId}`], { cwd });
26
69
  };
27
70
 
28
71
  const hasTrackableWork = async (cwd: string): Promise<boolean> => {
@@ -34,15 +77,6 @@ export default function (pi: ExtensionAPI) {
34
77
  return false;
35
78
  };
36
79
 
37
- const hasInProgressWork = async (cwd: string): Promise<boolean> => {
38
- const result = await SubprocessRunner.run("bd", ["list"], { cwd });
39
- if (result.code === 0 && result.stdout.includes("Total:")) {
40
- const m = result.stdout.match(/Total:\s*\d+\s+issues?\s*\((\d+)\s+open,\s*(\d+)\s+in progress\)/);
41
- if (m) return parseInt(m[2], 10) > 0;
42
- }
43
- return false;
44
- };
45
-
46
80
  pi.on("session_start", async (_event, ctx) => {
47
81
  cachedSessionId = ctx?.sessionManager?.getSessionId?.() ?? ctx?.sessionId ?? ctx?.session_id ?? cachedSessionId;
48
82
  return undefined;
@@ -54,33 +88,30 @@ export default function (pi: ExtensionAPI) {
54
88
  const sessionId = getSessionId(ctx);
55
89
 
56
90
  if (EventAdapter.isMutatingFileTool(event)) {
57
- const claim = await getSessionClaim(sessionId, cwd);
91
+ const claim = await getActiveClaim(sessionId, cwd);
58
92
  if (!claim) {
59
- const hasWork = await hasTrackableWork(cwd);
60
- if (hasWork) {
61
- if (ctx.hasUI) {
62
- ctx.ui.notify("Beads: Edit blocked. Claim an issue first.", "warning");
63
- }
64
- return {
65
- block: true,
66
- reason: `No active claim for session ${sessionId}.\n bd update <id> --claim\n`,
67
- };
68
- }
69
- }
93
+ const hasWork = await hasTrackableWork(cwd);
94
+ if (hasWork) {
95
+ if (ctx.hasUI) {
96
+ ctx.ui.notify("Beads: Edit blocked. Claim an issue first.", "warning");
97
+ }
98
+ return {
99
+ block: true,
100
+ reason: `No active claim for session ${sessionId}.\n bd update <id> --claim\n`,
101
+ };
102
+ }
103
+ }
70
104
  }
71
105
 
72
106
  if (isToolCallEventType("bash", event)) {
73
107
  const command = event.input.command;
74
108
  if (command && /\bgit\s+commit\b/.test(command)) {
75
- const claim = await getSessionClaim(sessionId, cwd);
109
+ const claim = await getActiveClaim(sessionId, cwd);
76
110
  if (claim) {
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 (Pi workflow) publish/merge are external steps; do not rely on xtrm finish.\n`,
82
- };
83
- }
111
+ return {
112
+ block: true,
113
+ reason: `Active claim [${claim}] — close it first.\n bd close ${claim}\n (Pi workflow) publish/merge are external steps; do not rely on xtrm finish.\n`,
114
+ };
84
115
  }
85
116
  }
86
117
  }
@@ -89,65 +120,61 @@ export default function (pi: ExtensionAPI) {
89
120
  });
90
121
 
91
122
  pi.on("tool_result", async (event, ctx) => {
92
- if (isBashToolResult(event)) {
93
- const command = event.input.command;
94
- const sessionId = getSessionId(ctx);
95
-
96
- // Auto-claim on bd update --claim regardless of exit code.
97
- // bd returns exit 1 with "already in_progress" when status unchanged — still a valid claim intent.
98
- if (command && /\bbd\s+update\b/.test(command) && /--claim\b/.test(command)) {
99
- const issueMatch = command.match(/\bbd\s+update\s+(\S+)/);
100
- if (issueMatch) {
101
- const issueId = issueMatch[1];
102
- const cwd = getCwd(ctx);
103
- await SubprocessRunner.run("bd", ["kv", "set", `claimed:${sessionId}`, issueId], { cwd });
104
- const claimNotice = `\n\n✅ **Beads**: Session \`${sessionId}\` claimed issue \`${issueId}\`. File edits are now unblocked.`;
105
- return { content: [...event.content, { type: "text", text: claimNotice }] };
106
- }
123
+ if (!isBashToolResult(event)) return undefined;
124
+
125
+ const command = event.input.command || "";
126
+ const sessionId = getSessionId(ctx);
127
+ const cwd = getCwd(ctx);
128
+
129
+ // Auto-claim on bd update --claim regardless of exit code.
130
+ if (/\bbd\s+update\b/.test(command) && /--claim\b/.test(command)) {
131
+ const issueMatch = command.match(/\bbd\s+update\s+(\S+)/);
132
+ if (issueMatch) {
133
+ const issueId = issueMatch[1];
134
+ await SubprocessRunner.run("bd", ["kv", "set", `claimed:${sessionId}`, issueId], { cwd });
135
+ memoryGateFired = false;
136
+ const claimNotice = `\n\n✅ **Beads**: Session \`${sessionId}\` claimed issue \`${issueId}\`. File edits are now unblocked.`;
137
+ return { content: [...event.content, { type: "text", text: claimNotice }] };
107
138
  }
139
+ }
108
140
 
109
- if (command && /\bbd\s+close\b/.test(command) && !event.isError) {
110
- const reminder = "\n\n**Beads Insight**: Work completed. Consider if this session produced insights worth persisting via `bd remember`.";
111
- const newContent = [...event.content, { type: "text", text: reminder }];
112
- return { content: newContent };
141
+ if (/\bbd\s+close\b/.test(command) && !event.isError) {
142
+ const closeMatch = command.match(/\bbd\s+close\s+(\S+)/);
143
+ const closedIssueId = closeMatch?.[1] ?? null;
144
+ if (closedIssueId) {
145
+ await SubprocessRunner.run("bd", ["kv", "set", `closed-this-session:${sessionId}`, closedIssueId], { cwd });
146
+ memoryGateFired = false;
113
147
  }
148
+ const reminder = "\n\n**Beads Insight**: Work completed. Consider if this session produced insights worth persisting via `bd remember`.";
149
+ return { content: [...event.content, { type: "text", text: reminder }] };
114
150
  }
151
+
115
152
  return undefined;
116
153
  });
117
154
 
118
- // Dual safety net: warn about unclosed claims when session ends (non-blocking)
119
- const notifySessionEnd = async (ctx: any) => {
155
+ // Memory gate: if this session closed an issue, prompt for insight persistence.
156
+ // Uses sendUserMessage to trigger a new turn in Pi (non-blocking alternative to Claude Stop hook).
157
+ const triggerMemoryGateIfNeeded = async (ctx: any) => {
120
158
  const cwd = getCwd(ctx);
121
159
  if (!EventAdapter.isBeadsProject(cwd)) return;
122
160
  const sessionId = getSessionId(ctx);
123
- const claim = await getSessionClaim(sessionId, cwd);
124
- if (!claim) return;
125
161
 
126
- const message = `Beads: session ending with active claim [${claim}]`;
127
- if (ctx.hasUI) {
128
- ctx.ui.notify(message, "warning");
129
- } else {
130
- logger.warn(message);
162
+ const markerPath = join(cwd, ".beads", ".memory-gate-done");
163
+ if (existsSync(markerPath)) {
164
+ try { unlinkSync(markerPath); } catch { /* ignore */ }
165
+ await clearSessionMarkers(sessionId, cwd);
166
+ memoryGateFired = false;
167
+ return;
131
168
  }
132
- };
133
169
 
134
- // Memory gate: if the session's claimed issue was closed, prompt for insights.
135
- // Uses sendUserMessage to trigger a new agent turn (Pi has no blocking stop hook).
136
- // memoryGateFired prevents re-triggering on the follow-up agent_end.
137
- const triggerMemoryGateIfNeeded = async (ctx: any) => {
138
170
  if (memoryGateFired) return;
139
- const cwd = getCwd(ctx);
140
- if (!EventAdapter.isBeadsProject(cwd)) return;
141
- const sessionId = getSessionId(ctx);
142
- const claimId = await getSessionClaim(sessionId, cwd);
143
- if (!claimId) return;
144
171
 
145
- const result = await SubprocessRunner.run("bd", ["list", "--status=closed"], { cwd });
146
- if (result.code !== 0 || !result.stdout.includes(claimId)) return;
172
+ const closedIssueId = await getClosedThisSession(sessionId, cwd);
173
+ if (!closedIssueId) return;
147
174
 
148
175
  memoryGateFired = true;
149
176
  pi.sendUserMessage(
150
- `🧠 Memory gate: claim \`${claimId}\` was closed this session.\n` +
177
+ `🧠 Memory gate: claim \`${closedIssueId}\` was closed this session.\n` +
151
178
  `For each closed issue, worth persisting?\n` +
152
179
  ` YES → \`bd remember "<insight>"\`\n` +
153
180
  ` NO → note "nothing to persist"\n` +
@@ -156,13 +183,12 @@ export default function (pi: ExtensionAPI) {
156
183
  };
157
184
 
158
185
  pi.on("agent_end", async (_event, ctx) => {
159
- await notifySessionEnd(ctx);
160
186
  await triggerMemoryGateIfNeeded(ctx);
161
187
  return undefined;
162
188
  });
163
189
 
164
190
  pi.on("session_shutdown", async (_event, ctx) => {
165
- await notifySessionEnd(ctx);
191
+ await triggerMemoryGateIfNeeded(ctx);
166
192
  return undefined;
167
193
  });
168
194
  }