xtrm-tools 2.1.28 → 2.1.30

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.28",
3
+ "version": "2.1.30",
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
@@ -36,6 +36,11 @@
36
36
  }
37
37
  ],
38
38
  "PostToolUse": [
39
+ {
40
+ "matcher": "Bash|execute_shell_command|bash",
41
+ "script": "beads-claim-sync.mjs",
42
+ "timeout": 5000
43
+ },
39
44
  {
40
45
  "matcher": "Bash",
41
46
  "script": "main-guard-post-push.mjs",
@@ -0,0 +1,30 @@
1
+ # XTRM Agent Workflow (Short)
2
+
3
+ This file is an **agent operating manual** (not a project overview).
4
+
5
+ 1. **Start with scope**
6
+ - Clarify task intent if ambiguous.
7
+ - Prefer semantic discovery (Serena + GitNexus) over broad grep-first exploration.
8
+
9
+ 2. **Track work in `bd`**
10
+ - Use `bd ready --json` / `bd update <id> --claim --json` before edits.
11
+ - Create discovered follow-ups with `--deps discovered-from:<id>`.
12
+
13
+ 3. **Branch per issue (strict)**
14
+ - Create a **new branch for each issue** from latest `main`.
15
+ - Do **not** continue new work on a previously used branch.
16
+ - Branch format: `feature/<issue-id>-<short-description>` (or `fix/...`, `chore/...`).
17
+
18
+ 4. **Edit safely**
19
+ - Use Serena symbol tools for code changes when possible.
20
+ - Run GitNexus impact checks before symbol changes and detect-changes before commit.
21
+
22
+ 5. **PR merge + return to main**
23
+ - Always merge via PR (squash merge preferred).
24
+ - After merge: switch to `main` and sync (`git reset --hard origin/main`).
25
+ - Delete merged branch locally and remotely (`git branch -d <branch>` and `git push origin --delete <branch>`).
26
+
27
+ 6. **Before finishing**
28
+ - Run relevant tests/linters.
29
+ - Close/update bead state.
30
+ - Ensure changes are committed and pushed.
@@ -0,0 +1,30 @@
1
+ # XTRM Agent Workflow (Short)
2
+
3
+ This file is an **agent operating manual** (not a project overview).
4
+
5
+ 1. **Start with scope**
6
+ - Clarify task intent if ambiguous.
7
+ - Prefer semantic discovery (Serena + GitNexus) over broad grep-first exploration.
8
+
9
+ 2. **Track work in `bd`**
10
+ - Use `bd ready --json` / `bd update <id> --claim --json` before edits.
11
+ - Create discovered follow-ups with `--deps discovered-from:<id>`.
12
+
13
+ 3. **Branch per issue (strict)**
14
+ - Create a **new branch for each issue** from latest `main`.
15
+ - Do **not** continue new work on a previously used branch.
16
+ - Branch format: `feature/<issue-id>-<short-description>` (or `fix/...`, `chore/...`).
17
+
18
+ 4. **Edit safely**
19
+ - Use Serena symbol tools for code changes when possible.
20
+ - Run GitNexus impact checks before symbol changes and detect-changes before commit.
21
+
22
+ 5. **PR merge + return to main**
23
+ - Always merge via PR (squash merge preferred).
24
+ - After merge: switch to `main` and sync (`git reset --hard origin/main`).
25
+ - Delete merged branch locally and remotely (`git branch -d <branch>` and `git push origin --delete <branch>`).
26
+
27
+ 6. **Before finishing**
28
+ - Run relevant tests/linters.
29
+ - Close/update bead state.
30
+ - Ensure changes are committed and pushed.
@@ -10,10 +10,18 @@ export default function (pi: ExtensionAPI) {
10
10
  const getCwd = (ctx: any) => ctx.cwd || process.cwd();
11
11
  const isBeadsProject = (cwd: string) => fs.existsSync(path.join(cwd, ".beads"));
12
12
 
13
- // Pin session to PID stable for the life of the Pi process, consistent across all extension handlers
14
- const sessionId = process.pid.toString();
13
+ let cachedSessionId: string | null = null;
15
14
 
16
- const getSessionClaim = async (cwd: string): Promise<string | null> => {
15
+ // Resolve a stable session ID across event types.
16
+ const getSessionId = (ctx: any): string => {
17
+ const fromManager = ctx?.sessionManager?.getSessionId?.();
18
+ const fromContext = ctx?.sessionId ?? ctx?.session_id;
19
+ const resolved = fromManager || fromContext || cachedSessionId || process.pid.toString();
20
+ if (resolved && !cachedSessionId) cachedSessionId = resolved;
21
+ return resolved;
22
+ };
23
+
24
+ const getSessionClaim = async (sessionId: string, cwd: string): Promise<string | null> => {
17
25
  const result = await SubprocessRunner.run("bd", ["kv", "get", `claimed:${sessionId}`], { cwd });
18
26
  if (result.code === 0) return result.stdout.trim();
19
27
  return null;
@@ -32,12 +40,18 @@ export default function (pi: ExtensionAPI) {
32
40
  return false;
33
41
  };
34
42
 
43
+ pi.on("session_start", async (_event, ctx) => {
44
+ cachedSessionId = ctx?.sessionManager?.getSessionId?.() ?? ctx?.sessionId ?? ctx?.session_id ?? cachedSessionId;
45
+ return undefined;
46
+ });
47
+
35
48
  pi.on("tool_call", async (event, ctx) => {
36
49
  const cwd = getCwd(ctx);
37
50
  if (!isBeadsProject(cwd)) return undefined;
51
+ const sessionId = getSessionId(ctx);
38
52
 
39
53
  if (EventAdapter.isMutatingFileTool(event)) {
40
- const claim = await getSessionClaim(cwd);
54
+ const claim = await getSessionClaim(sessionId, cwd);
41
55
  if (!claim) {
42
56
  const hasWork = await hasTrackableWork(cwd);
43
57
  if (hasWork) {
@@ -46,7 +60,7 @@ export default function (pi: ExtensionAPI) {
46
60
  }
47
61
  return {
48
62
  block: true,
49
- reason: `No active issue claim for this session (pid:${sessionId}).\n bd update <id> --claim\n bd kv set "claimed:${sessionId}" "<id>"`,
63
+ reason: `No active issue claim for this session (${sessionId}).\n bd update <id> --claim\n bd kv set "claimed:${sessionId}" "<id>"`,
50
64
  };
51
65
  }
52
66
  }
@@ -55,7 +69,7 @@ export default function (pi: ExtensionAPI) {
55
69
  if (isToolCallEventType("bash", event)) {
56
70
  const command = event.input.command;
57
71
  if (command && /\bgit\s+commit\b/.test(command)) {
58
- const claim = await getSessionClaim(cwd);
72
+ const claim = await getSessionClaim(sessionId, cwd);
59
73
  if (claim) {
60
74
  return {
61
75
  block: true,
@@ -71,6 +85,7 @@ export default function (pi: ExtensionAPI) {
71
85
  pi.on("tool_result", async (event, ctx) => {
72
86
  if (isBashToolResult(event)) {
73
87
  const command = event.input.command;
88
+ const sessionId = getSessionId(ctx);
74
89
 
75
90
  // Auto-claim on bd update --claim regardless of exit code.
76
91
  // bd returns exit 1 with "already in_progress" when status unchanged — still a valid claim intent.
@@ -80,7 +95,7 @@ export default function (pi: ExtensionAPI) {
80
95
  const issueId = issueMatch[1];
81
96
  const cwd = getCwd(ctx);
82
97
  await SubprocessRunner.run("bd", ["kv", "set", `claimed:${sessionId}`, issueId], { cwd });
83
- const claimNotice = `\n\n✅ **Beads**: Session \`pid:${sessionId}\` claimed issue \`${issueId}\`. File edits are now unblocked.`;
98
+ const claimNotice = `\n\n✅ **Beads**: Session \`${sessionId}\` claimed issue \`${issueId}\`. File edits are now unblocked.`;
84
99
  return { content: [...event.content, { type: "text", text: claimNotice }] };
85
100
  }
86
101
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * XTRM Custom Footer Extension
3
3
  *
4
- * Displays: XTRM brand, Turn count, Model, Context%, CWD, Git branch
4
+ * Displays: XTRM brand, Model, Context%, CWD, Git branch, Beads chip
5
5
  */
6
6
 
7
7
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
@@ -12,9 +12,6 @@ import * as fs from "node:fs";
12
12
  import { SubprocessRunner } from "./core/lib";
13
13
 
14
14
  export default function (pi: ExtensionAPI) {
15
- // Pin session to PID — stable for the life of the Pi process, matches beads.ts
16
- const SESSION_KEY = process.pid.toString();
17
-
18
15
  interface BeadState {
19
16
  claimId: string | null;
20
17
  shortId: string | null;
@@ -31,11 +28,12 @@ export default function (pi: ExtensionAPI) {
31
28
  };
32
29
  const STATUS_BG: Record<string, string> = {
33
30
  open: "\x1b[48;5;238m",
34
- in_progress: "\x1b[48;5;28m",
31
+ in_progress: "\x1b[48;5;39m",
35
32
  blocked: "\x1b[48;5;88m",
36
33
  };
37
34
 
38
35
  let capturedCtx: any = null;
36
+ let sessionId: string = "";
39
37
  let beadState: BeadState = { claimId: null, shortId: null, status: null, openCount: 0, lastFetch: 0 };
40
38
  let refreshing = false;
41
39
  let requestRender: (() => void) | null = null;
@@ -49,9 +47,10 @@ export default function (pi: ExtensionAPI) {
49
47
  if (refreshing || Date.now() - beadState.lastFetch < CACHE_TTL) return;
50
48
  const cwd = getCwd();
51
49
  if (!isBeadsProject(cwd)) return;
50
+ if (!sessionId) return;
52
51
  refreshing = true;
53
52
  try {
54
- const claimResult = await SubprocessRunner.run("bd", ["kv", "get", `claimed:${SESSION_KEY}`], { cwd });
53
+ const claimResult = await SubprocessRunner.run("bd", ["kv", "get", `claimed:${sessionId}`], { cwd });
55
54
  const claimId = claimResult.code === 0 ? claimResult.stdout.trim() || null : null;
56
55
 
57
56
  let status: string | null = null;
@@ -61,7 +60,7 @@ export default function (pi: ExtensionAPI) {
61
60
  try { status = JSON.parse(showResult.stdout)[0]?.status ?? null; } catch {}
62
61
  }
63
62
  if (status === "closed") {
64
- await SubprocessRunner.run("bd", ["kv", "clear", `claimed:${SESSION_KEY}`], { cwd });
63
+ await SubprocessRunner.run("bd", ["kv", "clear", `claimed:${sessionId}`], { cwd });
65
64
  beadState = { claimId: null, shortId: null, status: null, openCount: beadState.openCount, lastFetch: Date.now() };
66
65
  requestRender?.();
67
66
  return;
@@ -96,6 +95,8 @@ export default function (pi: ExtensionAPI) {
96
95
 
97
96
  pi.on("session_start", async (_event, ctx) => {
98
97
  capturedCtx = ctx;
98
+ // Get session ID from sessionManager/context (prefer UUID, consistent with hooks)
99
+ sessionId = ctx.sessionManager?.getSessionId?.() ?? ctx.sessionId ?? ctx.session_id ?? process.pid.toString();
99
100
 
100
101
  ctx.ui.setFooter((tui, theme, footerData) => {
101
102
  requestRender = () => tui.requestRender();
@@ -122,15 +123,16 @@ export default function (pi: ExtensionAPI) {
122
123
  const branchStr = branch ? theme.fg("accent", `⎇ ${branch}`) : "";
123
124
 
124
125
  const modelId = ctx.model?.id || "no-model";
125
- const modelStr = theme.fg("accent", modelId);
126
+ const modelChip = `\x1b[48;5;238m\x1b[38;5;15m ${modelId} \x1b[0m`;
126
127
 
127
128
  const sep = theme.fg("dim", " | ");
128
129
 
129
- const leftParts = [brand, modelStr, usageStr, cwdStr];
130
- if (branchStr) leftParts.push(branchStr);
131
-
130
+ const brandModel = `${brand} ${modelChip}`;
131
+ const leftParts = [brandModel, usageStr, cwdStr];
132
+
132
133
  const beadChip = buildBeadChip();
133
- if (beadChip) leftParts.push(beadChip);
134
+ const branchWithChip = branchStr ? `${branchStr} ${beadChip}`.trim() : beadChip;
135
+ if (branchWithChip) leftParts.push(branchWithChip);
134
136
 
135
137
  const left = leftParts.join(sep);
136
138
  return [truncateToWidth(left, width)];
@@ -139,7 +141,7 @@ export default function (pi: ExtensionAPI) {
139
141
  });
140
142
  });
141
143
 
142
- // Bust the bead cache immediately after any bd write so the chip reflects the new state
144
+ // Bust the bead cache immediately after any bd write
143
145
  pi.on("tool_result", async (event: any) => {
144
146
  const cmd = event?.input?.command;
145
147
  if (cmd && /\bbd\s+(close|update|create|claim)\b/.test(cmd)) {
@@ -0,0 +1,6 @@
1
+ {
2
+ "worktree": {
3
+ "parentDir": "../{{project}}.worktrees",
4
+ "onCreate": "npm install 2>/dev/null || true"
5
+ }
6
+ }
@@ -7,7 +7,8 @@
7
7
  "packages": [
8
8
  "npm:@aliou/pi-guardrails",
9
9
  "npm:pi-gitnexus",
10
- "npm:pi-serena-tools"
10
+ "npm:pi-serena-tools",
11
+ "npm:@zenobius/pi-worktrees"
11
12
  ],
12
13
  "steeringMode": "all",
13
14
  "followUpMode": "all",
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ // beads-claim-sync — PostToolUse hook
3
+ // Auto-sets bd kv claim when bd update --claim is detected.
4
+
5
+ import { spawnSync } from 'node:child_process';
6
+ import { readFileSync, existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+
9
+ function readInput() {
10
+ try {
11
+ return JSON.parse(readFileSync(0, 'utf-8'));
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ function isBeadsProject(cwd) {
18
+ return existsSync(join(cwd, '.beads'));
19
+ }
20
+
21
+ function isShellTool(toolName) {
22
+ return toolName === 'Bash' || toolName === 'bash' || toolName === 'execute_shell_command';
23
+ }
24
+
25
+ function commandSucceeded(payload) {
26
+ const tr = payload?.tool_response ?? payload?.tool_result ?? payload?.result;
27
+ if (!tr || typeof tr !== 'object') return true;
28
+
29
+ if (tr.success === false) return false;
30
+ if (tr.error) return false;
31
+
32
+ const numeric = [tr.exit_code, tr.exitCode, tr.status, tr.returncode].find((v) => Number.isInteger(v));
33
+ if (typeof numeric === 'number' && numeric !== 0) return false;
34
+
35
+ return true;
36
+ }
37
+
38
+ function main() {
39
+ const input = readInput();
40
+ if (!input || input.hook_event_name !== 'PostToolUse') process.exit(0);
41
+ if (!isShellTool(input.tool_name)) process.exit(0);
42
+ if (!commandSucceeded(input)) process.exit(0);
43
+
44
+ const cwd = input.cwd || process.cwd();
45
+ if (!isBeadsProject(cwd)) process.exit(0);
46
+
47
+ 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
+ const sessionId = input.session_id ?? input.sessionId;
57
+
58
+ if (!sessionId) {
59
+ process.stderr.write('Beads claim sync: no session_id in hook input\n');
60
+ process.exit(0);
61
+ }
62
+
63
+ const result = spawnSync('bd', ['kv', 'set', `claimed:${sessionId}`, issueId], {
64
+ cwd,
65
+ stdio: ['pipe', 'pipe', 'pipe'],
66
+ timeout: 5000,
67
+ });
68
+
69
+ if (result.status !== 0) {
70
+ const err = (result.stderr || result.stdout || '').toString().trim();
71
+ if (err) process.stderr.write(`Beads claim sync warning: ${err}\n`);
72
+ process.exit(0);
73
+ }
74
+
75
+ process.stdout.write(JSON.stringify({
76
+ additionalContext: `\n✅ **Beads**: Session \`${sessionId}\` claimed issue \`${issueId}\`.`,
77
+ }));
78
+ process.stdout.write('\n');
79
+ process.exit(0);
80
+ }
81
+
82
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "2.1.28",
3
+ "version": "2.1.30",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "license": "MIT",
6
6
  "type": "module",