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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +24 -5
- package/cli/dist/index.cjs +9812 -9983
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/instructions/agents-top.md +2 -4
- package/config/instructions/claude-top.md +2 -4
- package/config/pi/extensions/beads/index.ts +18 -78
- package/config/pi/extensions/custom-footer/index.ts +2 -3
- package/config/pi/extensions/xtrm-ui/format.ts +93 -0
- package/config/pi/extensions/xtrm-ui/index.ts +1044 -0
- package/config/pi/extensions/xtrm-ui/package.json +10 -0
- package/config/pi/extensions/xtrm-ui/themes/pidex-dark.json +85 -0
- package/config/pi/extensions/xtrm-ui/themes/pidex-light.json +85 -0
- package/config/pi/install-schema.json +0 -1
- package/hooks/beads-claim-sync.mjs +15 -96
- package/hooks/beads-gate-messages.mjs +2 -4
- package/hooks/beads-gate-utils.mjs +0 -18
- package/hooks/statusline.mjs +5 -3
- package/package.json +1 -1
- package/plugins/xtrm-tools/.claude-plugin/plugin.json +1 -1
- package/plugins/xtrm-tools/hooks/beads-claim-sync.mjs +15 -96
- package/plugins/xtrm-tools/hooks/beads-gate-messages.mjs +2 -4
- package/plugins/xtrm-tools/hooks/beads-gate-utils.mjs +0 -18
- package/plugins/xtrm-tools/hooks/statusline.mjs +5 -3
- package/plugins/xtrm-tools/skills/planning/SKILL.md +75 -20
- package/plugins/xtrm-tools/skills/using-xtrm/SKILL.md +1 -1
- package/plugins/xtrm-tools/skills/xt-debugging/SKILL.md +149 -0
- package/plugins/xtrm-tools/skills/xt-end/SKILL.md +28 -0
- package/skills/planning/SKILL.md +75 -20
- package/skills/using-xtrm/SKILL.md +1 -1
- package/skills/xt-debugging/SKILL.md +149 -0
- package/skills/xt-end/SKILL.md +28 -0
- package/plugins/xtrm-tools/skills/gitnexus-debugging/SKILL.md +0 -85
- package/skills/gitnexus-debugging/SKILL.md +0 -85
package/cli/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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:
|
|
217
|
-
//
|
|
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
|
-
|
|
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("
|
|
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
|
|
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
|
+
}
|