xtrm-tools 0.5.26 → 0.5.28
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 +5 -0
- package/README.md +8 -1
- package/cli/dist/index.cjs +263 -306
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/hooks.json +11 -10
- package/config/pi/extensions/beads/index.ts +103 -77
- package/config/pi/extensions/custom-footer/index.ts +245 -49
- package/config/pi/extensions/quality-gates/index.ts +28 -29
- package/config/pi/extensions/session-flow/index.ts +50 -21
- package/config/pi/extensions/xtrm-loader/index.ts +38 -24
- package/hooks/hooks.json +14 -0
- package/package.json +1 -1
- package/plugins/xtrm-tools/.claude-plugin/plugin.json +1 -1
- package/plugins/xtrm-tools/hooks/hooks.json +14 -0
- package/plugins/xtrm-tools/skills/planning/SKILL.md +350 -0
- package/plugins/xtrm-tools/skills/planning/evals/evals.json +19 -0
- package/skills/planning/SKILL.md +350 -0
- package/skills/planning/evals/evals.json +19 -0
- package/config/pi/extensions/plan-mode/README.md +0 -65
- package/config/pi/extensions/plan-mode/index.ts +0 -417
- package/config/pi/extensions/plan-mode/package.json +0 -12
- package/config/pi/extensions/plan-mode/utils.ts +0 -324
package/cli/package.json
CHANGED
package/config/hooks.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
3
|
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"script": "using-xtrm-reminder.mjs"
|
|
6
|
+
},
|
|
4
7
|
{
|
|
5
8
|
"script": "beads-compact-restore.mjs",
|
|
6
9
|
"timeout": 5000
|
|
7
10
|
},
|
|
8
11
|
{
|
|
9
|
-
"script": "
|
|
10
|
-
"timeout":
|
|
11
|
-
}
|
|
12
|
-
],
|
|
13
|
-
"UserPromptSubmit": [
|
|
12
|
+
"script": "quality-check-env.mjs",
|
|
13
|
+
"timeout": 5000
|
|
14
|
+
},
|
|
14
15
|
{
|
|
15
|
-
"script": "
|
|
16
|
+
"script": "xtrm-session-logger.mjs",
|
|
16
17
|
"timeout": 3000
|
|
17
18
|
}
|
|
18
19
|
],
|
|
@@ -40,17 +41,17 @@
|
|
|
40
41
|
"timeout": 5000
|
|
41
42
|
},
|
|
42
43
|
{
|
|
43
|
-
"matcher": "Write|
|
|
44
|
+
"matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
44
45
|
"script": "quality-check.cjs",
|
|
45
46
|
"timeout": 30000
|
|
46
47
|
},
|
|
47
48
|
{
|
|
48
|
-
"matcher": "Write|
|
|
49
|
+
"matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
49
50
|
"script": "quality-check.py",
|
|
50
51
|
"timeout": 30000
|
|
51
52
|
},
|
|
52
53
|
{
|
|
53
|
-
"matcher": "Bash|mcp__serena__find_symbol|mcp__serena__get_symbols_overview|mcp__serena__search_for_pattern|mcp__serena__find_referencing_symbols|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol|mcp__serena__rename_symbol",
|
|
54
|
+
"matcher": "Bash|Grep|Read|Glob|mcp__serena__find_symbol|mcp__serena__get_symbols_overview|mcp__serena__search_for_pattern|mcp__serena__find_referencing_symbols|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol|mcp__serena__rename_symbol",
|
|
54
55
|
"script": "gitnexus/gitnexus-hook.cjs",
|
|
55
56
|
"timeout": 10000
|
|
56
57
|
},
|
|
@@ -77,6 +78,6 @@
|
|
|
77
78
|
]
|
|
78
79
|
},
|
|
79
80
|
"statusLine": {
|
|
80
|
-
"script": "statusline
|
|
81
|
+
"script": "statusline.mjs"
|
|
81
82
|
}
|
|
82
83
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { isToolCallEventType, isBashToolResult } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { SubprocessRunner, EventAdapter } from "../core/lib";
|
|
6
6
|
|
|
7
7
|
export default function (pi: ExtensionAPI) {
|
|
8
8
|
const getCwd = (ctx: any) => ctx.cwd || process.cwd();
|
|
@@ -21,8 +21,51 @@ export default function (pi: ExtensionAPI) {
|
|
|
21
21
|
|
|
22
22
|
const getSessionClaim = async (sessionId: string, cwd: string): Promise<string | null> => {
|
|
23
23
|
const result = await SubprocessRunner.run("bd", ["kv", "get", `claimed:${sessionId}`], { cwd });
|
|
24
|
-
if (result.code
|
|
25
|
-
|
|
24
|
+
if (result.code !== 0) return null;
|
|
25
|
+
const claim = result.stdout.trim();
|
|
26
|
+
return claim.length > 0 ? claim : null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const clearClaimMarker = async (sessionId: string, cwd: string) => {
|
|
30
|
+
await SubprocessRunner.run("bd", ["kv", "clear", `claimed:${sessionId}`], { cwd });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const isIssueInProgress = async (issueId: string, cwd: string): Promise<boolean | null> => {
|
|
34
|
+
const result = await SubprocessRunner.run("bd", ["show", issueId, "--json"], { cwd });
|
|
35
|
+
if (result.code !== 0 || !result.stdout.trim()) return null;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(result.stdout);
|
|
38
|
+
const issue = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
39
|
+
if (!issue?.status) return null;
|
|
40
|
+
return issue.status === "in_progress";
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getActiveClaim = async (sessionId: string, cwd: string): Promise<string | null> => {
|
|
47
|
+
const claim = await getSessionClaim(sessionId, cwd);
|
|
48
|
+
if (!claim) return null;
|
|
49
|
+
|
|
50
|
+
const inProgress = await isIssueInProgress(claim, cwd);
|
|
51
|
+
if (inProgress === false) {
|
|
52
|
+
await clearClaimMarker(sessionId, cwd);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return claim;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getClosedThisSession = async (sessionId: string, cwd: string): Promise<string | null> => {
|
|
60
|
+
const result = await SubprocessRunner.run("bd", ["kv", "get", `closed-this-session:${sessionId}`], { cwd });
|
|
61
|
+
if (result.code !== 0) return null;
|
|
62
|
+
const issue = result.stdout.trim();
|
|
63
|
+
return issue.length > 0 ? issue : null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const clearSessionMarkers = async (sessionId: string, cwd: string) => {
|
|
67
|
+
await SubprocessRunner.run("bd", ["kv", "clear", `claimed:${sessionId}`], { cwd });
|
|
68
|
+
await SubprocessRunner.run("bd", ["kv", "clear", `closed-this-session:${sessionId}`], { cwd });
|
|
26
69
|
};
|
|
27
70
|
|
|
28
71
|
const hasTrackableWork = async (cwd: string): Promise<boolean> => {
|
|
@@ -34,15 +77,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
34
77
|
return false;
|
|
35
78
|
};
|
|
36
79
|
|
|
37
|
-
const hasInProgressWork = async (cwd: string): Promise<boolean> => {
|
|
38
|
-
const result = await SubprocessRunner.run("bd", ["list"], { cwd });
|
|
39
|
-
if (result.code === 0 && result.stdout.includes("Total:")) {
|
|
40
|
-
const m = result.stdout.match(/Total:\s*\d+\s+issues?\s*\((\d+)\s+open,\s*(\d+)\s+in progress\)/);
|
|
41
|
-
if (m) return parseInt(m[2], 10) > 0;
|
|
42
|
-
}
|
|
43
|
-
return false;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
80
|
pi.on("session_start", async (_event, ctx) => {
|
|
47
81
|
cachedSessionId = ctx?.sessionManager?.getSessionId?.() ?? ctx?.sessionId ?? ctx?.session_id ?? cachedSessionId;
|
|
48
82
|
return undefined;
|
|
@@ -54,33 +88,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
54
88
|
const sessionId = getSessionId(ctx);
|
|
55
89
|
|
|
56
90
|
if (EventAdapter.isMutatingFileTool(event)) {
|
|
57
|
-
const claim = await
|
|
91
|
+
const claim = await getActiveClaim(sessionId, cwd);
|
|
58
92
|
if (!claim) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
93
|
+
const hasWork = await hasTrackableWork(cwd);
|
|
94
|
+
if (hasWork) {
|
|
95
|
+
if (ctx.hasUI) {
|
|
96
|
+
ctx.ui.notify("Beads: Edit blocked. Claim an issue first.", "warning");
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
block: true,
|
|
100
|
+
reason: `No active claim for session ${sessionId}.\n bd update <id> --claim\n`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
70
104
|
}
|
|
71
105
|
|
|
72
106
|
if (isToolCallEventType("bash", event)) {
|
|
73
107
|
const command = event.input.command;
|
|
74
108
|
if (command && /\bgit\s+commit\b/.test(command)) {
|
|
75
|
-
const claim = await
|
|
109
|
+
const claim = await getActiveClaim(sessionId, cwd);
|
|
76
110
|
if (claim) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
reason: `Active claim [${claim}] — close it first.\n bd close ${claim}\n (Pi workflow) publish/merge are external steps; do not rely on xtrm finish.\n`,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
111
|
+
return {
|
|
112
|
+
block: true,
|
|
113
|
+
reason: `Active claim [${claim}] — close it first.\n bd close ${claim}\n (Pi workflow) publish/merge are external steps; do not rely on xtrm finish.\n`,
|
|
114
|
+
};
|
|
84
115
|
}
|
|
85
116
|
}
|
|
86
117
|
}
|
|
@@ -89,65 +120,61 @@ export default function (pi: ExtensionAPI) {
|
|
|
89
120
|
});
|
|
90
121
|
|
|
91
122
|
pi.on("tool_result", async (event, ctx) => {
|
|
92
|
-
if (isBashToolResult(event))
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
123
|
+
if (!isBashToolResult(event)) return undefined;
|
|
124
|
+
|
|
125
|
+
const command = event.input.command || "";
|
|
126
|
+
const sessionId = getSessionId(ctx);
|
|
127
|
+
const cwd = getCwd(ctx);
|
|
128
|
+
|
|
129
|
+
// Auto-claim on bd update --claim regardless of exit code.
|
|
130
|
+
if (/\bbd\s+update\b/.test(command) && /--claim\b/.test(command)) {
|
|
131
|
+
const issueMatch = command.match(/\bbd\s+update\s+(\S+)/);
|
|
132
|
+
if (issueMatch) {
|
|
133
|
+
const issueId = issueMatch[1];
|
|
134
|
+
await SubprocessRunner.run("bd", ["kv", "set", `claimed:${sessionId}`, issueId], { cwd });
|
|
135
|
+
memoryGateFired = false;
|
|
136
|
+
const claimNotice = `\n\n✅ **Beads**: Session \`${sessionId}\` claimed issue \`${issueId}\`. File edits are now unblocked.`;
|
|
137
|
+
return { content: [...event.content, { type: "text", text: claimNotice }] };
|
|
107
138
|
}
|
|
139
|
+
}
|
|
108
140
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
141
|
+
if (/\bbd\s+close\b/.test(command) && !event.isError) {
|
|
142
|
+
const closeMatch = command.match(/\bbd\s+close\s+(\S+)/);
|
|
143
|
+
const closedIssueId = closeMatch?.[1] ?? null;
|
|
144
|
+
if (closedIssueId) {
|
|
145
|
+
await SubprocessRunner.run("bd", ["kv", "set", `closed-this-session:${sessionId}`, closedIssueId], { cwd });
|
|
146
|
+
memoryGateFired = false;
|
|
113
147
|
}
|
|
148
|
+
const reminder = "\n\n**Beads Insight**: Work completed. Consider if this session produced insights worth persisting via `bd remember`.";
|
|
149
|
+
return { content: [...event.content, { type: "text", text: reminder }] };
|
|
114
150
|
}
|
|
151
|
+
|
|
115
152
|
return undefined;
|
|
116
153
|
});
|
|
117
154
|
|
|
118
|
-
//
|
|
119
|
-
|
|
155
|
+
// Memory gate: if this session closed an issue, prompt for insight persistence.
|
|
156
|
+
// Uses sendUserMessage to trigger a new turn in Pi (non-blocking alternative to Claude Stop hook).
|
|
157
|
+
const triggerMemoryGateIfNeeded = async (ctx: any) => {
|
|
120
158
|
const cwd = getCwd(ctx);
|
|
121
159
|
if (!EventAdapter.isBeadsProject(cwd)) return;
|
|
122
160
|
const sessionId = getSessionId(ctx);
|
|
123
|
-
const claim = await getSessionClaim(sessionId, cwd);
|
|
124
|
-
if (!claim) return;
|
|
125
161
|
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
162
|
+
const markerPath = join(cwd, ".beads", ".memory-gate-done");
|
|
163
|
+
if (existsSync(markerPath)) {
|
|
164
|
+
try { unlinkSync(markerPath); } catch { /* ignore */ }
|
|
165
|
+
await clearSessionMarkers(sessionId, cwd);
|
|
166
|
+
memoryGateFired = false;
|
|
167
|
+
return;
|
|
131
168
|
}
|
|
132
|
-
};
|
|
133
169
|
|
|
134
|
-
// Memory gate: if the session's claimed issue was closed, prompt for insights.
|
|
135
|
-
// Uses sendUserMessage to trigger a new agent turn (Pi has no blocking stop hook).
|
|
136
|
-
// memoryGateFired prevents re-triggering on the follow-up agent_end.
|
|
137
|
-
const triggerMemoryGateIfNeeded = async (ctx: any) => {
|
|
138
170
|
if (memoryGateFired) return;
|
|
139
|
-
const cwd = getCwd(ctx);
|
|
140
|
-
if (!EventAdapter.isBeadsProject(cwd)) return;
|
|
141
|
-
const sessionId = getSessionId(ctx);
|
|
142
|
-
const claimId = await getSessionClaim(sessionId, cwd);
|
|
143
|
-
if (!claimId) return;
|
|
144
171
|
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
172
|
+
const closedIssueId = await getClosedThisSession(sessionId, cwd);
|
|
173
|
+
if (!closedIssueId) return;
|
|
147
174
|
|
|
148
175
|
memoryGateFired = true;
|
|
149
176
|
pi.sendUserMessage(
|
|
150
|
-
`🧠 Memory gate: claim \`${
|
|
177
|
+
`🧠 Memory gate: claim \`${closedIssueId}\` was closed this session.\n` +
|
|
151
178
|
`For each closed issue, worth persisting?\n` +
|
|
152
179
|
` YES → \`bd remember "<insight>"\`\n` +
|
|
153
180
|
` NO → note "nothing to persist"\n` +
|
|
@@ -156,13 +183,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
156
183
|
};
|
|
157
184
|
|
|
158
185
|
pi.on("agent_end", async (_event, ctx) => {
|
|
159
|
-
await notifySessionEnd(ctx);
|
|
160
186
|
await triggerMemoryGateIfNeeded(ctx);
|
|
161
187
|
return undefined;
|
|
162
188
|
});
|
|
163
189
|
|
|
164
190
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
165
|
-
await
|
|
191
|
+
await triggerMemoryGateIfNeeded(ctx);
|
|
166
192
|
return undefined;
|
|
167
193
|
});
|
|
168
194
|
}
|