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/dist/index.cjs +48 -1
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/hooks.json +5 -0
- package/config/instructions/agents-top.md +30 -0
- package/config/instructions/claude-top.md +30 -0
- package/config/pi/extensions/beads.ts +22 -7
- package/config/pi/extensions/custom-footer.ts +15 -13
- package/config/pi/pi-worktrees-settings.json +6 -0
- package/config/pi/settings.json.template +2 -1
- package/hooks/beads-claim-sync.mjs +82 -0
- package/package.json +1 -1
package/cli/package.json
CHANGED
package/config/hooks.json
CHANGED
|
@@ -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
|
-
|
|
14
|
-
const sessionId = process.pid.toString();
|
|
13
|
+
let cachedSessionId: string | null = null;
|
|
15
14
|
|
|
16
|
-
|
|
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 (
|
|
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
|
|
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,
|
|
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;
|
|
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:${
|
|
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:${
|
|
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
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
const brandModel = `${brand} ${modelChip}`;
|
|
131
|
+
const leftParts = [brandModel, usageStr, cwdStr];
|
|
132
|
+
|
|
132
133
|
const beadChip = buildBeadChip();
|
|
133
|
-
|
|
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
|
|
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,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();
|