@tmustier/pi-agent-teams 0.5.3 → 0.5.5

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.
@@ -1,4 +1,4 @@
1
- import { SessionManager } from "@mariozechner/pi-coding-agent";
1
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
2
2
  import type { TeamAttachClaimHeartbeatResult } from "./team-attach-claim.js";
3
3
 
4
4
  interface SessionManagerWithHeader {
@@ -1,5 +1,5 @@
1
- import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
2
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
 
4
4
  export type ContextMode = "fresh" | "branch";
5
5
  export type WorkspaceMode = "shared" | "worktree";
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import type { AgentEvent } from "@mariozechner/pi-agent-core";
2
+ import type { AgentEvent } from "@earendil-works/pi-agent-core";
3
3
 
4
4
  export type TeammateStatus = "starting" | "idle" | "streaming" | "stopped" | "error";
5
5
 
@@ -1,5 +1,5 @@
1
- import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
2
- import type { Theme, ThemeColor, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
1
+ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
+ import type { Theme, ThemeColor, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
3
3
  import type { TeammateRpc } from "./teammate-rpc.js";
4
4
  import type { ActivityTracker, TranscriptLog, TranscriptEntry } from "./activity-tracker.js";
5
5
  import type { TeamTask } from "./task-store.js";
@@ -1,5 +1,5 @@
1
- import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
2
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
1
+ import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
  import type { TeamConfig, TeamMember } from "./team-config.js";
4
4
  import type { TeamTask } from "./task-store.js";
5
5
  import type { TeammateRpc, TeammateStatus } from "./teammate-rpc.js";
@@ -1,6 +1,6 @@
1
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
2
- import type { Component, TUI } from "@mariozechner/pi-tui";
3
- import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
1
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
+ import type { Component, TUI } from "@earendil-works/pi-tui";
3
+ import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
4
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";
@@ -1,5 +1,5 @@
1
- import type { AgentMessage, AgentToolResult } from "@mariozechner/pi-agent-core";
2
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { AgentMessage, AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { Type, type Static } from "@sinclair/typebox";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { popUnreadMessages, writeToMailbox } from "./mailbox.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmustier/pi-agent-teams",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Claude Code agent teams style workflow for Pi.",
5
5
  "license": "MIT",
6
6
  "author": "Thomas Mustier",
@@ -35,14 +35,15 @@
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
37
  "integration-todo-test": "tsx scripts/integration-todo-test.mts",
38
- "integration-cleanup-test": "tsx scripts/integration-cleanup-test.mts"
38
+ "integration-cleanup-test": "tsx scripts/integration-cleanup-test.mts",
39
+ "integration-branch-context-test": "tsx scripts/integration-branch-context-test.mts"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@eslint/js": "^9.39.2",
42
- "@mariozechner/pi-agent-core": "^0.62.0",
43
- "@mariozechner/pi-ai": "^0.62.0",
44
- "@mariozechner/pi-coding-agent": "^0.62.0",
45
- "@mariozechner/pi-tui": "^0.62.0",
43
+ "@earendil-works/pi-agent-core": "^0.74.0",
44
+ "@earendil-works/pi-ai": "^0.74.0",
45
+ "@earendil-works/pi-coding-agent": "^0.74.0",
46
+ "@earendil-works/pi-tui": "^0.74.0",
46
47
  "@sinclair/typebox": "^0.34.48",
47
48
  "eslint": "^9.39.2",
48
49
  "tsx": "^4.20.5",
@@ -50,10 +51,10 @@
50
51
  "typescript-eslint": "^8.54.0"
51
52
  },
52
53
  "peerDependencies": {
53
- "@mariozechner/pi-agent-core": "*",
54
- "@mariozechner/pi-ai": "*",
55
- "@mariozechner/pi-coding-agent": "*",
56
- "@mariozechner/pi-tui": "*",
54
+ "@earendil-works/pi-agent-core": "*",
55
+ "@earendil-works/pi-ai": "*",
56
+ "@earendil-works/pi-coding-agent": "*",
57
+ "@earendil-works/pi-tui": "*",
57
58
  "@sinclair/typebox": "*"
58
59
  }
59
60
  }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Integration test: branch-mode worker sessions should strip the leader's
3
+ * in-progress tool-use turn before starting delegated work.
4
+ *
5
+ * What this covers:
6
+ * - Prepare a persisted parent session whose leaf ends inside an unfinished
7
+ * assistant/tool-use turn (user -> assistant toolUse -> toolResult)
8
+ * - Derive a branch session using the same clean-turn selection logic used by
9
+ * teammate spawning
10
+ * - Start a real worker process from that branched session in worktree mode
11
+ * context conditions (git repo + persisted session)
12
+ * - Deliver an assigned task and verify the worker starts and completes it
13
+ *
14
+ * Usage:
15
+ * npx tsx scripts/integration-branch-context-test.mts
16
+ * npx tsx scripts/integration-branch-context-test.mts --timeoutSec 120
17
+ */
18
+
19
+ import * as fs from "node:fs";
20
+ import * as os from "node:os";
21
+ import * as path from "node:path";
22
+ import { spawn, spawnSync, type ChildProcess } from "node:child_process";
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
26
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
27
+ import { writeToMailbox } from "../extensions/teams/mailbox.js";
28
+ import { taskAssignmentPayload } from "../extensions/teams/protocol.js";
29
+ import { branchSelectionNote, resolveBranchLeafSelection } from "../extensions/teams/session-branching.js";
30
+ import { createTask, getTask } from "../extensions/teams/task-store.js";
31
+ import { ensureTeamConfig, loadTeamConfig } from "../extensions/teams/team-config.js";
32
+ import { sleep, terminateAll } from "./lib/pi-workers.js";
33
+
34
+ function parseArgs(argv: readonly string[]): { timeoutSec: number } {
35
+ let timeoutSec = 120;
36
+ for (let i = 0; i < argv.length; i += 1) {
37
+ if (argv[i] === "--timeoutSec") {
38
+ const v = argv[i + 1];
39
+ if (v) timeoutSec = Number.parseInt(v, 10);
40
+ i += 1;
41
+ }
42
+ }
43
+ if (!Number.isFinite(timeoutSec) || timeoutSec < 30) timeoutSec = 120;
44
+ return { timeoutSec };
45
+ }
46
+
47
+ function assert(condition: boolean, message: string): void {
48
+ if (!condition) throw new Error(message);
49
+ }
50
+
51
+ async function waitFor(
52
+ fn: () => boolean | Promise<boolean>,
53
+ opts: { timeoutMs: number; pollMs: number; label: string },
54
+ ): Promise<void> {
55
+ const { timeoutMs, pollMs, label } = opts;
56
+ const deadline = Date.now() + timeoutMs;
57
+ while (Date.now() < deadline) {
58
+ if (await fn()) return;
59
+ await sleep(pollMs);
60
+ }
61
+ throw new Error(`Timeout waiting for ${label}`);
62
+ }
63
+
64
+ function git(args: string[], cwd: string): void {
65
+ const res = spawnSync("git", args, {
66
+ cwd,
67
+ encoding: "utf8",
68
+ });
69
+ if (res.status !== 0) {
70
+ throw new Error(`git ${args.join(" ")} failed: ${res.stderr || res.stdout}`);
71
+ }
72
+ }
73
+
74
+ async function latestMemberStatus(teamDir: string, name: string): Promise<{ status?: string; sessionFile?: string } | null> {
75
+ const cfg = await loadTeamConfig(teamDir);
76
+ if (!cfg) return null;
77
+ const member = cfg.members.find((m) => m.name === name);
78
+ if (!member) return null;
79
+ return { status: member.status, sessionFile: member.sessionFile };
80
+ }
81
+
82
+ const { timeoutSec } = parseArgs(process.argv.slice(2));
83
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi-teams-branch-context-"));
84
+ const teamsRootDir = path.join(tmpRoot, "teams-root");
85
+ const repoDir = path.join(tmpRoot, "repo");
86
+ const teamId = "branch-context-team";
87
+ const taskListId = teamId;
88
+ const teamDir = path.join(teamsRootDir, teamId);
89
+ const sessionsDir = path.join(teamDir, "sessions");
90
+ const agentName = "alpha";
91
+ const leadName = "team-lead";
92
+ const procs: ChildProcess[] = [];
93
+
94
+ try {
95
+ fs.mkdirSync(repoDir, { recursive: true });
96
+ fs.mkdirSync(sessionsDir, { recursive: true });
97
+
98
+ git(["init", "-b", "main"], repoDir);
99
+ git(["config", "user.name", "Test User"], repoDir);
100
+ git(["config", "user.email", "test@example.com"], repoDir);
101
+ fs.writeFileSync(path.join(repoDir, "README.md"), "branch context integration\n", "utf8");
102
+ git(["add", "README.md"], repoDir);
103
+ git(["commit", "-m", "init"], repoDir);
104
+
105
+ const parent = SessionManager.create(repoDir, sessionsDir);
106
+ parent.appendModelChange("openai-codex", "gpt-5.4");
107
+ parent.appendThinkingLevelChange("minimal");
108
+ parent.appendMessage({
109
+ role: "user",
110
+ content: [{ type: "text", text: "Summarize the repo history." }],
111
+ timestamp: Date.now(),
112
+ });
113
+ const stableAssistantId = parent.appendMessage({
114
+ role: "assistant",
115
+ content: [{ type: "text", text: "The repo history is summarized." }],
116
+ api: "test",
117
+ provider: "test",
118
+ model: "test",
119
+ usage: {
120
+ input: 0,
121
+ output: 0,
122
+ cacheRead: 0,
123
+ cacheWrite: 0,
124
+ totalTokens: 0,
125
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
126
+ },
127
+ stopReason: "stop",
128
+ timestamp: Date.now(),
129
+ });
130
+ const compactionId = parent.appendCompaction("summarized", stableAssistantId, 1234);
131
+ const currentUserId = parent.appendMessage({
132
+ role: "user",
133
+ content: [{ type: "text", text: "Investigate the repo, then delegate part of it." }],
134
+ timestamp: Date.now(),
135
+ });
136
+ const toolUseAssistant: AssistantMessage = {
137
+ role: "assistant",
138
+ content: [{ type: "toolCall", id: "call-1", name: "read", arguments: { path: "README.md" } }],
139
+ api: "test",
140
+ provider: "test",
141
+ model: "test",
142
+ usage: {
143
+ input: 0,
144
+ output: 0,
145
+ cacheRead: 0,
146
+ cacheWrite: 0,
147
+ totalTokens: 0,
148
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
149
+ },
150
+ stopReason: "toolUse",
151
+ timestamp: Date.now(),
152
+ };
153
+ parent.appendMessage(toolUseAssistant);
154
+ parent.appendMessage({
155
+ role: "toolResult",
156
+ toolCallId: "call-1",
157
+ toolName: "read",
158
+ content: [{ type: "text", text: "branch context integration" }],
159
+ isError: false,
160
+ timestamp: Date.now(),
161
+ });
162
+
163
+ const parentLeafId = parent.getLeafId();
164
+ assert(parentLeafId !== null, "expected parent leaf id");
165
+ if (!parentLeafId) {
166
+ throw new Error("Missing parent leaf id");
167
+ }
168
+
169
+ const selection = resolveBranchLeafSelection(parent.getBranch(parentLeafId), parentLeafId);
170
+ assert(selection.adjusted, "expected unfinished turn branch selection to adjust away from active leaf");
171
+ assert(selection.leafId === compactionId, `expected branch selection to use the stable pre-user boundary id, got ${selection.leafId}`);
172
+ assert(branchSelectionNote(selection) === "branch(clean-turn)", "expected clean-turn branch note");
173
+ assert(selection.replayUserMessage?.role === "user", "expected the active user request to be replayed into the cleaned child branch");
174
+
175
+ const branchedSessionFile = parent.createBranchedSession(selection.leafId);
176
+ assert(Boolean(branchedSessionFile), "expected branched session file to be created");
177
+ if (!branchedSessionFile) {
178
+ throw new Error("Missing branched session file");
179
+ }
180
+ if (selection.replayUserMessage) {
181
+ parent.appendMessage(JSON.parse(JSON.stringify(selection.replayUserMessage)) as Parameters<typeof parent.appendMessage>[0]);
182
+ }
183
+
184
+ const childEntries = parent.getEntries();
185
+ assert(childEntries.some((entry) => entry.id === stableAssistantId), "child session should retain the latest completed assistant message");
186
+ assert(childEntries.some((entry) => entry.id === compactionId), "child session should retain the compaction entry before the active user");
187
+ assert(!childEntries.some((entry) => entry.id === currentUserId), "child session should drop the original unfinished-turn user entry");
188
+ assert(
189
+ childEntries.some(
190
+ (entry) =>
191
+ entry.type === "message" &&
192
+ typeof entry.message === "object" &&
193
+ entry.message !== null &&
194
+ (entry.message as { role?: string }).role === "user" &&
195
+ JSON.stringify((entry.message as { content?: unknown }).content).includes("Investigate the repo, then delegate part of it."),
196
+ ),
197
+ "child session should replay the active user request onto the cleaned branch",
198
+ );
199
+ assert(
200
+ !childEntries.some(
201
+ (entry) =>
202
+ entry.type === "message" &&
203
+ typeof entry.message === "object" &&
204
+ entry.message !== null &&
205
+ (entry.message as { role?: string }).role === "assistant" &&
206
+ entry.id !== stableAssistantId,
207
+ ),
208
+ "child session should exclude the in-progress assistant tool-use message",
209
+ );
210
+ assert(
211
+ !childEntries.some(
212
+ (entry) => entry.type === "message" && typeof entry.message === "object" && entry.message !== null && (entry.message as { role?: string }).role === "toolResult",
213
+ ),
214
+ "child session should exclude trailing tool results from the unfinished turn",
215
+ );
216
+
217
+ await ensureTeamConfig(teamDir, { teamId, taskListId, leadName, style: "normal" });
218
+
219
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
220
+ const repoRoot = path.resolve(scriptDir, "..");
221
+ const entryPath = path.join(repoRoot, "extensions", "teams", "index.ts");
222
+ assert(fs.existsSync(entryPath), `Missing teams entry path: ${entryPath}`);
223
+
224
+ const worker = spawn(
225
+ "pi",
226
+ [
227
+ "--mode",
228
+ "rpc",
229
+ "--session",
230
+ branchedSessionFile,
231
+ "--session-dir",
232
+ sessionsDir,
233
+ "--provider",
234
+ "openai-codex",
235
+ "--model",
236
+ "gpt-5.4",
237
+ "--thinking",
238
+ "minimal",
239
+ "--no-extensions",
240
+ "-e",
241
+ entryPath,
242
+ "--append-system-prompt",
243
+ `You are teammate '${agentName}'. Prefer working from the shared task list.`,
244
+ ],
245
+ {
246
+ cwd: repoDir,
247
+ env: {
248
+ ...process.env,
249
+ PI_TEAMS_ROOT_DIR: teamsRootDir,
250
+ PI_TEAMS_WORKER: "1",
251
+ PI_TEAMS_TEAM_ID: teamId,
252
+ PI_TEAMS_TASK_LIST_ID: taskListId,
253
+ PI_TEAMS_AGENT_NAME: agentName,
254
+ PI_TEAMS_LEAD_NAME: leadName,
255
+ PI_TEAMS_STYLE: "normal",
256
+ PI_TEAMS_AUTO_CLAIM: "0",
257
+ },
258
+ stdio: ["pipe", "pipe", "pipe"],
259
+ },
260
+ );
261
+ procs.push(worker);
262
+
263
+ await waitFor(
264
+ async () => {
265
+ const member = await latestMemberStatus(teamDir, agentName);
266
+ return member?.status === "online";
267
+ },
268
+ { timeoutMs: timeoutSec * 1000, pollMs: 250, label: `${agentName} online` },
269
+ );
270
+
271
+ const task = await createTask(teamDir, taskListId, {
272
+ subject: "Branch context integration",
273
+ description: "Reply with exactly 'branch context integration ok'. Do not edit files.",
274
+ owner: agentName,
275
+ });
276
+ await writeToMailbox(teamDir, taskListId, agentName, {
277
+ from: leadName,
278
+ text: JSON.stringify(taskAssignmentPayload(task, leadName)),
279
+ timestamp: new Date().toISOString(),
280
+ });
281
+
282
+ await waitFor(
283
+ async () => {
284
+ const current = await getTask(teamDir, taskListId, task.id);
285
+ const result = current?.metadata?.result;
286
+ return current?.status === "completed" && typeof result === "string" && result.includes("branch context integration ok");
287
+ },
288
+ { timeoutMs: timeoutSec * 1000, pollMs: 500, label: `task #${task.id} completion` },
289
+ );
290
+
291
+ console.log("PASS: branch context integration test passed");
292
+ } finally {
293
+ await terminateAll(procs);
294
+ try {
295
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
296
+ } catch {
297
+ // ignore
298
+ }
299
+ }