xtrm-tools 0.5.45 → 0.5.47

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 (36) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +24 -5
  4. package/cli/dist/index.cjs +9812 -9983
  5. package/cli/dist/index.cjs.map +1 -1
  6. package/cli/package.json +1 -1
  7. package/config/instructions/agents-top.md +2 -4
  8. package/config/instructions/claude-top.md +2 -4
  9. package/config/pi/extensions/beads/index.ts +18 -78
  10. package/config/pi/extensions/custom-footer/index.ts +2 -3
  11. package/config/pi/extensions/xtrm-ui/format.ts +93 -0
  12. package/config/pi/extensions/xtrm-ui/index.ts +1044 -0
  13. package/config/pi/extensions/xtrm-ui/package.json +10 -0
  14. package/config/pi/extensions/xtrm-ui/themes/pidex-dark.json +85 -0
  15. package/config/pi/extensions/xtrm-ui/themes/pidex-light.json +85 -0
  16. package/config/pi/install-schema.json +0 -1
  17. package/hooks/beads-claim-sync.mjs +15 -96
  18. package/hooks/beads-gate-messages.mjs +2 -4
  19. package/hooks/beads-gate-utils.mjs +0 -18
  20. package/hooks/statusline.mjs +5 -3
  21. package/package.json +1 -1
  22. package/plugins/xtrm-tools/.claude-plugin/plugin.json +1 -1
  23. package/plugins/xtrm-tools/hooks/beads-claim-sync.mjs +15 -96
  24. package/plugins/xtrm-tools/hooks/beads-gate-messages.mjs +2 -4
  25. package/plugins/xtrm-tools/hooks/beads-gate-utils.mjs +0 -18
  26. package/plugins/xtrm-tools/hooks/statusline.mjs +5 -3
  27. package/plugins/xtrm-tools/skills/planning/SKILL.md +75 -20
  28. package/plugins/xtrm-tools/skills/using-xtrm/SKILL.md +1 -1
  29. package/plugins/xtrm-tools/skills/xt-debugging/SKILL.md +149 -0
  30. package/plugins/xtrm-tools/skills/xt-end/SKILL.md +28 -0
  31. package/skills/planning/SKILL.md +75 -20
  32. package/skills/using-xtrm/SKILL.md +1 -1
  33. package/skills/xt-debugging/SKILL.md +149 -0
  34. package/skills/xt-end/SKILL.md +28 -0
  35. package/plugins/xtrm-tools/skills/gitnexus-debugging/SKILL.md +0 -85
  36. package/skills/gitnexus-debugging/SKILL.md +0 -85
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-cli",
3
- "version": "0.5.45",
3
+ "version": "0.5.47",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -20,8 +20,6 @@
20
20
  | **Stop** | Session end with unclosed claim | `bd close <id>` |
21
21
  | **Memory** | Auto-fires at session end if issue closed | `bd remember "<insight>"` then run the `bd kv set` command shown in the gate message |
22
22
 
23
- > `bd close` auto-commits via `git commit -am`. Do not double-commit after closing.
24
-
25
23
  ## bd Command Reference
26
24
 
27
25
  ```bash
@@ -45,7 +43,7 @@ bd create --title="..." --description="..." --type=task --priority=2
45
43
  # types: task | bug | feature | epic | chore | decision
46
44
 
47
45
  # Closing
48
- bd close <id> # Close + auto-commit
46
+ bd close <id> # Close issue
49
47
  bd close <id> --reason="Done: ..." # Close with context
50
48
  bd close <id1> <id2> <id3> # Batch close
51
49
 
@@ -74,7 +72,7 @@ bd doctor # Diagnose installation issues
74
72
  git checkout -b feature/<issue-id>-<slug> # or fix/... chore/...
75
73
  bd update <id> --claim # claim before any edit
76
74
  # ... write code ...
77
- bd close <id> --reason="..." # closes issue + auto-commits
75
+ bd close <id> --reason="..." # closes issue
78
76
  xt end # push, PR, merge, worktree cleanup
79
77
  ```
80
78
 
@@ -20,8 +20,6 @@
20
20
  | **Stop** | Session end with unclosed claim | `bd close <id>` |
21
21
  | **Memory** | Auto-fires at Stop if issue closed this session | `bd remember "<insight>"` then run the `bd kv set` command shown in the gate message |
22
22
 
23
- > `bd close` auto-commits via `git commit -am`. Do not double-commit after closing.
24
-
25
23
  ## bd Command Reference
26
24
 
