xtrm-tools 2.4.2 → 2.4.4
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 +958 -916
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/pi/extensions/beads.ts +1 -1
- package/config/pi/extensions/core/guard-rules.ts +34 -2
- package/config/pi/extensions/custom-footer.ts +5 -5
- package/config/pi/extensions/plan-mode/index.ts +91 -14
- package/config/pi/extensions/plan-mode/package.json +12 -0
- package/config/pi/extensions/plan-mode/utils.ts +158 -2
- package/config/pi/extensions/session-flow.ts +18 -4
- package/hooks/beads-claim-sync.mjs +64 -118
- package/hooks/beads-commit-gate.mjs +19 -4
- package/hooks/beads-compact-restore.mjs +2 -24
- package/hooks/beads-compact-save.mjs +1 -27
- package/hooks/beads-edit-gate.mjs +9 -2
- package/hooks/beads-gate-core.mjs +13 -9
- package/hooks/beads-gate-messages.mjs +5 -23
- package/hooks/beads-gate-utils.mjs +65 -1
- package/hooks/beads-memory-gate.mjs +23 -16
- package/hooks/beads-stop-gate.mjs +1 -52
- package/hooks/branch-state.mjs +2 -1
- package/hooks/guard-rules.mjs +33 -1
- package/hooks/hooks.json +22 -42
- package/hooks/main-guard.mjs +36 -35
- package/package.json +1 -1
- package/config/pi/extensions/main-guard-post-push.ts +0 -44
- package/config/pi/extensions/main-guard.ts +0 -122
- package/hooks/session-state.mjs +0 -138
package/cli/package.json
CHANGED
|
@@ -78,7 +78,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
78
78
|
if (inProgress) {
|
|
79
79
|
return {
|
|
80
80
|
block: true,
|
|
81
|
-
reason: `Active claim [${claim}] — close it first.\n bd close ${claim}\n
|
|
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
82
|
};
|
|
83
83
|
}
|
|
84
84
|
}
|
|
@@ -16,6 +16,7 @@ export const PI_MUTATING_FILE_TOOLS = [
|
|
|
16
16
|
];
|
|
17
17
|
|
|
18
18
|
export const SAFE_BASH_PREFIXES = [
|
|
19
|
+
// Git read-only
|
|
19
20
|
"git status",
|
|
20
21
|
"git log",
|
|
21
22
|
"git diff",
|
|
@@ -30,10 +31,41 @@ export const SAFE_BASH_PREFIXES = [
|
|
|
30
31
|
"git worktree",
|
|
31
32
|
"git checkout -b",
|
|
32
33
|
"git switch -c",
|
|
34
|
+
// Tools
|
|
33
35
|
"gh",
|
|
34
36
|
"bd",
|
|
35
|
-
"touch .beads/",
|
|
36
37
|
"npx gitnexus",
|
|
38
|
+
"xtrm finish",
|
|
39
|
+
// Read-only filesystem
|
|
40
|
+
"cat",
|
|
41
|
+
"ls",
|
|
42
|
+
"head",
|
|
43
|
+
"tail",
|
|
44
|
+
"pwd",
|
|
45
|
+
"which",
|
|
46
|
+
"type",
|
|
47
|
+
"env",
|
|
48
|
+
"printenv",
|
|
49
|
+
"find",
|
|
50
|
+
"grep",
|
|
51
|
+
"rg",
|
|
52
|
+
"fd",
|
|
53
|
+
"wc",
|
|
54
|
+
"sort",
|
|
55
|
+
"uniq",
|
|
56
|
+
"cut",
|
|
57
|
+
"awk",
|
|
58
|
+
"jq",
|
|
59
|
+
"yq",
|
|
60
|
+
"bat",
|
|
61
|
+
"less",
|
|
62
|
+
"more",
|
|
63
|
+
"file",
|
|
64
|
+
"stat",
|
|
65
|
+
"du",
|
|
66
|
+
"tree",
|
|
67
|
+
// Allowed writes (specific paths)
|
|
68
|
+
"touch .beads/",
|
|
37
69
|
];
|
|
38
70
|
|
|
39
71
|
export const DANGEROUS_BASH_PATTERNS = [
|
|
@@ -67,4 +99,4 @@ export const DANGEROUS_BASH_PATTERNS = [
|
|
|
67
99
|
"(?:^|\\s)python\\s+-c\\b",
|
|
68
100
|
"(?:^|\\s)perl\\s+-e\\b",
|
|
69
101
|
"(?:^|\\s)ruby\\s+-e\\b",
|
|
70
|
-
];
|
|
102
|
+
];
|
|
@@ -132,14 +132,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
132
132
|
const modelId = ctx.model?.id || "no-model";
|
|
133
133
|
const modelChip = chip(modelId);
|
|
134
134
|
|
|
135
|
-
const sep =
|
|
135
|
+
const sep = " ";
|
|
136
136
|
|
|
137
137
|
const brandModel = `${brand} ${modelChip}`;
|
|
138
|
-
const leftParts = [brandModel, usageStr, cwdStr];
|
|
139
|
-
|
|
140
138
|
const beadChip = buildBeadChip();
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
139
|
+
const leftParts = [brandModel, usageStr];
|
|
140
|
+
if (beadChip) leftParts.push(beadChip);
|
|
141
|
+
leftParts.push(cwdStr);
|
|
142
|
+
if (branchStr) leftParts.push(branchStr);
|
|
143
143
|
|
|
144
144
|
const left = leftParts.join(sep);
|
|
145
145
|
return [truncateToWidth(left, width)];
|
|
@@ -16,7 +16,17 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
|
16
16
|
import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
|
|
17
17
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
18
18
|
import { Key } from "@mariozechner/pi-tui";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
extractTodoItems,
|
|
21
|
+
isSafeCommand,
|
|
22
|
+
markCompletedSteps,
|
|
23
|
+
type TodoItem,
|
|
24
|
+
getShortId,
|
|
25
|
+
isBeadsProject,
|
|
26
|
+
deriveEpicTitle,
|
|
27
|
+
createPlanIssues,
|
|
28
|
+
bdClaim,
|
|
29
|
+
} from "./utils.js";
|
|
20
30
|
|
|
21
31
|
// Tools
|
|
22
32
|
const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
|
|
@@ -39,6 +49,8 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
39
49
|
let planModeEnabled = false;
|
|
40
50
|
let executionMode = false;
|
|
41
51
|
let todoItems: TodoItem[] = [];
|
|
52
|
+
let epicId: string | null = null;
|
|
53
|
+
let issueIds: Map<number, string> = new Map(); // step -> issue ID
|
|
42
54
|
|
|
43
55
|
pi.registerFlag("plan", {
|
|
44
56
|
description: "Start in plan mode (read-only exploration)",
|
|
@@ -57,15 +69,17 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
57
69
|
ctx.ui.setStatus("plan-mode", undefined);
|
|
58
70
|
}
|
|
59
71
|
|
|
60
|
-
// Widget showing todo list
|
|
72
|
+
// Widget showing todo list with bd issue IDs
|
|
61
73
|
if (executionMode && todoItems.length > 0) {
|
|
62
74
|
const lines = todoItems.map((item) => {
|
|
75
|
+
const issueId = issueIds.get(item.step);
|
|
76
|
+
const idLabel = issueId ? `[${getShortId(issueId)}] ` : "";
|
|
63
77
|
if (item.completed) {
|
|
64
78
|
return (
|
|
65
|
-
ctx.ui.theme.fg("success", "☑ ") + ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(item.text))
|
|
79
|
+
ctx.ui.theme.fg("success", "☑ ") + ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(`${idLabel}${item.text}`))
|
|
66
80
|
);
|
|
67
81
|
}
|
|
68
|
-
return `${ctx.ui.theme.fg("muted", "☐ ")}${item.text}`;
|
|
82
|
+
return `${ctx.ui.theme.fg("muted", "☐ ")}${idLabel}${item.text}`;
|
|
69
83
|
});
|
|
70
84
|
ctx.ui.setWidget("plan-todos", lines);
|
|
71
85
|
} else {
|
|
@@ -77,6 +91,8 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
77
91
|
planModeEnabled = !planModeEnabled;
|
|
78
92
|
executionMode = false;
|
|
79
93
|
todoItems = [];
|
|
94
|
+
epicId = null;
|
|
95
|
+
issueIds.clear();
|
|
80
96
|
|
|
81
97
|
if (planModeEnabled) {
|
|
82
98
|
pi.setActiveTools(PLAN_MODE_TOOLS);
|
|
@@ -93,6 +109,8 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
93
109
|
enabled: planModeEnabled,
|
|
94
110
|
todos: todoItems,
|
|
95
111
|
executing: executionMode,
|
|
112
|
+
epicId,
|
|
113
|
+
issueIds: Object.fromEntries(issueIds),
|
|
96
114
|
});
|
|
97
115
|
}
|
|
98
116
|
|
|
@@ -108,7 +126,11 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
108
126
|
ctx.ui.notify("No todos. Create a plan first with /plan", "info");
|
|
109
127
|
return;
|
|
110
128
|
}
|
|
111
|
-
const list = todoItems.map((item, i) =>
|
|
129
|
+
const list = todoItems.map((item, i) => {
|
|
130
|
+
const issueId = issueIds.get(item.step);
|
|
131
|
+
const idLabel = issueId ? `[${getShortId(issueId)}] ` : "";
|
|
132
|
+
return `${i + 1}. ${item.completed ? "✓" : "○"} ${idLabel}${item.text}`;
|
|
133
|
+
}).join("\n");
|
|
112
134
|
ctx.ui.notify(`Plan Progress:\n${list}`, "info");
|
|
113
135
|
},
|
|
114
136
|
});
|
|
@@ -187,7 +209,11 @@ Do NOT attempt to make changes - just describe what you would do.`,
|
|
|
187
209
|
|
|
188
210
|
if (executionMode && todoItems.length > 0) {
|
|
189
211
|
const remaining = todoItems.filter((t) => !t.completed);
|
|
190
|
-
const todoList = remaining.map((t) =>
|
|
212
|
+
const todoList = remaining.map((t) => {
|
|
213
|
+
const issueId = issueIds.get(t.step);
|
|
214
|
+
const idLabel = issueId ? `[${getShortId(issueId)}] ` : "";
|
|
215
|
+
return `${t.step}. ${idLabel}${t.text}`;
|
|
216
|
+
}).join("\n");
|
|
191
217
|
return {
|
|
192
218
|
message: {
|
|
193
219
|
customType: "plan-execution-context",
|
|
@@ -221,13 +247,19 @@ After completing a step, include a [DONE:n] tag in your response.`,
|
|
|
221
247
|
// Check if execution is complete
|
|
222
248
|
if (executionMode && todoItems.length > 0) {
|
|
223
249
|
if (todoItems.every((t) => t.completed)) {
|
|
224
|
-
const completedList = todoItems.map((t) =>
|
|
250
|
+
const completedList = todoItems.map((t) => {
|
|
251
|
+
const issueId = issueIds.get(t.step);
|
|
252
|
+
const idLabel = issueId ? `[${getShortId(issueId)}] ` : "";
|
|
253
|
+
return `~~${idLabel}${t.text}~~`;
|
|
254
|
+
}).join("\n");
|
|
225
255
|
pi.sendMessage(
|
|
226
256
|
{ customType: "plan-complete", content: `**Plan Complete!** ✓\n\n${completedList}`, display: true },
|
|
227
257
|
{ triggerTurn: false },
|
|
228
258
|
);
|
|
229
259
|
executionMode = false;
|
|
230
260
|
todoItems = [];
|
|
261
|
+
epicId = null;
|
|
262
|
+
issueIds.clear();
|
|
231
263
|
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
232
264
|
updateStatus(ctx);
|
|
233
265
|
persistState(); // Save cleared state so resume doesn't restore old execution mode
|
|
@@ -246,31 +278,72 @@ After completing a step, include a [DONE:n] tag in your response.`,
|
|
|
246
278
|
}
|
|
247
279
|
}
|
|
248
280
|
|
|
249
|
-
//
|
|
281
|
+
// Auto-create epic + issues if in a beads project
|
|
282
|
+
const cwd = ctx.cwd || process.cwd();
|
|
283
|
+
if (todoItems.length > 0 && isBeadsProject(cwd) && !epicId) {
|
|
284
|
+
// Derive epic title from user prompt or first step
|
|
285
|
+
const epicTitle = deriveEpicTitle(event.messages);
|
|
286
|
+
|
|
287
|
+
ctx.ui.notify("Creating epic and issues in bd...", "info");
|
|
288
|
+
const result = await createPlanIssues(epicTitle, todoItems, cwd);
|
|
289
|
+
|
|
290
|
+
if (result) {
|
|
291
|
+
epicId = result.epic.id;
|
|
292
|
+
for (const issue of result.issues) {
|
|
293
|
+
// Match issue to todo by title similarity
|
|
294
|
+
const matchingTodo = todoItems.find(t =>
|
|
295
|
+
issue.title.toLowerCase().includes(t.text.toLowerCase().slice(0, 30)) ||
|
|
296
|
+
t.text.toLowerCase().includes(issue.title.toLowerCase().slice(0, 30))
|
|
297
|
+
);
|
|
298
|
+
if (matchingTodo) {
|
|
299
|
+
issueIds.set(matchingTodo.step, issue.id);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
ctx.ui.notify(`Created epic ${getShortId(epicId)} with ${result.issues.length} issues`, "info");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Show plan steps with bd issue IDs
|
|
250
307
|
if (todoItems.length > 0) {
|
|
251
|
-
const todoListText = todoItems.map((t, i) =>
|
|
308
|
+
const todoListText = todoItems.map((t, i) => {
|
|
309
|
+
const issueId = issueIds.get(t.step);
|
|
310
|
+
const idLabel = issueId ? `[${getShortId(issueId)}] ` : "";
|
|
311
|
+
return `${i + 1}. ☐ ${idLabel}${t.text}`;
|
|
312
|
+
}).join("\n");
|
|
313
|
+
|
|
314
|
+
const epicLabel = epicId ? `\n\nEpic: ${getShortId(epicId)}` : "";
|
|
252
315
|
pi.sendMessage(
|
|
253
316
|
{
|
|
254
317
|
customType: "plan-todo-list",
|
|
255
|
-
content: `**Plan Steps (${todoItems.length})
|
|
318
|
+
content: `**Plan Steps (${todoItems.length}):**${epicLabel}\n\n${todoListText}`,
|
|
256
319
|
display: true,
|
|
257
320
|
},
|
|
258
321
|
{ triggerTurn: false },
|
|
259
322
|
);
|
|
260
323
|
}
|
|
261
324
|
|
|
262
|
-
|
|
263
|
-
|
|
325
|
+
// Show "Start implementation?" prompt (no approval dialog for issue creation)
|
|
326
|
+
const choice = await ctx.ui.select("Start implementation?", [
|
|
327
|
+
todoItems.length > 0 ? "Yes, start with first step" : "Yes",
|
|
264
328
|
"Stay in plan mode",
|
|
265
329
|
"Refine the plan",
|
|
266
330
|
]);
|
|
267
331
|
|
|
268
|
-
if (choice?.startsWith("
|
|
332
|
+
if (choice?.startsWith("Yes")) {
|
|
333
|
+
// Auto-exit plan mode and start execution
|
|
269
334
|
planModeEnabled = false;
|
|
270
335
|
executionMode = todoItems.length > 0;
|
|
271
336
|
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
272
337
|
updateStatus(ctx);
|
|
273
338
|
|
|
339
|
+
// Auto-claim first issue if available
|
|
340
|
+
if (todoItems.length > 0 && issueIds.size > 0) {
|
|
341
|
+
const firstIssueId = issueIds.get(todoItems[0].step);
|
|
342
|
+
if (firstIssueId) {
|
|
343
|
+
await bdClaim(firstIssueId, cwd);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
274
347
|
const execMessage =
|
|
275
348
|
todoItems.length > 0
|
|
276
349
|
? `Execute the plan. Start with: ${todoItems[0].text}`
|
|
@@ -298,12 +371,16 @@ After completing a step, include a [DONE:n] tag in your response.`,
|
|
|
298
371
|
// Restore persisted state
|
|
299
372
|
const planModeEntry = entries
|
|
300
373
|
.filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode")
|
|
301
|
-
.pop() as { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } } | undefined;
|
|
374
|
+
.pop() as { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean; epicId?: string; issueIds?: Record<number, string> } } | undefined;
|
|
302
375
|
|
|
303
376
|
if (planModeEntry?.data) {
|
|
304
377
|
planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled;
|
|
305
378
|
todoItems = planModeEntry.data.todos ?? todoItems;
|
|
306
379
|
executionMode = planModeEntry.data.executing ?? executionMode;
|
|
380
|
+
epicId = planModeEntry.data.epicId ?? null;
|
|
381
|
+
if (planModeEntry.data.issueIds) {
|
|
382
|
+
issueIds = new Map(Object.entries(planModeEntry.data.issueIds).map(([k, v]) => [Number(k), v]));
|
|
383
|
+
}
|
|
307
384
|
}
|
|
308
385
|
|
|
309
386
|
// On resume: re-scan messages to rebuild completion state
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xtrm/pi-plan-mode",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Plan mode extension for Pi - read-only exploration with plan extraction and bd integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.ts"
|
|
8
|
+
},
|
|
9
|
+
"keywords": ["pi", "extension", "plan-mode", "xtrm"],
|
|
10
|
+
"author": "xtrm",
|
|
11
|
+
"license": "MIT"
|
|
12
|
+
}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* Extracted for testability.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
|
|
6
10
|
// Destructive commands blocked in plan mode
|
|
7
11
|
const DESTRUCTIVE_PATTERNS = [
|
|
8
12
|
/\brm\b/i,
|
|
@@ -134,7 +138,8 @@ export function extractTodoItems(message: string): TodoItem[] {
|
|
|
134
138
|
const planSection = message.slice(message.indexOf(headerMatch[0]) + headerMatch[0].length);
|
|
135
139
|
const numberedPattern = /^\s*(\d+)[.)]\s+\*{0,2}([^*\n]+)/gm;
|
|
136
140
|
|
|
137
|
-
|
|
141
|
+
const matches = Array.from(planSection.matchAll(numberedPattern));
|
|
142
|
+
for (const match of matches) {
|
|
138
143
|
const text = match[2]
|
|
139
144
|
.trim()
|
|
140
145
|
.replace(/\*{1,2}$/, "")
|
|
@@ -151,7 +156,8 @@ export function extractTodoItems(message: string): TodoItem[] {
|
|
|
151
156
|
|
|
152
157
|
export function extractDoneSteps(message: string): number[] {
|
|
153
158
|
const steps: number[] = [];
|
|
154
|
-
|
|
159
|
+
const matches = Array.from(message.matchAll(/\[DONE:(\d+)\]/gi));
|
|
160
|
+
for (const match of matches) {
|
|
155
161
|
const step = Number(match[1]);
|
|
156
162
|
if (Number.isFinite(step)) steps.push(step);
|
|
157
163
|
}
|
|
@@ -166,3 +172,153 @@ export function markCompletedSteps(text: string, items: TodoItem[]): number {
|
|
|
166
172
|
}
|
|
167
173
|
return doneSteps.length;
|
|
168
174
|
}
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// bd (beads) Integration Functions
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Extract short ID from full bd issue ID.
|
|
182
|
+
* Example: "jaggers-agent-tools-xr9b.1" → "xr9b.1"
|
|
183
|
+
*/
|
|
184
|
+
export function getShortId(fullId: string): string {
|
|
185
|
+
const parts = fullId.split("-");
|
|
186
|
+
// Last part is the ID (e.g., "xr9b.1")
|
|
187
|
+
return parts[parts.length - 1];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if a directory is a beads project (has .beads directory).
|
|
192
|
+
*/
|
|
193
|
+
export function isBeadsProject(cwd: string): boolean {
|
|
194
|
+
return existsSync(join(cwd, ".beads"));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Derive epic title from user prompt or conversation messages.
|
|
199
|
+
*/
|
|
200
|
+
export function deriveEpicTitle(messages: Array<{ role: string; content?: unknown }>): string {
|
|
201
|
+
// Find the last user message
|
|
202
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
203
|
+
const msg = messages[i];
|
|
204
|
+
if (msg.role === "user") {
|
|
205
|
+
const content = msg.content;
|
|
206
|
+
if (typeof content === "string") {
|
|
207
|
+
// Extract first sentence or first 50 chars
|
|
208
|
+
const firstSentence = content.split(/[.!?\n]/)[0].trim();
|
|
209
|
+
if (firstSentence.length > 10 && firstSentence.length < 80) {
|
|
210
|
+
return firstSentence;
|
|
211
|
+
}
|
|
212
|
+
if (firstSentence.length >= 80) {
|
|
213
|
+
return `${firstSentence.slice(0, 77)}...`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return "Plan execution";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Run a bd command and return the result.
|
|
223
|
+
*/
|
|
224
|
+
function runBd(args: string[], cwd: string): { stdout: string; stderr: string; status: number } {
|
|
225
|
+
const result = spawnSync("bd", args, {
|
|
226
|
+
cwd,
|
|
227
|
+
encoding: "utf8",
|
|
228
|
+
timeout: 30000,
|
|
229
|
+
});
|
|
230
|
+
return {
|
|
231
|
+
stdout: result.stdout || "",
|
|
232
|
+
stderr: result.stderr || "",
|
|
233
|
+
status: result.status ?? 1,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Create an epic in bd.
|
|
239
|
+
*/
|
|
240
|
+
export function bdCreateEpic(title: string, cwd: string): { id: string; title: string } | null {
|
|
241
|
+
const result = runBd(["create", title, "-t", "epic", "-p", "1", "--json"], cwd);
|
|
242
|
+
if (result.status === 0) {
|
|
243
|
+
try {
|
|
244
|
+
const data = JSON.parse(result.stdout);
|
|
245
|
+
if (Array.isArray(data) && data[0]) {
|
|
246
|
+
return { id: data[0].id, title: data[0].title };
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// Parse the ID from stdout if JSON parse fails
|
|
250
|
+
const match = result.stdout.match(/Created issue:\s*(\S+)/);
|
|
251
|
+
if (match) {
|
|
252
|
+
return { id: match[1], title };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create a task issue in bd under an epic.
|
|
261
|
+
*/
|
|
262
|
+
export function bdCreateIssue(
|
|
263
|
+
title: string,
|
|
264
|
+
description: string,
|
|
265
|
+
parentId: string,
|
|
266
|
+
cwd: string,
|
|
267
|
+
): { id: string; title: string } | null {
|
|
268
|
+
const result = runBd(
|
|
269
|
+
["create", title, "-t", "task", "-p", "1", "--parent", parentId, "-d", description, "--json"],
|
|
270
|
+
cwd,
|
|
271
|
+
);
|
|
272
|
+
if (result.status === 0) {
|
|
273
|
+
try {
|
|
274
|
+
const data = JSON.parse(result.stdout);
|
|
275
|
+
if (Array.isArray(data) && data[0]) {
|
|
276
|
+
return { id: data[0].id, title: data[0].title };
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
const match = result.stdout.match(/Created issue:\s*(\S+)/);
|
|
280
|
+
if (match) {
|
|
281
|
+
return { id: match[1], title };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Claim an issue in bd.
|
|
290
|
+
*/
|
|
291
|
+
export function bdClaim(issueId: string, cwd: string): boolean {
|
|
292
|
+
const result = runBd(["update", issueId, "--claim"], cwd);
|
|
293
|
+
return result.status === 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Result of creating plan issues.
|
|
298
|
+
*/
|
|
299
|
+
export interface PlanIssuesResult {
|
|
300
|
+
epic: { id: string; title: string };
|
|
301
|
+
issues: Array<{ id: string; title: string }>;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Create an epic and issues from todo items.
|
|
306
|
+
*/
|
|
307
|
+
export function createPlanIssues(
|
|
308
|
+
epicTitle: string,
|
|
309
|
+
todos: TodoItem[],
|
|
310
|
+
cwd: string,
|
|
311
|
+
): PlanIssuesResult | null {
|
|
312
|
+
const epic = bdCreateEpic(epicTitle, cwd);
|
|
313
|
+
if (!epic) return null;
|
|
314
|
+
|
|
315
|
+
const issues: Array<{ id: string; title: string }> = [];
|
|
316
|
+
for (const todo of todos) {
|
|
317
|
+
const issue = bdCreateIssue(todo.text, `Step ${todo.step} of plan: ${epicTitle}`, epic.id, cwd);
|
|
318
|
+
if (issue) {
|
|
319
|
+
issues.push(issue);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return { epic, issues };
|
|
324
|
+
}
|
|
@@ -84,7 +84,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
84
84
|
|
|
85
85
|
const ensured = await ensureWorktreeSessionState(cwd, issueId);
|
|
86
86
|
if (ensured.ok) {
|
|
87
|
-
const
|
|
87
|
+
const state = readSessionState(cwd);
|
|
88
|
+
const worktreePath = state?.worktreePath;
|
|
89
|
+
const nextStep = worktreePath
|
|
90
|
+
? `\nNext: cd ${worktreePath} && pi (sandboxed session)`
|
|
91
|
+
: "";
|
|
92
|
+
const text = `\n\n🧭 Session Flow: ${ensured.message}${nextStep}`;
|
|
88
93
|
return { content: [...event.content, { type: "text", text }] };
|
|
89
94
|
}
|
|
90
95
|
return undefined;
|
|
@@ -99,18 +104,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
99
104
|
if (state.phase === "waiting-merge" || state.phase === "pending-cleanup") {
|
|
100
105
|
const pr = state.prNumber != null ? `#${state.prNumber}` : "(pending PR)";
|
|
101
106
|
const url = state.prUrl ? ` ${state.prUrl}` : "";
|
|
102
|
-
pi.sendUserMessage(
|
|
107
|
+
pi.sendUserMessage(
|
|
108
|
+
`⚠ PR ${pr}${url} is still pending. xtrm finish is deprecated for Pi workflow. ` +
|
|
109
|
+
"Use xtpi publish (when available) and external merge/cleanup steps.",
|
|
110
|
+
);
|
|
103
111
|
return undefined;
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
if (state.phase === "conflicting") {
|
|
107
115
|
const files = state.conflictFiles?.length ? state.conflictFiles.join(", ") : "unknown files";
|
|
108
|
-
pi.sendUserMessage(
|
|
116
|
+
pi.sendUserMessage(
|
|
117
|
+
`⚠ Conflicts in: ${files}. xtrm finish is deprecated for Pi workflow. ` +
|
|
118
|
+
"Resolve conflicts, then continue with publish-only flow.",
|
|
119
|
+
);
|
|
109
120
|
return undefined;
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
if (state.phase === "claimed" || state.phase === "phase1-done") {
|
|
113
|
-
pi.sendUserMessage(
|
|
124
|
+
pi.sendUserMessage(
|
|
125
|
+
`⚠ Session has an active worktree at ${state.worktreePath}. ` +
|
|
126
|
+
"Use publish-only workflow (no automatic push/PR/merge).",
|
|
127
|
+
);
|
|
114
128
|
}
|
|
115
129
|
return undefined;
|
|
116
130
|
});
|