xtrm-tools 0.5.48 → 0.6.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.
Files changed (29) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/cli/dist/index.cjs +6218 -7897
  3. package/cli/dist/index.cjs.map +1 -1
  4. package/cli/package.json +5 -1
  5. package/config/pi/extensions/beads/index.ts +1 -1
  6. package/config/pi/extensions/beads/package.json +4 -1
  7. package/config/pi/extensions/core/package.json +18 -0
  8. package/config/pi/extensions/custom-footer/index.ts +138 -71
  9. package/config/pi/extensions/custom-footer/package.json +4 -1
  10. package/config/pi/extensions/quality-gates/index.ts +1 -1
  11. package/config/pi/extensions/quality-gates/package.json +4 -1
  12. package/config/pi/extensions/service-skills/index.ts +1 -1
  13. package/config/pi/extensions/service-skills/package.json +4 -1
  14. package/config/pi/extensions/session-flow/index.ts +1 -1
  15. package/config/pi/extensions/session-flow/package.json +4 -1
  16. package/config/pi/extensions/xtrm-loader/index.ts +1 -1
  17. package/config/pi/extensions/xtrm-loader/package.json +4 -1
  18. package/hooks/beads-compact-restore.mjs +11 -3
  19. package/hooks/beads-compact-save.mjs +13 -1
  20. package/hooks/tsconfig-cache.json +12 -2
  21. package/package.json +3 -2
  22. package/plugins/xtrm-tools/.claude-plugin/plugin.json +1 -1
  23. package/plugins/xtrm-tools/hooks/beads-compact-restore.mjs +11 -3
  24. package/plugins/xtrm-tools/hooks/beads-compact-save.mjs +13 -1
  25. package/plugins/xtrm-tools/hooks/tsconfig-cache.json +12 -2
  26. package/plugins/xtrm-tools/skills/xt-end/SKILL.md +1 -0
  27. package/plugins/xtrm-tools/skills/xt-merge/SKILL.md +141 -18
  28. package/skills/xt-end/SKILL.md +1 -0
  29. package/skills/xt-merge/SKILL.md +141 -18
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-cli",
3
- "version": "0.5.48",
3
+ "version": "0.6.0",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -8,6 +8,10 @@
8
8
  "xtrm": "dist/index.cjs",
9
9
  "xt": "dist/index.cjs"
10
10
  },
11
+ "files": [
12
+ "dist",
13
+ "config"
14
+ ],
11
15
  "scripts": {
12
16
  "prebuild": "node -e \"if(process.cwd().includes('/.xtrm/worktrees/')){console.error('ERROR: Do not run npm run build from a worktree — dist paths will be contaminated.\\nRun from the main repo: cd <repo-root>/cli && npm run build');process.exit(1)}\"",
13
17
  "build": "tsup",
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { isToolCallEventType, isBashToolResult } from "@mariozechner/pi-coding-agent";
3
- import { SubprocessRunner, EventAdapter } from "../core/lib";
3
+ import { SubprocessRunner, EventAdapter } from "@xtrm/pi-core";
4
4
 
5
5
  export default function (pi: ExtensionAPI) {
6
6
  const getCwd = (ctx: any) => ctx.cwd || process.cwd();
@@ -12,5 +12,8 @@
12
12
  "xtrm"
13
13
  ],
14
14
  "author": "xtrm",
15
- "license": "MIT"
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@xtrm/pi-core": "^1.0.0"
18
+ }
16
19
  }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@xtrm/pi-core",
3
+ "version": "1.0.0",
4
+ "description": "Shared utilities for xtrm Pi extensions",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./lib.ts",
8
+ "./lib": "./lib.ts",
9
+ "./logger": "./logger.ts",
10
+ "./runner": "./runner.ts",
11
+ "./adapter": "./adapter.ts",
12
+ "./guard-rules": "./guard-rules.ts",
13
+ "./session-state": "./session-state.ts"
14
+ },
15
+ "keywords": ["pi", "extension", "xtrm"],
16
+ "author": "xtrm",
17
+ "license": "MIT"
18
+ }
@@ -1,15 +1,16 @@
1
1
  /**
2
2
  * XTRM Custom Footer Extension
3
3
  *
4
- * Displays: XTRM brand, model/context, host, cwd, git branch/status, beads state.
4
+ * Layout:
5
+ * Line 1: ~/path (branch *+↑) — with git status flags, no session name
6
+ * Line 2: XX%/window | (provider) model • thinking — simplified stats
7
+ * Line 3: ◐ 4843.5 Rework project bootstrap... — beads claim or ○ 6 open
5
8
  */