27
25
  ```bash
@@ -45,7 +43,7 @@ bd create --title="..." --description="..." --type=task --priority=2
45
43
  # types: task | bug | feature | epic | chore | decision
46
44
 
47
45
  # Closing
48
- bd close <id> # Close + auto-commit
46
+ bd close <id> # Close issue
49
47
  bd close <id> --reason="Done: ..." # Close with context
50
48
  bd close <id1> <id2> <id3> # Batch close
51
49
 
@@ -74,7 +72,7 @@ bd doctor # Diagnose installation issues
74
72
  git checkout -b feature/<issue-id>-<slug> # or fix/... chore/...
75
73
  bd update <id> --claim # claim before any edit
76
74
  # ... write code ...
77
- bd close <id> --reason="..." # closes issue + auto-commits
75
+ bd close <id> --reason="..." # closes issue
78
76
  xt end # push, PR, merge, worktree cleanup
79
77
  ```
80
78
 
@@ -2,61 +2,6 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { isToolCallEventType, isBashToolResult } from "@mariozechner/pi-coding-agent";
3
3
  import { SubprocessRunner, EventAdapter } from "../core/lib";
4
4
 
5
- // ─── Autocommit helpers (mirrors hooks/beads-claim-sync.mjs) ─────────────────
6
-
7
- async function hasGitChanges(cwd: string): Promise<boolean> {
8
- const result = await SubprocessRunner.run("git", ["status", "--porcelain"], { cwd });
9
- if (result.code !== 0) return false;
10
- return result.stdout.trim().length > 0;
11
- }
12
-
13
- async function stageUntracked(cwd: string): Promise<void> {
14
- const result = await SubprocessRunner.run("git", ["ls-files", "--others", "--exclude-standard"], { cwd });
15
- if (result.code !== 0 || !result.stdout.trim()) return;
16
- const untracked = result.stdout.trim().split("\n").filter(Boolean);
17
- if (untracked.length > 0) {
18
- await SubprocessRunner.run("git", ["add", "--", ...untracked], { cwd });
19
- }
20
- }
21
-
22
- async function getCloseReason(cwd: string, issueId: string, command: string): Promise<string> {
23
- // 1. Parse --reason "..." from the command itself
24
- const reasonMatch = command.match(/--reason[=\s]+["']([^"']+)["']/);
25
- if (reasonMatch) return reasonMatch[1].trim();
26
-
27
- // 2. Fall back to bd show <id> --json
28
- const show = await SubprocessRunner.run("bd", ["show", issueId, "--json"], { cwd });
29
- if (show.code === 0 && show.stdout.trim()) {
30
- try {
31
- const parsed = JSON.parse(show.stdout);
32
- const issue = Array.isArray(parsed) ? parsed[0] : parsed;
33
- const reason = issue?.close_reason;
34
- if (typeof reason === "string" && reason.trim()) return reason.trim();
35
- } catch { /* fall through */ }
36
- }
37
-
38
- return `Close ${issueId}`;
39
- }
40
-
41
- async function autoCommit(cwd: string, issueId: string, command: string): Promise<{ ok: boolean; message: string }> {
42
- if (!await hasGitChanges(cwd)) {
43
- return { ok: true, message: "No changes detected — auto-commit skipped." };
44
- }
45
-
46
- await stageUntracked(cwd);
47
-
48
- const reason = await getCloseReason(cwd, issueId, command);
49
- const commitMessage = `${reason} (${issueId})`;
50
- const result = await SubprocessRunner.run("git", ["commit", "--no-verify", "-am", commitMessage], { cwd });
51
-
52
- if (result.code !== 0) {
53
- const err = (result.stderr || result.stdout || "").trim();
54
- return { ok: false, message: `Auto-commit failed: ${err || "unknown error"}` };
55
- }
56
-
57
- return { ok: true, message: `Auto-committed: \`${commitMessage}\`` };
58
- }
59
-
60
5
  export default function (pi: ExtensionAPI) {
61
6
  const getCwd = (ctx: any) => ctx.cwd || process.cwd();
62
7
 
@@ -195,26 +140,33 @@ export default function (pi: ExtensionAPI) {
195
140
  const closeMatch = command.match(/\bbd\s+close\s+(\S+)/);
196
141
  const closedIssueId = closeMatch?.[1] ?? null;
197
142
 
198
- // Auto-commit staged changes (mirrors hooks/beads-claim-sync.mjs)
199
- const commit = closedIssueId ? await autoCommit(cwd, closedIssueId, command) : null;
200
-
201
143
  if (closedIssueId) {
202
144
  await SubprocessRunner.run("bd", ["kv", "set", `closed-this-session:${sessionId}`, closedIssueId], { cwd });
203
145
  memoryGateFired = false;
204
146
  }
205
147
 
206
- const commitLine = commit
207
- ? `\n${commit.ok ? "✅" : "⚠️"} **Session Flow**: ${commit.message}`
208
- : "";
209
- const reminder = `\n\n**Beads Insight**: Work completed. Consider if this session produced insights worth persisting via \`bd remember\`.${commitLine}`;
210
- return { content: [...event.content, { type: "text", text: reminder }] };
148
+ // Inject memory gate as agent-visible context only — parity with Claude Stop hook {additionalContext}.
149
+ // No UI notification; the agent sees this silently in its tool result context.
150
+ const memoryGateText = closedIssueId
151
+ ? `\n\n**Beads Memory Gate**: Issue \`${closedIssueId}\` closed this session.\n` +
152
+ `For each candidate insight, check ALL 4:\n` +
153
+ ` 1. Hard to rediscover from code/docs?\n` +
154
+ ` 2. Not obvious from the current implementation?\n` +
155
+ ` 3. Will affect a future decision?\n` +
156
+ ` 4. Still relevant in ~14 days?\n` +
157
+ `KEEP (all 4 yes) → \`bd remember "<insight>"\`\n` +
158
+ `SKIP examples: file maps, flag inventories, per-issue summaries, wording tweaks, facts obvious from reading the source.\n` +
159
+ `When done: \`bd kv set "memory-gate-done:${sessionId}" "saved: <key>"\` (or \`"nothing novel — <reason>"\`)`
160
+ : `\n\n**Beads**: Work completed. Consider if this session produced insights worth persisting via \`bd remember\`.`;
161
+ return { content: [...event.content, { type: "text", text: memoryGateText }] };
211
162
  }
212
163
 
213
164
  return undefined;
214
165
  });
