@tmustier/pi-agent-teams 0.4.0 → 0.5.1
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/CHANGELOG.md +37 -0
- package/README.md +55 -9
- package/WORKFLOW.md +110 -0
- package/docs/claude-parity.md +16 -13
- package/docs/hook-contract.md +183 -0
- package/docs/smoke-test-plan.md +26 -7
- package/extensions/teams/activity-tracker.ts +296 -8
- package/extensions/teams/cleanup.ts +222 -3
- package/extensions/teams/hooks.ts +57 -5
- package/extensions/teams/leader-attach-commands.ts +8 -4
- package/extensions/teams/leader-inbox.ts +162 -4
- package/extensions/teams/leader-info-commands.ts +105 -3
- package/extensions/teams/leader-lifecycle-commands.ts +205 -3
- package/extensions/teams/leader-messaging-commands.ts +19 -7
- package/extensions/teams/leader-spawn-command.ts +5 -1
- package/extensions/teams/leader-team-command.ts +51 -2
- package/extensions/teams/leader-teams-tool.ts +264 -10
- package/extensions/teams/leader.ts +174 -9
- package/extensions/teams/mailbox.ts +6 -1
- package/extensions/teams/spawn-types.ts +4 -0
- package/extensions/teams/teammate-rpc.ts +14 -0
- package/extensions/teams/teams-panel.ts +117 -19
- package/extensions/teams/teams-ui-shared.ts +205 -2
- package/extensions/teams/teams-widget.ts +67 -14
- package/extensions/teams/worker.ts +18 -6
- package/extensions/teams/worktree.ts +143 -0
- package/package.json +3 -2
- package/scripts/integration-cleanup-test.mts +419 -0
- package/scripts/smoke-test.mts +655 -2
- package/skills/agent-teams/SKILL.md +24 -7
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
2
2
|
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
3
3
|
import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
|
|
4
|
-
import type { TeammateRpc
|
|
4
|
+
import type { TeammateRpc } from "./teammate-rpc.js";
|
|
5
5
|
import type { ActivityTracker } from "./activity-tracker.js";
|
|
6
6
|
import type { TeamTask } from "./task-store.js";
|
|
7
7
|
import type { TeamConfig, TeamMember } from "./team-config.js";
|
|
8
8
|
import type { TeamsStyle } from "./teams-style.js";
|
|
9
9
|
import { formatMemberDisplayName, getTeamsStrings } from "./teams-style.js";
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
DISPLAY_STATUS_COLOR,
|
|
12
|
+
DISPLAY_STATUS_ICON,
|
|
13
|
+
formatElapsed,
|
|
13
14
|
formatTokens,
|
|
15
|
+
getMemberModel,
|
|
16
|
+
getMemberThinking,
|
|
14
17
|
getVisibleWorkerNames,
|
|
18
|
+
isTeamDone,
|
|
15
19
|
padRight,
|
|
16
|
-
|
|
20
|
+
renderPolicySummary,
|
|
21
|
+
resolveDisplayStatus,
|
|
22
|
+
shortModelLabel,
|
|
17
23
|
toolActivity,
|
|
18
24
|
} from "./teams-ui-shared.js";
|
|
25
|
+
import type { DisplayStatus, LeaderModelInfo } from "./teams-ui-shared.js";
|
|
19
26
|
|
|
20
27
|
export interface WidgetDeps {
|
|
21
28
|
getTeammates(): Map<string, TeammateRpc>;
|
|
@@ -26,6 +33,7 @@ export interface WidgetDeps {
|
|
|
26
33
|
isDelegateMode(): boolean;
|
|
27
34
|
getActiveTeamId(): string | null;
|
|
28
35
|
getSessionTeamId(): string | null;
|
|
36
|
+
getLeaderModel(): LeaderModelInfo | null;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
export type WidgetFactory = (tui: TUI, theme: Theme) => Component;
|
|
@@ -34,11 +42,19 @@ interface WidgetRow {
|
|
|
34
42
|
icon: string; // raw char (before styling)
|
|
35
43
|
iconColor: ThemeColor;
|
|
36
44
|
displayName: string;
|
|
37
|
-
statusKey:
|
|
45
|
+
statusKey: DisplayStatus;
|
|
38
46
|
pending: number;
|
|
39
47
|
completed: number;
|
|
40
48
|
tokensStr: string; // "—" for chairman
|
|
41
49
|
activityText: string;
|
|
50
|
+
/** Compact time-in-current-state string, e.g. "3m12s". Empty for leader. */
|
|
51
|
+
elapsedStr: string;
|
|
52
|
+
/** Short model label (e.g. "claude-sonnet-4-5") or null. */
|
|
53
|
+
modelLabel: string | null;
|
|
54
|
+
/** Thinking level (e.g. "high") or null. */
|
|
55
|
+
thinkingLabel: string | null;
|
|
56
|
+
/** Active task subject (if any). */
|
|
57
|
+
activeTaskSubject: string | null;
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
function shortTeamId(teamId: string): string {
|
|
@@ -96,6 +112,15 @@ export function createTeamsWidget(deps: WidgetDeps): WidgetFactory {
|
|
|
96
112
|
);
|
|
97
113
|
}
|
|
98
114
|
|
|
115
|
+
// ── Policy summary ──
|
|
116
|
+
const policyLines = renderPolicySummary({
|
|
117
|
+
teamConfig,
|
|
118
|
+
leaderModel: deps.getLeaderModel(),
|
|
119
|
+
theme,
|
|
120
|
+
width,
|
|
121
|
+
});
|
|
122
|
+
for (const pl of policyLines) lines.push(pl);
|
|
123
|
+
|
|
99
124
|
// ── Build row data ──
|
|
100
125
|
const cfgMembers = teamConfig?.members ?? [];
|
|
101
126
|
const cfgByName = new Map<string, TeamMember>();
|
|
@@ -116,6 +141,10 @@ export function createTeamsWidget(deps: WidgetDeps): WidgetFactory {
|
|
|
116
141
|
completed: leadTasks.filter((t) => t.status === "completed").length,
|
|
117
142
|
tokensStr: "\u2014",
|
|
118
143
|
activityText: "",
|
|
144
|
+
elapsedStr: "",
|
|
145
|
+
modelLabel: null,
|
|
146
|
+
thinkingLabel: null,
|
|
147
|
+
activeTaskSubject: null,
|
|
119
148
|
});
|
|
120
149
|
}
|
|
121
150
|
|
|
@@ -131,19 +160,27 @@ export function createTeamsWidget(deps: WidgetDeps): WidgetFactory {
|
|
|
131
160
|
for (const name of workerNames) {
|
|
132
161
|
const rpc = teammates.get(name);
|
|
133
162
|
const cfg = cfgByName.get(name);
|
|
134
|
-
const statusKey =
|
|
163
|
+
const statusKey = resolveDisplayStatus(rpc, cfg);
|
|
135
164
|
const activity = tracker.get(name);
|
|
136
165
|
const owned = tasks.filter((t) => t.owner === name);
|
|
166
|
+
const activeTask = owned.find((t) => t.status === "in_progress");
|
|
167
|
+
const memberModel = getMemberModel(cfg);
|
|
168
|
+
const memberThinking = getMemberThinking(cfg);
|
|
169
|
+
const elapsed = rpc ? formatElapsed(Date.now() - rpc.lastStatusChangeAt) : "";
|
|
137
170
|
|
|
138
171
|
rows.push({
|
|
139
|
-
icon:
|
|
140
|
-
iconColor:
|
|
172
|
+
icon: DISPLAY_STATUS_ICON[statusKey],
|
|
173
|
+
iconColor: DISPLAY_STATUS_COLOR[statusKey],
|
|
141
174
|
displayName: formatMemberDisplayName(style, name),
|
|
142
175
|
statusKey,
|
|
143
176
|
pending: owned.filter((t) => t.status === "pending").length,
|
|
144
177
|
completed: owned.filter((t) => t.status === "completed").length,
|
|
145
178
|
tokensStr: formatTokens(activity.totalTokens),
|
|
146
179
|
activityText: toolActivity(activity.currentToolName),
|
|
180
|
+
elapsedStr: elapsed,
|
|
181
|
+
modelLabel: memberModel ? shortModelLabel(memberModel) : null,
|
|
182
|
+
thinkingLabel: memberThinking,
|
|
183
|
+
activeTaskSubject: activeTask ? `#${String(activeTask.id)} ${activeTask.subject}` : null,
|
|
147
184
|
});
|
|
148
185
|
}
|
|
149
186
|
|
|
@@ -163,7 +200,7 @@ export function createTeamsWidget(deps: WidgetDeps): WidgetFactory {
|
|
|
163
200
|
for (const r of rows) {
|
|
164
201
|
const icon = theme.fg(r.iconColor, r.icon);
|
|
165
202
|
const styledName = theme.bold(r.displayName);
|
|
166
|
-
const statusLabel = theme.fg(
|
|
203
|
+
const statusLabel = theme.fg(DISPLAY_STATUS_COLOR[r.statusKey], padRight(r.statusKey, 9));
|
|
167
204
|
const pNum = String(r.pending).padStart(pW);
|
|
168
205
|
const cNum = String(r.completed).padStart(cW);
|
|
169
206
|
const tokStr = r.tokensStr.padStart(tokW);
|
|
@@ -171,10 +208,21 @@ export function createTeamsWidget(deps: WidgetDeps): WidgetFactory {
|
|
|
171
208
|
"dim",
|
|
172
209
|
` \u00b7 ${pNum} pending \u00b7 ${cNum} complete \u00b7 ${tokStr} tokens`,
|
|
173
210
|
);
|
|
211
|
+
const elapsedLabel = r.elapsedStr ? " " + theme.fg("dim", r.elapsedStr) : "";
|
|
174
212
|
const actLabel = r.activityText ? " " + theme.fg("warning", r.activityText) : "";
|
|
213
|
+
// Model + thinking badge (compact)
|
|
214
|
+
const badges: string[] = [];
|
|
215
|
+
if (r.modelLabel) badges.push(r.modelLabel);
|
|
216
|
+
if (r.thinkingLabel && r.thinkingLabel !== "off") badges.push(`t:${r.thinkingLabel}`);
|
|
217
|
+
const badgeStr = badges.length > 0 ? " " + theme.fg("muted", badges.join(" \u00b7 ")) : "";
|
|
175
218
|
|
|
176
|
-
const row = ` ${icon} ${padRight(styledName, nameColWidth)} ${statusLabel}${cols}${actLabel}`;
|
|
219
|
+
const row = ` ${icon} ${padRight(styledName, nameColWidth)} ${statusLabel}${elapsedLabel}${cols}${actLabel}${badgeStr}`;
|
|
177
220
|
lines.push(truncateToWidth(row, width));
|
|
221
|
+
// Active task on second line (indented, only when actively working)
|
|
222
|
+
if (r.activeTaskSubject) {
|
|
223
|
+
const taskLine = ` ${theme.fg("dim", "\u2514")} ${theme.fg("warning", r.activeTaskSubject)}`;
|
|
224
|
+
lines.push(truncateToWidth(taskLine, width));
|
|
225
|
+
}
|
|
178
226
|
}
|
|
179
227
|
|
|
180
228
|
// ── Total row ──
|
|
@@ -198,10 +246,15 @@ export function createTeamsWidget(deps: WidgetDeps): WidgetFactory {
|
|
|
198
246
|
}
|
|
199
247
|
|
|
200
248
|
// ── Hints line ──
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
"
|
|
204
|
-
|
|
249
|
+
const teamDone = isTeamDone(tasks, teammates);
|
|
250
|
+
const hints = teamDone
|
|
251
|
+
? theme.fg("success", " All tasks done.") +
|
|
252
|
+
" " +
|
|
253
|
+
theme.fg("dim", "/team done \u00b7 /team task list")
|
|
254
|
+
: theme.fg(
|
|
255
|
+
"dim",
|
|
256
|
+
" /team widget \u00b7 /team dm <name> <msg> \u00b7 /team task list",
|
|
257
|
+
);
|
|
205
258
|
lines.push(truncateToWidth(hints, width));
|
|
206
259
|
|
|
207
260
|
return lines;
|
|
@@ -130,16 +130,19 @@ export function runWorker(pi: ExtensionAPI): void {
|
|
|
130
130
|
const TeamMessageToolParamsSchema = Type.Object({
|
|
131
131
|
recipient: Type.String({ description: "Name of the comrade to message" }),
|
|
132
132
|
message: Type.String({ description: "The message to send" }),
|
|
133
|
+
urgent: Type.Optional(Type.Boolean({
|
|
134
|
+
description: "When true, the message interrupts the recipient's active turn via steering instead of waiting for idle. Use for time-sensitive coordination.",
|
|
135
|
+
})),
|
|
133
136
|
});
|
|
134
137
|
// Match the schema at compile-time.
|
|
135
138
|
type TeamMessageToolParams = Static<typeof TeamMessageToolParamsSchema>;
|
|
136
139
|
// Tool result details to match AgentToolResult<TDetails> contract.
|
|
137
|
-
type TeamMessageToolDetails = { recipient: string; timestamp: string };
|
|
140
|
+
type TeamMessageToolDetails = { recipient: string; timestamp: string; urgent: boolean };
|
|
138
141
|
|
|
139
142
|
pi.registerTool({
|
|
140
143
|
name: "team_message",
|
|
141
144
|
label: "Team Message",
|
|
142
|
-
description: "Send a message to a comrade. Use this to coordinate with peers on related tasks.",
|
|
145
|
+
description: "Send a message to a comrade. Use this to coordinate with peers on related tasks. Set urgent=true to interrupt their active turn (use sparingly — only for time-sensitive coordination).",
|
|
143
146
|
parameters: TeamMessageToolParamsSchema,
|
|
144
147
|
async execute(
|
|
145
148
|
_toolCallId,
|
|
@@ -150,12 +153,14 @@ export function runWorker(pi: ExtensionAPI): void {
|
|
|
150
153
|
): Promise<AgentToolResult<TeamMessageToolDetails>> {
|
|
151
154
|
const recipient = sanitizeName(params.recipient);
|
|
152
155
|
const message = params.message;
|
|
156
|
+
const isUrgent = params.urgent === true;
|
|
153
157
|
const ts = new Date().toISOString();
|
|
154
158
|
// Write to recipient's mailbox in team namespace
|
|
155
159
|
await writeToMailbox(teamDir, TEAM_MAILBOX_NS, recipient, {
|
|
156
160
|
from: agentName,
|
|
157
161
|
text: message,
|
|
158
162
|
timestamp: ts,
|
|
163
|
+
...(isUrgent ? { urgent: true } : {}),
|
|
159
164
|
});
|
|
160
165
|
// CC leader with peer_dm_sent notification
|
|
161
166
|
await writeToMailbox(teamDir, TEAM_MAILBOX_NS, leadName, {
|
|
@@ -165,13 +170,14 @@ export function runWorker(pi: ExtensionAPI): void {
|
|
|
165
170
|
from: agentName,
|
|
166
171
|
to: recipient,
|
|
167
172
|
summary: message.slice(0, 100),
|
|
173
|
+
urgent: isUrgent,
|
|
168
174
|
timestamp: ts,
|
|
169
175
|
}),
|
|
170
176
|
timestamp: ts,
|
|
171
177
|
});
|
|
172
178
|
return {
|
|
173
|
-
content: [{ type: "text", text:
|
|
174
|
-
details: { recipient, timestamp: ts },
|
|
179
|
+
content: [{ type: "text", text: `${isUrgent ? "Urgent message" : "Message"} sent to ${recipient}` }],
|
|
180
|
+
details: { recipient, timestamp: ts, urgent: isUrgent },
|
|
175
181
|
};
|
|
176
182
|
},
|
|
177
183
|
});
|
|
@@ -330,8 +336,14 @@ export function runWorker(pi: ExtensionAPI): void {
|
|
|
330
336
|
pendingTaskAssignments.push(assign.taskId);
|
|
331
337
|
continue;
|
|
332
338
|
}
|
|
333
|
-
|
|
334
|
-
|
|
339
|
+
|
|
340
|
+
// Urgent DMs interrupt the active turn via steer; normal DMs queue for idle.
|
|
341
|
+
if (m.urgent && isStreaming) {
|
|
342
|
+
const prefix = `[urgent message from ${m.from}] `;
|
|
343
|
+
pi.sendUserMessage(prefix + m.text, { deliverAs: "steer" });
|
|
344
|
+
} else {
|
|
345
|
+
pendingDmTexts.push(m.text);
|
|
346
|
+
}
|
|
335
347
|
}
|
|
336
348
|
|
|
337
349
|
if (!shutdownInProgress) await maybeStartNextWork();
|
|
@@ -3,6 +3,12 @@ import * as path from "node:path";
|
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { sanitizeName } from "./names.js";
|
|
5
5
|
|
|
6
|
+
export type WorktreeCleanupResult = {
|
|
7
|
+
removedWorktrees: string[];
|
|
8
|
+
removedBranches: string[];
|
|
9
|
+
warnings: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
6
12
|
async function execGit(args: string[], opts: { cwd: string; timeoutMs?: number } ): Promise<{ stdout: string; stderr: string }> {
|
|
7
13
|
return await new Promise((resolve, reject) => {
|
|
8
14
|
execFile(
|
|
@@ -101,3 +107,140 @@ export async function ensureWorktreeCwd(opts: {
|
|
|
101
107
|
return { cwd: opts.leaderCwd, warnings, mode: "shared" };
|
|
102
108
|
}
|
|
103
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Find the git repo root from a directory that may be inside a worktree.
|
|
113
|
+
* Returns null if not a git repo.
|
|
114
|
+
*/
|
|
115
|
+
async function findRepoRoot(cwd: string): Promise<string | null> {
|
|
116
|
+
try {
|
|
117
|
+
// --show-superproject-working-tree returns empty if not a worktree subproject.
|
|
118
|
+
// Use the commondir approach: worktrees share a common git dir with the main repo.
|
|
119
|
+
const commonDir = (await execGit(["rev-parse", "--git-common-dir"], { cwd })).stdout.trim();
|
|
120
|
+
if (commonDir && !path.isAbsolute(commonDir)) {
|
|
121
|
+
// Relative to the .git dir of the worktree — resolve upward.
|
|
122
|
+
const gitDir = (await execGit(["rev-parse", "--git-dir"], { cwd })).stdout.trim();
|
|
123
|
+
const absCommon = path.resolve(cwd, gitDir, commonDir);
|
|
124
|
+
// The common dir is the .git directory of the main repo. Its parent is the repo root.
|
|
125
|
+
return path.dirname(absCommon);
|
|
126
|
+
}
|
|
127
|
+
if (commonDir && path.isAbsolute(commonDir)) {
|
|
128
|
+
return path.dirname(commonDir);
|
|
129
|
+
}
|
|
130
|
+
// Fallback: plain repo
|
|
131
|
+
const toplevel = (await execGit(["rev-parse", "--show-toplevel"], { cwd })).stdout.trim();
|
|
132
|
+
return toplevel || null;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Remove all git worktrees under `<teamDir>/worktrees/` and their associated branches.
|
|
140
|
+
*
|
|
141
|
+
* Steps for each worktree:
|
|
142
|
+
* 1. `git worktree remove --force <path>` (removes the worktree checkout)
|
|
143
|
+
* 2. `git branch -D <branch>` (removes the local branch)
|
|
144
|
+
* 3. Falls back to filesystem removal if git commands fail
|
|
145
|
+
*
|
|
146
|
+
* Also runs `git worktree prune` to clean up stale worktree bookkeeping.
|
|
147
|
+
*/
|
|
148
|
+
export async function cleanupWorktrees(opts: {
|
|
149
|
+
teamDir: string;
|
|
150
|
+
teamId: string;
|
|
151
|
+
/** A directory known to be in the git repo (e.g. leaderCwd). */
|
|
152
|
+
repoCwd?: string;
|
|
153
|
+
}): Promise<WorktreeCleanupResult> {
|
|
154
|
+
const removedWorktrees: string[] = [];
|
|
155
|
+
const removedBranches: string[] = [];
|
|
156
|
+
const warnings: string[] = [];
|
|
157
|
+
|
|
158
|
+
const worktreesDir = path.join(opts.teamDir, "worktrees");
|
|
159
|
+
let entries: string[];
|
|
160
|
+
try {
|
|
161
|
+
entries = await fs.promises.readdir(worktreesDir);
|
|
162
|
+
} catch {
|
|
163
|
+
// No worktrees directory — nothing to do.
|
|
164
|
+
return { removedWorktrees, removedBranches, warnings };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (entries.length === 0) {
|
|
168
|
+
return { removedWorktrees, removedBranches, warnings };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Find the repo root. Prefer deriving from the worktree paths themselves (they belong
|
|
172
|
+
// to the repo that created them), only fall back to repoCwd when none resolve.
|
|
173
|
+
// This avoids cross-repo issues when cleanup targets a team created from a different repo.
|
|
174
|
+
let repoRoot: string | null = null;
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const candidate = path.join(worktreesDir, entry);
|
|
177
|
+
repoRoot = await findRepoRoot(candidate);
|
|
178
|
+
if (repoRoot) break;
|
|
179
|
+
}
|
|
180
|
+
if (!repoRoot && opts.repoCwd) {
|
|
181
|
+
repoRoot = await findRepoRoot(opts.repoCwd);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const shortTeam = sanitizeName(opts.teamId).slice(0, 12) || "team";
|
|
185
|
+
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
const worktreePath = path.join(worktreesDir, entry);
|
|
188
|
+
const safeAgent = sanitizeName(entry);
|
|
189
|
+
const branch = `pi-teams/${shortTeam}/${safeAgent}`;
|
|
190
|
+
|
|
191
|
+
// 1. Remove worktree via git
|
|
192
|
+
if (repoRoot) {
|
|
193
|
+
try {
|
|
194
|
+
await execGit(["worktree", "remove", "--force", worktreePath], { cwd: repoRoot, timeoutMs: 30_000 });
|
|
195
|
+
removedWorktrees.push(worktreePath);
|
|
196
|
+
} catch {
|
|
197
|
+
// Git removal failed — try filesystem fallback below.
|
|
198
|
+
try {
|
|
199
|
+
await fs.promises.rm(worktreePath, { recursive: true, force: true });
|
|
200
|
+
removedWorktrees.push(worktreePath);
|
|
201
|
+
} catch (fsErr: unknown) {
|
|
202
|
+
const msg = fsErr instanceof Error ? fsErr.message : String(fsErr);
|
|
203
|
+
warnings.push(`Failed to remove worktree ${worktreePath}: ${msg}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 2. Remove the associated branch
|
|
208
|
+
try {
|
|
209
|
+
await execGit(["branch", "-D", branch], { cwd: repoRoot });
|
|
210
|
+
removedBranches.push(branch);
|
|
211
|
+
} catch {
|
|
212
|
+
// Branch may not exist (shared workspace fallback) — that's fine.
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
// No repo root — just delete the directory.
|
|
216
|
+
try {
|
|
217
|
+
await fs.promises.rm(worktreePath, { recursive: true, force: true });
|
|
218
|
+
removedWorktrees.push(worktreePath);
|
|
219
|
+
} catch (fsErr: unknown) {
|
|
220
|
+
const msg = fsErr instanceof Error ? fsErr.message : String(fsErr);
|
|
221
|
+
warnings.push(`Failed to remove worktree directory ${worktreePath}: ${msg}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 3. Prune stale worktree bookkeeping entries
|
|
227
|
+
if (repoRoot) {
|
|
228
|
+
try {
|
|
229
|
+
await execGit(["worktree", "prune"], { cwd: repoRoot });
|
|
230
|
+
} catch {
|
|
231
|
+
warnings.push("git worktree prune failed (non-fatal)");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 4. Remove the now-empty worktrees directory itself
|
|
236
|
+
try {
|
|
237
|
+
const remaining = await fs.promises.readdir(worktreesDir);
|
|
238
|
+
if (remaining.length === 0) {
|
|
239
|
+
await fs.promises.rmdir(worktreesDir);
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// ignore — best effort
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { removedWorktrees, removedBranches, warnings };
|
|
246
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmustier/pi-agent-teams",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Claude Code agent teams style workflow for Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Thomas Mustier",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"integration-claim-test": "tsx scripts/integration-claim-test.mts",
|
|
35
35
|
"integration-spawn-overrides-test": "tsx scripts/integration-spawn-overrides-test.mts",
|
|
36
36
|
"integration-hooks-remediation-test": "tsx scripts/integration-hooks-remediation-test.mts",
|
|
37
|
-
"integration-todo-test": "tsx scripts/integration-todo-test.mts"
|
|
37
|
+
"integration-todo-test": "tsx scripts/integration-todo-test.mts",
|
|
38
|
+
"integration-cleanup-test": "tsx scripts/integration-cleanup-test.mts"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@eslint/js": "^9.39.2",
|