6
9
 
7
10
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
- import { truncateToWidth } from "@mariozechner/pi-tui";
9
- import { basename, relative } from "node:path";
10
- import { hostname } from "node:os";
11
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
11
12
 
12
- import { SubprocessRunner, EventAdapter } from "../core/lib";
13
+ import { SubprocessRunner, EventAdapter } from "@xtrm/pi-core";
13
14
 
14
15
  export default function (pi: ExtensionAPI) {
15
16
  interface BeadState {
@@ -22,11 +23,8 @@ export default function (pi: ExtensionAPI) {
22
23
  }
23
24
 
24
25
  interface RuntimeState {
25
- host: string;
26
- displayDir: string;
27
26
  branch: string | null;
28
27
  gitStatus: string;
29
- venv: string | null;
30
28
  lastFetch: number;
31
29
  }
32
30
 
@@ -52,6 +50,7 @@ export default function (pi: ExtensionAPI) {
52
50
 
53
51
  const chip = (text: string, bg = CHIP_BG_NEUTRAL): string => `${bg}${CHIP_FG} ${text} ${CHIP_RESET}`;
54
52
 
53
+ let capturedPi: ExtensionAPI = pi;
55
54
  let capturedCtx: any = null;
56
55
  let sessionId = "";
57
56
  let requestRender: (() => void) | null = null;
@@ -70,17 +69,17 @@ export default function (pi: ExtensionAPI) {
70
69
  };
71
70
 
72
71
  let runtimeState: RuntimeState = {
73
- host: hostname().split(".")[0] || "host",
74
- displayDir: process.cwd(),
75
72
  branch: null,
76
73
  gitStatus: "",
77
- venv: process.env.VIRTUAL_ENV ? basename(process.env.VIRTUAL_ENV) : null,
78
74
  lastFetch: 0,
79
75
  };
80
76
 
81
77
  const getCwd = () => capturedCtx?.cwd || process.cwd();
82
78
  const getShortId = (id: string) => id.split("-").pop() ?? id;
83
79
 
80
+ /**
81
+ * Parse git status --porcelain output into status flags
82
+ */
84
83
  const parseGitFlags = (porcelain: string): string => {
85
84
  let modified = false;
86
85
  let staged = false;
@@ -93,33 +92,29 @@ export default function (pi: ExtensionAPI) {
93
92
  return `${modified ? "*" : ""}${staged ? "+" : ""}${deleted ? "-" : ""}`;
94
93
  };
95
94
 
95
+ /**
96
+ * Fetch git branch and status
97
+ */
96
98
  const refreshRuntimeState = async () => {
97
99
  if (refreshingRuntime || Date.now() - runtimeState.lastFetch < CACHE_TTL) return;
98
100
  refreshingRuntime = true;
99
101
  const cwd = getCwd();
100
102
  try {
101
- const host = hostname().split(".")[0] || "host";
102
- const venv = process.env.VIRTUAL_ENV ? basename(process.env.VIRTUAL_ENV) : null;
103
+ let branch: string | null = null;
104
+ let gitStatus = "";
105
+
103
106
  const rootResult = await SubprocessRunner.run("git", ["rev-parse", "--show-toplevel"], { cwd });
104
107
  const repoRoot = rootResult.code === 0 ? rootResult.stdout.trim() : null;
105
108
 
106
- const displayDir = repoRoot
107
- ? (() => {
108
- const relPath = relative(repoRoot, cwd) || ".";
109
- return relPath === "." ? basename(repoRoot) : `${basename(repoRoot)}/${relPath}`;
110
- })()
111
- : (() => {
112
- const parts = cwd.split("/");
113
- return parts.length > 2 ? parts.slice(-2).join("/") : cwd;
114
- })();
115
-
116
- let branch: string | null = null;
117
- let gitStatus = "";
118
109
  if (repoRoot) {
119
110
  const branchResult = await SubprocessRunner.run("git", ["branch", "--show-current"], { cwd });
120
111
  branch = branchResult.code === 0 ? branchResult.stdout.trim() || null : null;
121
112
 
122
- const porcelainResult = await SubprocessRunner.run("git", ["--no-optional-locks", "status", "--porcelain"], { cwd });
113
+ const porcelainResult = await SubprocessRunner.run(
114
+ "git",
115
+ ["--no-optional-locks", "status", "--porcelain"],
116
+ { cwd },
117
+ );
123
118
  const baseFlags = porcelainResult.code === 0 ? parseGitFlags(porcelainResult.stdout) : "";
124
119
 
125
120
  let upstreamFlags = "";
@@ -141,11 +136,8 @@ export default function (pi: ExtensionAPI) {
141
136
  }
142
137
 
143
138
  runtimeState = {
144
- host,
145
- displayDir,
146
139
  branch,
147
140
  gitStatus,
148
- venv,
149
141
  lastFetch: Date.now(),
150
142
  };
151
143
  requestRender?.();
@@ -156,6 +148,17 @@ export default function (pi: ExtensionAPI) {
156
148
  }
157
149
  };
158
150
 
151
+ /**
152
+ * Format token counts (from original footer)
153
+ */
154
+ const formatTokens = (count: number): string => {
155
+ if (count < 1000) return count.toString();
156
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
157
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
158
+ if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
159
+ return `${Math.round(count / 1000000)}M`;
160
+ };
161
+
159
162
  const refreshBeadState = async () => {
160
163
  if (refreshingBeads || Date.now() - beadState.lastFetch < CACHE_TTL) return;
161
164
  const cwd = getCwd();
@@ -216,29 +219,27 @@ export default function (pi: ExtensionAPI) {
216
219
  }
217
220
  };
218
221
 
219
- const buildBeadChip = (): string => {
220
- const { claimId, shortId, status, openCount } = beadState;
221
- if (claimId && shortId && status) {
222
- const icon = STATUS_ICONS[status] ?? "?";
223
- const bg = STATUS_BG[status] ?? CHIP_BG_NEUTRAL;
224
- return chip(`bd:${shortId}${icon}`, bg);
225
- }
226
- if (openCount > 0) return chip(`bd:${openCount}${STATUS_ICONS.open}`);
227
- return "";
228
- };
229
-
230
- const buildIssueLine = (width: number, theme: any): string => {
222
+ /**
223
+ * Build beads line: 4843.5 Rework project bootstrap... or ○ 6 open
224
+ */
225
+ const buildBeadsLine = (width: number, theme: any): string => {
231
226
  const { shortId, claimTitle, status, openCount } = beadState;
227
+
228
+ // Claimed: ◐ 4843.5 Rework project bootstrap, verificat...
232
229
  if (shortId && claimTitle && status) {
233
230
  const icon = STATUS_ICONS[status] ?? "◐";
234
231
  const prefix = `${icon} ${shortId} `;
235
232
  const title = theme.fg("muted", claimTitle);
236
233
  return truncateToWidth(`${prefix}${title}`, width);
237
234
  }
235
+
236
+ // Unclaimed with open issues: ○ 6 open
238
237
  if (openCount > 0) {
239
238
  return truncateToWidth(`○ ${openCount} open`, width);
240
239
  }
241
- return truncateToWidth("○ no open issues", width);
240
+
241
+ // No open issues: ○ no open issues
242
+ return truncateToWidth(`○ no open issues`, width);
242
243
  };
243
244
 
244
245
  let footerReapplyTimer: ReturnType<typeof setTimeout> | null = null;
@@ -262,36 +263,103 @@ export default function (pi: ExtensionAPI) {
262
263
  refreshRuntimeState().catch(() => {});
263
264
  refreshBeadState().catch(() => {});
264
265
 
265
- const BOLD = "\x1b[1m";
266
- const BOLD_OFF = "\x1b[22m";
267
- const brand = `${BOLD}${theme.fg("dim", "XTRM")}${BOLD_OFF}`;
266
+ // === LINE 1: ~/path (branch *+↑) ===
267
+ // Like original, no session name, but with git status flags
268
+ let pwd = process.cwd();
269
+ const home = process.env.HOME || process.env.USERPROFILE;
270
+ if (home && pwd.startsWith(home)) {
271
+ pwd = `~${pwd.slice(home.length)}`;
272
+ }
268
273
 
274
+ // Use runtimeState branch (with git status) or fallback to footerData
275
+ const branch = runtimeState.branch || footerData.getGitBranch();
276
+ if (branch) {
277
+ const branchWithStatus = runtimeState.gitStatus
278
+ ? `${branch} ${runtimeState.gitStatus}`
279
+ : branch;
280
+ pwd = `${pwd} (${branchWithStatus})`;
281
+ }
282
+
283
+ const pwdLine = truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "..."));
284
+
285
+ // === LINE 2: XX%/window (provider) model • thinking ===
269
286
  const usage = ctx.getContextUsage();
270
- const pct = usage?.percent ?? 0;
271
- const usageStr = theme.fg("dim", `[${pct.toFixed(0)}%]`);
272
-
273
- const modelId = ctx.model?.id || "no-model";
274
- const modelStr = `${modelId} ${usageStr}`;
275
-
276
- const branchFromFooter = footerData.getGitBranch();
277
- const branch = runtimeState.branch || branchFromFooter;
278
- const branchWithStatus = branch
279
- ? runtimeState.gitStatus
280
- ? `${branch} (${runtimeState.gitStatus})`
281
- : branch
282
- : "";
283
- const branchStr = branchWithStatus ? theme.fg("muted", branchWithStatus) : "";
284
- const hostStr = theme.fg("muted", runtimeState.host);
285
- const cwdStr = `${BOLD}${runtimeState.displayDir}${BOLD_OFF}`;
286
- const venvStr = runtimeState.venv ? theme.fg("muted", `(${runtimeState.venv})`) : "";
287
-
288
- const line1Parts = [brand, modelStr, hostStr, cwdStr];
289
- if (branchStr) line1Parts.push(branchStr);
290
- if (venvStr) line1Parts.push(venvStr);
291
-
292
- const line1 = truncateToWidth(line1Parts.join(" "), width);
293
- const line2 = buildIssueLine(width, theme);
294
- return [line1, line2];
287
+ const model = ctx.model;
288
+
289
+ const contextWindow = usage?.contextWindow ?? model?.contextWindow ?? 0;
290
+ const contextPercentValue = usage?.percent ?? 0;
291
+ const contextPercent = usage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
292
+
293
+ // Build left side: context %/window (color-coded like original)
294
+ const contextDisplay =
295
+ contextPercent === "?"
296
+ ? `?/${formatTokens(contextWindow)}`
297
+ : `${contextPercent}%/${formatTokens(contextWindow)}`;
298
+
299
+ let statsLeft: string;
300
+ if (contextPercentValue > 90) {
301
+ statsLeft = theme.fg("error", contextDisplay);
302
+ } else if (contextPercentValue > 70) {
303
+ statsLeft = theme.fg("warning", contextDisplay);
304
+ } else {
305
+ statsLeft = contextDisplay;
306
+ }
307
+
308
+ // Build right side: (provider) model • thinking
309
+ const modelName = model?.id || "no-model";
310
+ const providerCount = footerData.getAvailableProviderCount();
311
+
312
+ // Thinking level if model supports reasoning
313
+ let rightSideWithoutProvider = modelName;
314
+ if (model?.reasoning) {
315
+ const thinkingLevel = capturedPi.getThinkingLevel() || "off";
316
+ rightSideWithoutProvider =
317
+ thinkingLevel === "off" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;
318
+ }
319
+
320
+ // Prepend provider if >1
321
+ let rightSide = rightSideWithoutProvider;
322
+ if (providerCount > 1 && model) {
323
+ rightSide = `(${model.provider}) ${rightSideWithoutProvider}`;
324
+ if (visibleWidth(statsLeft) + 3 + visibleWidth(rightSide) > width) {
325
+ rightSide = rightSideWithoutProvider;
326
+ }
327
+ }
328
+
329
+ // Keep provider/model adjacent to usage (no right-bound alignment)
330
+ const separator = " ";
331
+
332
+ // Calculate layout
333
+ let leftWidth = visibleWidth(statsLeft);
334
+ const separatorWidth = visibleWidth(separator);
335
+
336
+ // Check if left side too wide
337
+ if (leftWidth > width - separatorWidth) {
338
+ statsLeft = truncateToWidth(statsLeft, width - separatorWidth, "...");
339
+ leftWidth = visibleWidth(statsLeft);
340
+ }
341
+
342
+ // Truncate right side to remaining width and place immediately after separator
343
+ const availableForRight = width - leftWidth - separatorWidth;
344
+ let line2: string;
345
+ if (availableForRight > 0) {
346
+ const truncatedRight = truncateToWidth(rightSide, availableForRight, "");
347
+ line2 = statsLeft + separator + truncatedRight;
348
+ } else {
349
+ line2 = truncateToWidth(statsLeft, width, "");
350
+ }
351
+
352
+ // Apply dim to each part separately (like original footer)
353
+ // statsLeft may contain color codes for context % that end with reset
354
+ const dimLeft = theme.fg("dim", statsLeft);
355
+ const dimSep = theme.fg("dim", separator);
356
+ const remainder = line2.slice(leftWidth + separatorWidth);
357
+ const dimRemainder = theme.fg("dim", remainder);
358
+
359
+ // === LINE 3: ◐ 4843.5 Rework project bootstrap... ===
360
+ const line3 = buildBeadsLine(width, theme);
361
+
362
+ return [pwdLine, dimLeft + dimSep + dimRemainder, line3];
295
363
  },
296
364
  };
297
365
  });
@@ -325,7 +393,6 @@ export default function (pi: ExtensionAPI) {
325
393
  });
326
394
 
327
395
  pi.on("model_select", async (_event, ctx) => {
328
- runtimeState.lastFetch = 0;
329
396
  scheduleFooterReapply(ctx);
330
397
  });
331
398
 
@@ -12,5 +12,8 @@
12
12
  "xtrm"
13
13
  ],