215
166
 
216
- // Memory gate: if this session closed an issue, prompt for insight persistence.
217
- // Uses sendUserMessage to trigger a new turn in Pi (non-blocking alternative to Claude Stop hook).
167
+ // Memory gate: clean up session markers and check ack at agent_end/session_shutdown.
168
+ // Memory gate prompt was already injected into bd close tool_result context (silent, agent-visible only).
169
+ // No UI notification — parity with Claude Stop hook {additionalContext} pattern.
218
170
  const triggerMemoryGateIfNeeded = async (ctx: any) => {
219
171
  const cwd = getCwd(ctx);
220
172
  if (!EventAdapter.isBeadsProject(cwd)) return;
@@ -234,19 +186,7 @@ export default function (pi: ExtensionAPI) {
234
186
  if (!closedIssueId) return;
235
187
 
236
188
  memoryGateFired = true;
237
- pi.sendUserMessage(
238
- `🧠 Memory gate: claim \`${closedIssueId}\` was closed this session.\n` +
239
- `For each candidate insight, check ALL 4:\n` +
240
- ` 1. Hard to rediscover from code/docs?\n` +
241
- ` 2. Not obvious from the current implementation?\n` +
242
- ` 3. Will affect a future decision?\n` +
243
- ` 4. Still relevant in ~14 days?\n` +
244
- `KEEP (all 4 yes) → \`bd remember "<insight>"\`\n` +
245
- `SKIP examples: file maps, flag inventories, per-issue summaries,\n` +
246
- ` wording tweaks, facts obvious from reading the source.\n` +
247
- `KEEP: \`bd kv set "memory-gate-done:${sessionId}" "saved: <key>"\`\n` +
248
- `SKIP: \`bd kv set "memory-gate-done:${sessionId}" "nothing novel — <one-line reason>"\``,
249
- );
189
+ // No notify — memory gate was injected into bd close tool_result content (silent, agent-visible only).
250
190
  };
251
191
 
252
192
  pi.on("agent_end", async (_event, ctx) => {
@@ -264,12 +264,11 @@ export default function (pi: ExtensionAPI) {
264
264
 
265
265
  const BOLD = "\x1b[1m";
266
266
  const BOLD_OFF = "\x1b[22m";
267
- const brand = `${BOLD}${theme.fg("accent", "XTRM")}${BOLD_OFF}`;
267
+ const brand = `${BOLD}${theme.fg("dim", "XTRM")}${BOLD_OFF}`;
268
268
 
269
269
  const usage = ctx.getContextUsage();
270
270
  const pct = usage?.percent ?? 0;
271
- const pctColor = pct > 75 ? "error" : pct > 50 ? "warning" : "success";
272
- const usageStr = theme.fg(pctColor, `[${pct.toFixed(0)}%]`);
271
+ const usageStr = theme.fg("dim", `[${pct.toFixed(0)}%]`);
273
272
 
274
273
  const modelId = ctx.model?.id || "no-model";
275
274
  const modelStr = `${modelId} ${usageStr}`;
@@ -0,0 +1,93 @@
1
+ export interface DiffStats {
2
+ additions: number;
3
+ removals: number;
4
+ }
5
+
6
+ export function shortenHome(path: string): string {
7
+ const home = process.env.HOME;
8
+ if (home && path.startsWith(home)) return `~${path.slice(home.length)}`;
9
+ return path;
10
+ }
11
+
12
+ export function shortenPath(path: string, max = 56): string {
13
+ const normalized = shortenHome(path);
14
+ if (normalized.length <= max) return normalized;
15
+ const parts = normalized.split("/").filter(Boolean);
16
+ if (parts.length <= 2) return `…${normalized.slice(-(max - 1))}`;
17
+ const tail = parts.slice(-2).join("/");
18
+ const head = parts[0]?.startsWith("~") ? "~/" : "…/";
19
+ const candidate = `${head}${tail}`;
20
+ if (candidate.length <= max) return candidate;
21
+ return `…${candidate.slice(-(max - 1))}`;
22
+ }
23
+
24
+ export function shortenCommand(command: string, max = 72): string {
25
+ const singleLine = command.replace(/\s+/g, " ").trim();
26
+ if (singleLine.length <= max) return singleLine;
27
+ return `${singleLine.slice(0, Math.max(0, max - 1))}…`;
28
+ }
29
+
30
+ export function lineCount(text: string): number {
31
+ if (!text) return 0;
32
+ return text.split("\n").length;
33
+ }
34
+
35
+ export function previewLines(text: string, count: number): string[] {
36
+ return text.split("\n").slice(0, count);
37
+ }
38
+
39
+ export function cleanOutputLines(text: string): string[] {
40
+ return text
41
+ .split("\n")
42
+ .filter((line) => line.trim().length > 0)
43
+ .filter((line) => !/^exit code:\s*-?\d+$/i.test(line.trim()));
44
+ }
45
+
46
+ export function countPrefixedItems(text: string, prefixes: string[]): number {
47
+ return text.split("\n").filter((line) => prefixes.some((prefix) => line.startsWith(prefix))).length;
48
+ }
49
+
50
+ export function diffStats(diff: string): DiffStats {
51
+ let additions = 0;
52
+ let removals = 0;
53
+ for (const line of diff.split("\n")) {
54
+ if (line.startsWith("+") && !line.startsWith("+++")) additions++;
55
+ if (line.startsWith("-") && !line.startsWith("---")) removals++;
56
+ }
57
+ return { additions, removals };
58
+ }
59
+
60
+ export function formatDuration(durationMs: number | undefined): string | undefined {
61
+ if (!durationMs || durationMs < 0) return undefined;
62
+ if (durationMs < 1000) return `${durationMs}ms`;
63
+ const seconds = durationMs / 1000;
64
+ if (seconds < 10) return `${seconds.toFixed(1)}s`;
65
+ return `${Math.round(seconds)}s`;
66
+ }
67
+
68
+ export function formatLineLabel(count: number, noun: string): string {
69
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
70
+ }
71
+
72
+ export function renderToolSummary(
73
+ theme: { fg(color: string, text: string): string; bold(text: string): string },
74
+ status: "pending" | "success" | "error" | "muted",
75
+ label: string,
76
+ subject?: string,
77
+ meta?: string,
78
+ ): string {
79
+ const color =
80
+ status === "pending" ? "accent"
81
+ : status === "error" ? "error"
82
+ : status === "success" ? "success"
83
+ : "muted";
84
+ let text = `${theme.fg(color, "•")} ${theme.fg("toolTitle", theme.bold(label))}`;
85
+ if (subject) text += ` ${theme.fg("accent", subject)}`;
86
+ if (meta) text += theme.fg("muted", ` · ${meta}`);
87
+ return text;
88
+ }
89
+
90
+ export function joinMeta(parts: Array<string | undefined | false>): string | undefined {
91
+ const filtered = parts.filter((part): part is string => typeof part === "string" && part.length > 0);
92
+ return filtered.length > 0 ? filtered.join(" · ") : undefined;
93
+ }