14
14
  "author": "xtrm",
15
- "license": "MIT"
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@xtrm/pi-core": "^1.0.0"
18
+ }
16
19
  }
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { SubprocessRunner, EventAdapter } from "../core/lib";
2
+ import { SubprocessRunner, EventAdapter } from "@xtrm/pi-core";
3
3
  import * as path from "node:path";
4
4
  import * as fs from "node:fs";
5
5
 
@@ -12,5 +12,8 @@
12
12
  "xtrm"
13
13
  ],
14
14
  "author": "xtrm",
15
- "license": "MIT"
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@xtrm/pi-core": "^1.0.0"
18
+ }
16
19
  }
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { SubprocessRunner } from "../core/lib";
2
+ import { SubprocessRunner } from "@xtrm/pi-core";
3
3
  import * as path from "node:path";
4
4
  import * as fs from "node:fs";
5
5
 
@@ -12,5 +12,8 @@
12
12
  "xtrm"
13
13
  ],
14
14
  "author": "xtrm",
15
- "license": "MIT"
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@xtrm/pi-core": "^1.0.0"
18
+ }
16
19
  }
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { isBashToolResult } from "@mariozechner/pi-coding-agent";
3
- import { SubprocessRunner, EventAdapter } from "../core/lib";
3
+ import { SubprocessRunner, EventAdapter } from "@xtrm/pi-core";
4
4
 
5
5
  function isClaimCommand(command: string): { isClaim: boolean; issueId: string | null } {
6
6
  if (!/\bbd\s+update\b/.test(command) || !/--claim\b/.test(command)) {
@@ -12,5 +12,8 @@
12
12
  "xtrm"
13
13
  ],
14
14
  "author": "xtrm",
15
- "license": "MIT"
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@xtrm/pi-core": "^1.0.0"
18
+ }
16
19
  }
@@ -2,7 +2,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { homedir } from "node:os";
5
- import { Logger } from "../core/lib";
5
+ import { Logger } from "@xtrm/pi-core";
6
6
 
7
7
  const logger = new Logger({ namespace: "xtrm-loader" });
8
8
 
@@ -12,5 +12,8 @@
12
12
  "xtrm"
13
13
  ],
14
14
  "author": "xtrm",
15
- "license": "MIT"
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@xtrm/pi-core": "^1.0.0"
18
+ }
16
19
  }
@@ -21,12 +21,14 @@ const lastActivePath = path.join(cwd, '.beads', '.last_active');
21
21
  if (!existsSync(lastActivePath)) process.exit(0);
22
22
 
23
23
  let ids = [];
24
+ let serenaProject = null;
24
25
 
25
26
  try {
26
27
  const raw = readFileSync(lastActivePath, 'utf8').trim();
27
28
  if (raw.startsWith('{')) {
28
29
  const parsed = JSON.parse(raw);
29
30
  ids = Array.isArray(parsed.ids) ? parsed.ids.filter(Boolean) : [];
31
+ serenaProject = parsed.serenaProject ?? null;
30
32
  } else {
31
33
  // Backward compatibility: legacy newline format
32
34
  ids = raw.split('\n').filter(Boolean);
@@ -53,14 +55,20 @@ for (const id of ids) {
53
55
  }
54
56
  }
55
57
 
56
- if (restored > 0) {
57
- const lines = [`Restored ${restored} in_progress issue${restored === 1 ? '' : 's'} from last session before compaction.`];
58
+ if (restored > 0 || serenaProject) {
59
+ const parts = [];
60
+ if (restored > 0) {
61
+ parts.push(`Restored ${restored} in_progress issue${restored === 1 ? '' : 's'} from last session before compaction. Check \`bd list\` for details.`);
62
+ }
63
+ if (serenaProject) {
64
+ parts.push(`Serena was active on project "${serenaProject}" — re-activate with activate_project("${serenaProject}") before any symbol lookups.`);
65
+ }
58
66
 
59
67
  process.stdout.write(
60
68
  JSON.stringify({
61
69
  hookSpecificOutput: {
62
70
  hookEventName: 'SessionStart',
63
- additionalSystemPrompt: `${lines.join(' ')} Check \`bd list\` for details.`,
71
+ additionalSystemPrompt: parts.join(' '),
64
72
  },
65
73
  }) + '\n',
66
74
  );
@@ -44,7 +44,19 @@ const bundle = {
44
44
  savedAt: new Date().toISOString(),
45
45
  };
46
46
 
47
- if (bundle.ids.length === 0) process.exit(0);
47
+ // Detect active Serena project
48
+ const serenaProjectYml = path.join(cwd, '.serena', 'project.yml');
49
+ if (existsSync(serenaProjectYml)) {
50
+ try {
51
+ const yml = readFileSync(serenaProjectYml, 'utf8');
52
+ const m = yml.match(/^project_name:\s*["']?([^"'\n]+)["']?/m);
53
+ if (m) bundle.serenaProject = m[1].trim();
54
+ } catch {
55
+ // ignore — not critical
56
+ }
57
+ }
58
+
59
+ if (bundle.ids.length === 0 && !bundle.serenaProject) process.exit(0);
48
60
 
49
61
  writeFileSync(path.join(beadsDir, '.last_active'), JSON.stringify(bundle, null, 2) + '\n', 'utf8');
50
62
 
@@ -1,4 +1,14 @@
1
1
  {
2
- "hashes": {},
3
- "mappings": {}
2
+ "hashes": {
3
+ "/home/dawid/projects/specialists/tsconfig.json": "84f37aa64b949b7aaba41974cb6137341a36730b7d3e174803cb9289eae1e7c7"
4
+ },
5
+ "mappings": {
6
+ "src/**/*": {
7
+ "configPath": "/home/dawid/projects/specialists/tsconfig.json",
8
+ "excludes": [
9
+ "node_modules",
10
+ "dist"
11
+ ]
12
+ }
13
+ }
4
14
  }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "0.5.48",
3
+ "version": "0.6.0",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "workspaces": [
8
- "cli"
8
+ "cli",
9
+ "config/pi/extensions/core"
9
10
  ],
10
11
  "bin": {
11
12
  "xtrm": "cli/dist/index.cjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "0.5.48",
3
+ "version": "0.6.0",
4
4
  "description": "xtrm-tools: dual-runtime workflow enforcement (Claude Code + Pi) — hooks, extensions, skills, and MCP servers",
5
5
  "author": {
6
6
  "name": "jaggers"
@@ -21,12 +21,14 @@ const lastActivePath = path.join(cwd, '.beads', '.last_active');
21
21
  if (!existsSync(lastActivePath)) process.exit(0);
22
22
 
23
23
  let ids = [];
24
+ let serenaProject = null;
24
25
 
25
26
  try {
26
27
  const raw = readFileSync(lastActivePath, 'utf8').trim();
27
28
  if (raw.startsWith('{')) {
28
29
  const parsed = JSON.parse(raw);
29
30
  ids = Array.isArray(parsed.ids) ? parsed.ids.filter(Boolean) : [];
31
+ serenaProject = parsed.serenaProject ?? null;
30
32
  } else {
31
33
  // Backward compatibility: legacy newline format
32
34
  ids = raw.split('\n').filter(Boolean);
@@ -53,14 +55,20 @@ for (const id of ids) {
53
55
  }
54
56
  }
55
57
 
56
- if (restored > 0) {
57
- const lines = [`Restored ${restored} in_progress issue${restored === 1 ? '' : 's'} from last session before compaction.`];
58
+ if (restored > 0 || serenaProject) {
59
+ const parts = [];
60
+ if (restored > 0) {
61
+ parts.push(`Restored ${restored} in_progress issue${restored === 1 ? '' : 's'} from last session before compaction. Check \`bd list\` for details.`);
62
+ }
63
+ if (serenaProject) {
64
+ parts.push(`Serena was active on project "${serenaProject}" — re-activate with activate_project("${serenaProject}") before any symbol lookups.`);
65
+ }
58
66
 
59
67
  process.stdout.write(
60
68
  JSON.stringify({
61
69
  hookSpecificOutput: {
62
70
  hookEventName: 'SessionStart',
63
- additionalSystemPrompt: `${lines.join(' ')} Check \`bd list\` for details.`,
71
+ additionalSystemPrompt: parts.join(' '),
64
72
  },
65
73
  }) + '\n',
66
74
  );
@@ -44,7 +44,19 @@ const bundle = {
44
44
  savedAt: new Date().toISOString(),
45
45
  };
46
46
 
47
- if (bundle.ids.length === 0) process.exit(0);
47
+ // Detect active Serena project
48
+ const serenaProjectYml = path.join(cwd, '.serena', 'project.yml');
49
+ if (existsSync(serenaProjectYml)) {
50
+ try {
51
+ const yml = readFileSync(serenaProjectYml, 'utf8');
52
+ const m = yml.match(/^project_name:\s*["']?([^"'\n]+)["']?/m);
53
+ if (m) bundle.serenaProject = m[1].trim();
54
+ } catch {
55
+ // ignore — not critical
56
+ }
57
+ }
58
+
59
+ if (bundle.ids.length === 0 && !bundle.serenaProject) process.exit(0);
48
60
 
49
61
  writeFileSync(path.join(beadsDir, '.last_active'), JSON.stringify(bundle, null, 2) + '\n', 'utf8');
50
62
 
@@ -1,4 +1,14 @@
1
1
  {
2
- "hashes": {},
3
- "mappings": {}
2
+ "hashes": {
3
+ "/home/dawid/projects/specialists/tsconfig.json": "84f37aa64b949b7aaba41974cb6137341a36730b7d3e174803cb9289eae1e7c7"
4
+ },
5
+ "mappings": {
6
+ "src/**/*": {
7
+ "configPath": "/home/dawid/projects/specialists/tsconfig.json",
8
+ "excludes": [
9
+ "node_modules",
10
+ "dist"
11
+ ]
12
+ }
13
+ }
4
14
  }
@@ -19,6 +19,7 @@ Default to **autonomous execution**:
19
19
  - do not ask the user routine clarification questions
20
20
  - prefer deterministic fallbacks over conversational review
21
21
  - only stop when a real blocker prevents safe progress
22
+ - **always invoke `xt end --yes`** — never call `xt end` without this flag; the bare command prompts interactively for worktree removal which blocks autonomous execution
22
23
 
23
24
  ## Success States
24
25