@fradser/pi-agent-teams 0.9.2 → 1.0.0

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/src/types.ts CHANGED
@@ -1,148 +1,39 @@
1
- import type { Static, TSchema, TString, TObject, TUnion } from "typebox";
1
+ import type { TSchema, TString, TObject, TUnion } from "typebox";
2
2
  import { Type } from "typebox";
3
- import { WORKER_BUILTIN_TOOLS } from "./worker-tools.ts";
4
-
5
- // ── Teammate ──────────────────────────────────────────────────────
6
-
7
- export const TeammateStatus = Type.Union(
8
- [
9
- Type.Literal("starting"),
10
- Type.Literal("idle"),
11
- Type.Literal("working"),
12
- Type.Literal("stopped"),
13
- ],
14
- { description: "Lifecycle of a resident teammate" },
15
- );
16
- export type TeammateStatus = Static<typeof TeammateStatus>;
17
-
18
- export interface WorkerAssignment {
19
- id: string;
20
- kind: "direct" | "board";
21
- /** Resource tags prevent concurrent mutating assignments from overlapping. */
22
- resources: string[];
23
- /** A terminal direct assignment is closed until the leader explicitly reopens it. */
24
- closed?: boolean;
25
- }
26
-
27
- /** A named, long-lived child Pi process on the team roster. */
28
- export interface Teammate {
29
- /** Unique among living teammates; also the mailbox and roster key. */
30
- name: string;
31
- /** Resolved agent definition name. */
32
- agent: string;
33
- /** Per-spawn capability identity, regenerated for every process. */
34
- spawnId: string;
35
- /** Stable Work Item selected by agent; each reopen creates a new assignment attempt. */
36
- workId?: string;
37
- context?: "fresh" | "fork";
38
- pid: number;
39
- status: TeammateStatus;
40
- /** Working directory (the worktree root when isolated). */
41
- cwd?: string;
42
- /** Whether this teammate owns a dedicated Git worktree. */
43
- isolation: "worktree" | "none";
44
- /** Board task currently claimed by this teammate, if any. */
45
- currentTaskId?: string;
46
- /** The one assignment this worker is allowed to execute. Direct assignments
47
- * and board claims are mutually exclusive until the leader opens another. */
48
- assignment?: WorkerAssignment;
49
- /** Most recent assignment retained for successor handoff after release or shutdown. */
50
- lastAssignment?: WorkerAssignment;
51
- lastTaskId?: string;
52
- /** Effective launch model reference ("provider/model"); absent when Pi picks its default. */
53
- model?: string;
54
- /** Live assistant text assembled from the RPC stream. */
55
- liveText?: string;
56
- /** Current child tool name, if a tool is executing. */
57
- activeTool?: string;
58
- /** Live assistant reasoning streamed while no tool runs. */
59
- liveThinking?: string;
60
- /** Assistant turns observed in the current wake-up sequence. */
61
- turns?: number;
62
- /** The child finished its current sequence and awaits the next prompt. */
63
- sequenceEnded?: boolean;
64
- /** Leader reports are closed until a new prompt starts after a terminal report. */
65
- reportSequenceEnded?: boolean;
66
- /** When the harness last sent a claimable-task notice to this teammate. */
67
- lastNoticeAt?: number;
68
- /** Claimable task ids already announced to this teammate; one notice per id until it re-arms. */
69
- noticedTaskIds?: string[];
70
- usage?: WorkerUsage;
71
- error?: string;
72
- /** Effective tool allowlist granted to the child process (role tools plus
73
- * the capability set). Absent only for legacy snapshots. */
74
- tools?: string[];
75
- /** True once recognized model/stream activity was observed for this
76
- * incarnation; console telemetry only, never a leader notification. */
77
- modelOutputSeen?: boolean;
78
- createdAt: number;
79
- updatedAt: number;
80
- stoppedAt?: number;
81
- /** Last wall-clock time output was observed (any stream event or prompt delivery). */
82
- lastOutputAt?: number;
83
- }
84
-
85
- // ── Task board ────────────────────────────────────────────────────
86
-
87
- export const TaskStatus = Type.Union(
88
- [
89
- Type.Literal("pending"),
90
- Type.Literal("claimed"),
91
- Type.Literal("completed"),
92
- Type.Literal("superseded"),
93
- ],
94
- { description: "Board lifecycle of a task" },
95
- );
96
- export type TaskStatus = Static<typeof TaskStatus>;
97
-
98
- /** One task on the shared board. Only the leader process writes board state. */
99
- export interface BoardTask {
100
- id: string;
101
- subject: string;
102
- description?: string;
103
- /** Task ids that must complete before this task is claimable. */
104
- dependsOn: string[];
105
- /** Completion gate: a review prompt a fresh one-shot reviewer answers with
106
- * VERDICT: PASS or FAIL; overrides the agent-role default. */
107
- verify?: string;
108
- /** Mutating work must use stable resource tags such as `firmware/sub-node`.
109
- * A resource conflicts with itself and any slash-prefixed descendant. */
110
- resources: string[];
111
- status: TaskStatus;
112
- claimedBy?: string;
113
- /** Replacement task that made this task obsolete. */
114
- supersededBy?: string;
115
- result?: string;
116
- /** Deferred mail archived when an accepted result closes its Work Item. */
117
- deferredMessages?: Array<{ from: string; subject: string; body: string; timestamp: number }>;
118
- errorMessage?: string;
119
- createdAt: number;
120
- updatedAt: number;
121
- completedAt?: number;
122
- }
123
-
124
- /** A claim or submission intent expressed by a worker through marker files.
125
- * Workers never write the board file itself. `status` applies to submissions. */
126
- export interface TaskIntent {
127
- taskId: string;
128
- worker: string;
129
- spawnId: string;
130
- assignmentId?: string;
131
- status?: "completed" | "failed";
132
- result?: string;
133
- timestamp: number;
134
- }
135
-
136
- // ── Spawn ─────────────────────────────────────────────────────────
137
-
138
- /** Token/cost usage reported by a teammate (accumulated across sequences). */
139
- export interface WorkerUsage {
140
- input: number;
141
- output: number;
142
- cacheRead: number;
143
- cacheWrite: number;
144
- totalTokens: number;
145
- cost: number;
3
+ import { AGENT_BUILTIN_TOOLS } from "@fradser/pi-subagents";
4
+
5
+ // ── Roster ───────────────────────────────────────────────────────
6
+ // A child process on the roster is an execution fact, so the type lives in
7
+ // @fradser/pi-subagents with the code that manages it. Re-exported here because
8
+ // the coordination vocabulary below (TeamState) references it and callers read
9
+ // this module as the package's type surface.
10
+
11
+ export type { Agent, AgentStatus, AgentUsage } from "@fradser/pi-subagents";
12
+ import type { Agent } from "@fradser/pi-subagents";
13
+
14
+ // ── Task tasks ────────────────────────────────────────────────────
15
+ // The Work Item data model lives in @fradser/pi-tasks. Re-exported here because the
16
+ // coordination vocabulary below (Agent, TeamState) references it and callers
17
+ // read this module as the package's type surface.
18
+
19
+ export type { Task, TaskIntent, TaskStatus } from "@fradser/pi-tasks";
20
+ // The assignment a teammate holds is the task domain's own shape. Re-exported
21
+ // under the historical name so the coordination vocabulary stays readable.
22
+ export type { WorkAssignment as WorkerAssignment } from "@fradser/pi-tasks";
23
+ import type { Task } from "@fradser/pi-tasks";
24
+
25
+ /**
26
+ * The result shape every coordination tool returns.
27
+ *
28
+ * Declared because `additionalProperties: false` and a single `required` on a
29
+ * flat schema cannot express per-action requirements, so refusals are reported
30
+ * here rather than thrown. Every tool reports failure this way, and a caller
31
+ * reads one shape whatever went wrong.
32
+ */
33
+ export interface CoordinationToolResult {
34
+ content: Array<{ type: "text"; text: string }>;
35
+ details: Record<string, unknown>;
36
+ isError?: boolean;
146
37
  }
147
38
 
148
39
  // ── Mailbox ───────────────────────────────────────────────────────
@@ -216,13 +107,10 @@ export interface InboxMessage {
216
107
 
217
108
  export const InlineAgentDefinitionParams = Type.Object({
218
109
  description: Type.String({ minLength: 1, description: "Routing contract for the generated Agent" }),
219
- tools: Type.Optional(Type.Array(Type.String(), { description: `Explicit minimal tool grant. Canonical built-ins: ${WORKER_BUILTIN_TOOLS.join(", ")}. Omitted or [] means coordination-only (agent_event and work), with no file or shell access. No aliases or inherited leader extension tools.` })),
110
+ tools: Type.Optional(Type.Array(Type.String(), { description: `Explicit minimal tool grant. Canonical built-ins: ${AGENT_BUILTIN_TOOLS.join(", ")}. Omitted or [] means coordination-only (message and task), with no file or shell access. No aliases or inherited leader extension tools.` })),
220
111
  model: Type.Optional(Type.String({ description: "Provider/model pin or inherit" })),
221
112
  verify: Type.Optional(Type.String({ description: "Default Work verification gate" })),
222
- worktree: Type.Optional(Type.Boolean({ description: "Dedicated Git worktree" })),
223
113
  prompt: Type.String({ minLength: 1, description: "Agent role prompt" }),
224
- persist: Type.Optional(Type.Boolean({ description: "Persist only when explicitly requested" })),
225
- persistScope: Type.Optional(Type.Union([Type.Literal("project"), Type.Literal("project-local")])),
226
114
  }, { additionalProperties: false });
227
115
 
228
116
  /** Some harnesses deliver object/array parameters as JSON strings and Pi
@@ -260,6 +148,10 @@ function mergeBranchSubSchemas(pool: any[]): any {
260
148
  return { anyOf: pool };
261
149
  }
262
150
 
151
+ /** The root also declares `type: "object"`. A schema that carries `properties`
152
+ * without an explicit object type is rejected by Google's GenerateContent API
153
+ * ("parameters.properties: only allowed for OBJECT type"), which the Gemini
154
+ * routes reach through their provider translators. */
263
155
  function actionUnion<T extends TObject[]>(branches: [...T], options?: Record<string, unknown>): TUnion<T> {
264
156
  const pools: Record<string, any[]> = {};
265
157
  for (const branch of branches) {
@@ -273,7 +165,7 @@ function actionUnion<T extends TObject[]>(branches: [...T], options?: Record<str
273
165
  }
274
166
  const properties: Record<string, any> = {};
275
167
  for (const [key, pool] of Object.entries(pools)) properties[key] = mergeBranchSubSchemas(pool);
276
- return Type.Union(branches, { ...options, properties } as never) as TUnion<T>;
168
+ return Type.Union(branches, { type: "object", ...options, properties } as never) as TUnion<T>;
277
169
  }
278
170
 
279
171
  /** Tolerate harnesses that deliver object or array parameters as JSON strings. */
@@ -314,7 +206,7 @@ export const AgentActionParams = actionUnion([
314
206
  ], { description: "Delegate, start, inspect, or stop an Agent session" });
315
207
 
316
208
  /** Shut down one living teammate. */
317
- /** Create a board task (leader-only). */
209
+ /** Create a task set task (leader-only). */
318
210
  /** First public Work interface slice. Work creation reuses the current
319
211
  * single-writer task record until every acquisition path shares one lifecycle. */
320
212
  export const WorkToolParams = actionUnion([
@@ -353,12 +245,19 @@ export const WorkToolParams = actionUnion([
353
245
  }, { additionalProperties: false }),
354
246
  ], { description: "Create, list, assign, release, reopen, or supersede current Work Items" });
355
247
 
356
- /** Shared leader/worker read-only board view. */
357
- /** Shared communication event parameters across Leader, Worker, and Peers. */
248
+ /** Shared leader/worker read-only tasks view. */
249
+ /** The one communication primitive, across Leader, Worker, and Peers.
250
+ *
251
+ * `body` rather than `message`: the field is the text of a message, and a
252
+ * parameter named after the tool is a small way of making every caller write
253
+ * `message: { message: ... }`. `kind` rather than `intent`: the two values are
254
+ * what the recipient owes the sender — nothing, or a decision — and that is a
255
+ * kind of message, not an intent the tool acts on.
256
+ */
358
257
  export const AgentEventParams = Type.Object({
359
- message: Type.String({ description: "Message content" }),
258
+ body: Type.String({ description: "Message content" }),
360
259
  to: Type.Optional(Type.String({ minLength: 1, description: "Recipient Agent, leader, or exact session route" })),
361
- intent: Type.Optional(Type.Union([Type.Literal("inform"), Type.Literal("request")])),
260
+ kind: Type.Optional(Type.Union([Type.Literal("inform"), Type.Literal("request")])),
362
261
  }, { additionalProperties: false });
363
262
 
364
263
  /** The reserved recipient name for reports to the team leader. */
@@ -366,7 +265,7 @@ export const LEADER_RECIPIENT = "leader";
366
265
 
367
266
  /** The single messaging primitive: addressed peer mail and leader reports.
368
267
  * `status` is honored only for to="leader" terminal reports. */
369
- /** Self-claim a pending board task. */
268
+ /** Self-claim a pending tasks task. */
370
269
  /** Worker-only Work claim operation. It queues an intent; the single-writer
371
270
  * harness remains the only authority that can grant ownership. */
372
271
  export const WorkerWorkToolParams = actionUnion([
@@ -393,8 +292,8 @@ export const TEAM_RUNTIME_VERSION = 2;
393
292
 
394
293
  export interface TeamState {
395
294
  runtimeVersion: number;
396
- teammates: Record<string, Teammate>;
397
- tasks: Record<string, BoardTask>;
295
+ agents: Record<string, Agent>;
296
+ tasks: Record<string, Task>;
398
297
  /** Unified teammate model for this session; spawns without a role pin use it. */
399
298
  defaultModel?: string;
400
299
  /** Single leader inbox for teammate reports and harness diagnostics. */
package/src/ui.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { isTaskReplaced } from "@fradser/pi-tasks";
1
2
  /** Passive team widget and the /agent-teams management console
2
- * (session teammates + persistent agent roles + board). */
3
+ * (session agents + persistent agent roles + tasks). */
3
4
 
4
5
  import { truncateToWidth, Key, matchesKey, fuzzyFilter } from "@earendil-works/pi-tui";
5
6
  import {
@@ -21,20 +22,20 @@ import { type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
21
22
  import {
22
23
  clampConsoleScroll, consoleScrollRange, maxConsoleBody, scrollConsoleDetail, wrapConsoleDetail,
23
24
  } from "./console-viewport.ts";
24
- import { formatTeammateLabel, runningTeammateActivity } from "./activity.ts";
25
- import { MODEL_INHERIT_ALIAS, discoverAgents, resolveAgent, type AgentDefinition } from "./agents.ts";
26
- import { getPeerDeliveryState, getState, getTeammate, getTeamDefaultModel, listTasks, listTeammates, livingTeammates, setTeamDefaultModel } from "./state.ts";
25
+ import { formatAgentLabel, runningTeammateActivity } from "./activity.ts";
26
+ import { MODEL_INHERIT_ALIAS, discoverAgents, resolveAgent, type AgentDefinition } from "@fradser/pi-subagents";
27
+ import { getPeerDeliveryState, getState, getAgent, getTeamDefaultModel, listTasks, listAgents, livingAgents, setTeamDefaultModel } from "./state.ts";
27
28
  import {
28
29
  currentLeaderModelRef,
29
30
  ensureLivePoll,
30
31
  formatSilenceDuration,
31
32
  resolveSpawnModel,
32
33
  runtimeDirPath,
33
- shutdownTeammate,
34
+ shutdownAgent,
34
35
  stallSilenceMs,
35
36
  stallThresholdMs,
36
37
  } from "./team-machine.ts";
37
- import type { Teammate } from "./types.ts";
38
+ import type { Agent } from "./types.ts";
38
39
  import { mapPickerKey } from "./picker-keys.ts";
39
40
  import { inboxPath } from "./statefile.ts";
40
41
  import * as fs from "node:fs";
@@ -44,14 +45,14 @@ let spinnerTimer: ReturnType<typeof setInterval> | undefined;
44
45
  let spinnerFrame = 0;
45
46
 
46
47
  const teamActivityWidget = createLiveActivityWidget({
47
- key: "teammate",
48
+ key: "agent",
48
49
  placement: "aboveEditor",
49
50
  fit: truncateToWidth,
50
51
  activityFormat: "markdown",
51
52
  });
52
53
 
53
54
  function ensureSpinner(): void {
54
- const working = livingTeammates().some(isWorking);
55
+ const working = livingAgents().some(isWorking);
55
56
  if (working && !spinnerTimer) {
56
57
  spinnerTimer = setInterval(() => {
57
58
  spinnerFrame = (spinnerFrame + 1) % PI_SPINNER_FRAMES.length;
@@ -71,20 +72,20 @@ export function stopUiTimers(): void {
71
72
  }
72
73
 
73
74
  export function ensureTeamWidget(ctx?: { ui?: ExtensionUIContext; mode?: string }): void {
74
- const working = livingTeammates().filter(isWorking);
75
- teamActivityWidget.update(ctx, working.map((teammate) => ({
76
- id: teammate.name,
77
- identity: teammate.name,
78
- activity: runningTeammateActivity(teammate) + stallSuffix(teammate),
75
+ const working = livingAgents().filter(agent => !agent.standalone && isWorking(agent));
76
+ teamActivityWidget.update(ctx, working.map((agent) => ({
77
+ id: agent.name,
78
+ identity: agent.name,
79
+ activity: runningTeammateActivity(agent) + stallSuffix(agent),
79
80
  })));
80
81
  }
81
82
 
82
- function isWorking(teammate: { status: string }): boolean {
83
- return teammate.status === "working" || teammate.status === "starting";
83
+ function isWorking(agent: { status: string }): boolean {
84
+ return agent.status === "working" || agent.status === "starting";
84
85
  }
85
86
 
86
87
  /** Silence marker appended once a working teammate passes its stall threshold. */
87
- function stallSuffix(teammate: Teammate): string {
88
+ function stallSuffix(teammate: Agent): string {
88
89
  if (!isWorking(teammate)) return "";
89
90
  const silence = stallSilenceMs(teammate);
90
91
  const threshold = stallThresholdMs(teammate);
@@ -166,18 +167,43 @@ function displaySource(sourcePath: string | undefined): string {
166
167
  return relative.startsWith("..") ? sourcePath : `./${relative}`;
167
168
  }
168
169
 
170
+ /** Passive console telemetry for the spawn environment policy: names and counts
171
+ * only, never a withheld value. Per ADR-0002 observation lives here rather than
172
+ * in a leader notification, so a routine spawn never interrupts the leader.
173
+ * child-env.ts already bounds the name list, so this renders it as given and
174
+ * the console cannot drift from the diagnostic. */
175
+ function buildEnvPolicyDetail(policy: Agent["envPolicy"]): string[] {
176
+ if (!policy || policy.withheldCount === 0) return [];
177
+ const extra = policy.withheldSecretCount - policy.withheldSecretNames.length;
178
+ const names = policy.withheldSecretNames.length > 0
179
+ ? ` (credential-shaped: ${policy.withheldSecretNames.join(", ")}${extra > 0 ? `, +${extra} more` : ""})`
180
+ : "";
181
+ const lines = [
182
+ ` Env: ${policy.withheldCount} leader variable${policy.withheldCount === 1 ? "" : "s"} withheld${names}`,
183
+ ];
184
+ if (policy.missingAllowed.length > 0) {
185
+ lines.push(` Env allow: requested but absent from the leader: ${policy.missingAllowed.join(", ")}`);
186
+ }
187
+ if (policy.flaggedAllowOverrides.length > 0) {
188
+ lines.push(` Env allow: operator passes credential-shaped variables: ${policy.flaggedAllowOverrides.join(", ")}`);
189
+ }
190
+ return lines;
191
+ }
192
+
169
193
  function buildTeammateDetail(name: string): string[] {
170
- const teammate = getTeammate(name);
194
+ const teammate = getAgent(name);
171
195
  if (!teammate) return ["(teammate removed from the roster)"];
172
196
  const reports = getState().leaderMailbox.filter((message) => message.from === name);
173
197
  const lines: string[] = [
174
- `@${teammate.name} (${teammate.agent}) [${teammate.status}]`,
198
+ `@${teammate.name} (${teammate.role}) [${teammate.status}]`,
175
199
  "",
176
200
  "== teammate ==",
177
201
  ` Status: ${teammate.status}${teammate.currentTaskId ? ` | Task: ${teammate.currentTaskId}` : ""}`,
178
- ` Spawn: ${teammate.pid > 0 ? `pid ${teammate.pid}` : "pid unknown"} | Isolation: ${teammate.isolation}`,
202
+ ` Spawn: ${teammate.pid > 0 ? `pid ${teammate.pid}` : "pid unknown"}`,
203
+ ...(teammate.cwd ? [` Launch cwd: ${teammate.cwd}`] : []),
179
204
  ...(teammate.model ? [` Launch model: ${teammate.model}`] : []),
180
205
  ...(teammate.tools?.length ? [` Tools: ${teammate.tools.join(", ")}`] : []),
206
+ ...buildEnvPolicyDetail(teammate.envPolicy),
181
207
  ` Created: ${new Date(teammate.createdAt).toLocaleString()}`,
182
208
  ];
183
209
  if (teammate.stoppedAt) lines.push(` Stopped: ${new Date(teammate.stoppedAt).toLocaleString()}`);
@@ -213,7 +239,7 @@ function buildTeammateDetail(name: string): string[] {
213
239
  function buildTaskDetail(taskId: string): string[] {
214
240
  const tasks = listTasks();
215
241
  const task = tasks.find((candidate) => candidate.id === taskId);
216
- if (!task) return ["(task not on the board)"];
242
+ if (!task) return ["(task not found)"];
217
243
  const byId = new Map(tasks.map((candidate) => [candidate.id, candidate]));
218
244
  return [
219
245
  `[${task.id}] ${task.subject}`,
@@ -227,7 +253,6 @@ function buildTaskDetail(taskId: string): string[] {
227
253
  : []),
228
254
  ` Verify: ${task.verify ?? "(none)"}`,
229
255
  ...(task.result ? [" Result:", ...indent(cap(task.result))] : []),
230
- ...(task.errorMessage ? [" Error:", ...indent(cap(task.errorMessage))] : []),
231
256
  ` Created: ${new Date(task.createdAt).toLocaleString()}`,
232
257
  ...(task.completedAt ? [` Completed: ${new Date(task.completedAt).toLocaleString()}`] : []),
233
258
  ];
@@ -245,8 +270,8 @@ function buildRoleDetail(name: string): string[] {
245
270
  ` Tools: ${def.tools.join(", ") || "(role defaults)"}`,
246
271
  ` Model: ${describeSpawnModel(def.model)}`,
247
272
  ` Verify: ${def.verify ?? "(none)"}`,
248
- ` Worktree: ${def.worktree ? "true" : "false"}`,
249
- ` Living instances: ${livingTeammates().filter((teammate) => teammate.agent === def.name).length}`,
273
+ ` Data: ${def.scope === "session" ? "temporary; no-session, no Memory" : "durable; private Memory, scratch workspace and project history"}`,
274
+ ` Living instances: ${livingAgents().filter((teammate) => teammate.role === def.name).length}`,
250
275
  "",
251
276
  "== role prompt ==",
252
277
  ...indent(cap(def.prompt)),
@@ -266,21 +291,21 @@ function describeSpawnModel(pinned: string | undefined): string {
266
291
 
267
292
  // ── Full-screen console ───────────────────────────────────────────
268
293
 
269
- type ConsolePage = "roster" | "board";
294
+ type ConsolePage = "roster" | "tasks";
270
295
 
271
296
  interface RosterRow {
272
297
  key: string;
273
- kind: "teammate";
298
+ kind: "agent";
274
299
  }
275
300
  interface RoleRow {
276
301
  key: string;
277
302
  kind: "role";
278
303
  }
279
- interface BoardRow {
304
+ interface TaskRow {
280
305
  key: string;
281
306
  kind: "task";
282
307
  }
283
- type ConsoleRow = RosterRow | RoleRow | BoardRow;
308
+ type ConsoleRow = RosterRow | RoleRow | TaskRow;
284
309
 
285
310
  /** SGR mouse-wheel events carry a button code; return signed line delta. */
286
311
  function wheelDelta(data: string): number | undefined {
@@ -298,7 +323,7 @@ function wheelDelta(data: string): number | undefined {
298
323
  const LIST_CHROME_LINES = 5;
299
324
 
300
325
  /** Full-screen Team Console — owns input via ctx.ui.custom. Pages: roster /
301
- * board; each row opens a scrolling detail view; m opens the searchable
326
+ * tasks; each row opens a scrolling detail view; m opens the searchable
302
327
  * teammate-model picker. */
303
328
  export function openTeamConsole(ctx: {
304
329
  ui: ExtensionUIContext;
@@ -308,9 +333,9 @@ export function openTeamConsole(ctx: {
308
333
  let page: ConsolePage = "roster";
309
334
  let mode: "list" | "detail" | "picker" = "list";
310
335
  let selectedRoster = 0;
311
- let selectedBoard = 0;
336
+ let selectedTasks = 0;
312
337
  let listOffset = 0;
313
- let detailKind: ConsoleRow["kind"] = "teammate";
338
+ let detailKind: ConsoleRow["kind"] = "agent";
314
339
  let detailKey = "";
315
340
  let offset = 0;
316
341
  let confirmName: string | undefined;
@@ -338,7 +363,7 @@ export function openTeamConsole(ctx: {
338
363
  if (renderTimer) return;
339
364
  renderTimer = setInterval(() => {
340
365
  // Redraw only while something can animate (spinner/activity/stall).
341
- if (livingTeammates().some(isWorking)) requestRender();
366
+ if (livingAgents().some(isWorking)) requestRender();
342
367
  }, PI_SPINNER_INTERVAL_MS);
343
368
  renderTimer.unref?.();
344
369
  };
@@ -351,7 +376,7 @@ export function openTeamConsole(ctx: {
351
376
 
352
377
  const style = createPiThemeStyle(theme);
353
378
 
354
- // ── Teammate-model picker (type-to-filter) ──────────────────
379
+ // ── Agent-model picker (type-to-filter) ──────────────────
355
380
  let picker: SearchPicker<{ provider: string; id: string; name?: string }> | undefined;
356
381
  let pickerOffset = 0;
357
382
 
@@ -443,10 +468,10 @@ export function openTeamConsole(ctx: {
443
468
  if (!picked) return;
444
469
  if (isClearEntry(picked)) {
445
470
  setTeamDefaultModel(undefined);
446
- notifyPi(ctx.ui, "Teammate model cleared — Pi picks its default", "info");
471
+ notifyPi(ctx.ui, "Agent model cleared — Pi picks its default", "info");
447
472
  } else {
448
473
  setTeamDefaultModel(modelLabel(picked));
449
- notifyPi(ctx.ui, `Teammate model set to ${modelLabel(picked)} for this session`, "info");
474
+ notifyPi(ctx.ui, `Agent model set to ${modelLabel(picked)} for this session`, "info");
450
475
  }
451
476
  closeModelPicker();
452
477
  return;
@@ -463,7 +488,7 @@ export function openTeamConsole(ctx: {
463
488
  // The console is the management surface: idle and stopped stay
464
489
  // visible, and persistent agent roles are listed after the session.
465
490
  ? [
466
- ...listTeammates().map((t) => ({ key: t.name, kind: "teammate" as const })),
491
+ ...listAgents().map((t) => ({ key: t.name, kind: "agent" as const })),
467
492
  ...getRoles().map((def) => ({ key: def.name, kind: "role" as const })),
468
493
  ]
469
494
  : listTasks().map((task) => ({ key: task.id, kind: "task" as const }));
@@ -479,9 +504,9 @@ export function openTeamConsole(ctx: {
479
504
  };
480
505
 
481
506
  const headerLine = (): string => {
482
- const alive = livingTeammates();
507
+ const alive = livingAgents();
483
508
  const tasks = listTasks();
484
- return `team ${alive.length} alive · ${alive.filter(isWorking).length} working · ${getRoles().length} roles · board ${tasks.filter((task) => task.status === "pending").length}p/${tasks.filter((task) => task.status === "claimed").length}c/${tasks.filter((task) => task.status === "completed").length}d/${tasks.filter((task) => task.status === "superseded").length}s · model ${getTeamDefaultModel() ?? "auto"} · ${page}`;
509
+ return `team ${alive.length} alive · ${alive.filter(isWorking).length} working · ${getRoles().length} roles · tasks ${tasks.filter((task) => !isTaskReplaced(task) && task.status === "pending").length}p/${tasks.filter((task) => !isTaskReplaced(task) && task.status === "in_progress").length}c/${tasks.filter((task) => !isTaskReplaced(task) && task.status === "blocked").length}b/${tasks.filter((task) => !isTaskReplaced(task) && task.status === "cancelled").length}x/${tasks.filter((task) => task.status === "completed").length}d/${tasks.filter((task) => isTaskReplaced(task)).length}s · model ${getTeamDefaultModel() ?? "auto"} · ${page}`;
485
510
  };
486
511
 
487
512
  interface ContentLine {
@@ -491,14 +516,14 @@ export function openTeamConsole(ctx: {
491
516
  }
492
517
 
493
518
  const teammateRowText = (key: string, selected: boolean): string => {
494
- const teammate = getTeammate(key)!;
519
+ const teammate = getAgent(key)!;
495
520
  const marker = selected ? style.accent("❯ ") : " ";
496
521
  const name = renderLiveActivityIdentity(teammate.name, theme, "@");
497
522
  if (isWorking(teammate)) {
498
523
  const prefix = `${marker}${name} `;
499
524
  const width = tui.terminal.columns;
500
525
  const available = Math.max(0, width - prefix.length - PI_SPINNER_FRAMES[spinnerFrame].length - 2);
501
- const status = theme.fg("warning", formatTeammateLabel(PI_SPINNER_FRAMES[spinnerFrame], runningTeammateActivity(teammate) + stallSuffix(teammate), available));
526
+ const status = theme.fg("warning", formatAgentLabel(PI_SPINNER_FRAMES[spinnerFrame], runningTeammateActivity(teammate) + stallSuffix(teammate), available));
502
527
  return `${prefix}${status}`;
503
528
  }
504
529
  if (teammate.status === "idle") {
@@ -511,7 +536,7 @@ export function openTeamConsole(ctx: {
511
536
  const def = getRoles().find((candidate) => candidate.name === key)!;
512
537
  const marker = selected ? style.accent("❯ ") : " ";
513
538
  const name = renderLiveActivityIdentity(key, theme, "@");
514
- const live = livingTeammates().filter((teammate) => teammate.agent === key && teammate.status !== "stopped").length;
539
+ const live = livingAgents().filter((teammate) => teammate.role === key && teammate.status !== "stopped").length;
515
540
  const liveTag = live > 0 ? style.success(`[${live} live]`) : style.dim("[0 live]");
516
541
  const provenance = def.scope === "session" ? "in-memory" : displaySource(def.source);
517
542
  return `${marker}${name} ${style.dim(def.scope)} ${style.dim(provenance)} ${liveTag}`;
@@ -524,38 +549,39 @@ export function openTeamConsole(ctx: {
524
549
  const holder = task.claimedBy ? style.dim(` @${task.claimedBy}`) : "";
525
550
  const statusText = task.status === "completed"
526
551
  ? style.success("✓")
527
- : task.status === "superseded"
528
- ? style.dim("⊘ superseded")
529
- : task.status === "claimed"
530
- ? theme.fg("warning", "◐ claimed")
531
- : style.dim("○ pending");
552
+ : isTaskReplaced(task)
553
+ ? style.dim("replaced")
554
+ : task.status === "in_progress"
555
+ ? theme.fg("warning", "in progress")
556
+ : task.status === "blocked" ? style.dim("blocked")
557
+ : task.status === "cancelled" ? style.dim("cancelled") : style.dim("pending");
532
558
  return `${marker}${label} ${statusText} ${theme.fg("customMessageText", task.subject)}${holder}`;
533
559
  };
534
560
 
535
561
  const buildContent = (): ContentLine[] => {
536
562
  const rows = currentRows();
537
563
  const lines: ContentLine[] = [];
538
- if (page === "board") {
564
+ if (page === "tasks") {
539
565
  if (rows.length === 0) {
540
566
  lines.push({ text: style.dim("The Work list is empty. Create Work with work action=create."), select: -1 });
541
567
  }
542
568
  rows.forEach((row, index) => lines.push({ text: taskRowText(row.key, index === currentPageSelection(rows.length)), select: index }));
543
569
  return lines;
544
570
  }
545
- const teammates = rows.filter((row): row is RosterRow => row.kind === "teammate");
571
+ const agents = rows.filter((row): row is RosterRow => row.kind === "agent");
546
572
  const roles = rows.filter((row): row is RoleRow => row.kind === "role");
547
573
  const selection = currentPageSelection(rows.length);
548
- lines.push({ text: style.dim("== teammates (this session) =="), select: -1 });
549
- if (teammates.length === 0) {
550
- lines.push({ text: style.dim("No teammates this session. Ask the model to spawn one."), select: -1 });
574
+ lines.push({ text: style.dim("== agents (this session) =="), select: -1 });
575
+ if (agents.length === 0) {
576
+ lines.push({ text: style.dim("No agents this session. Ask the model to spawn one."), select: -1 });
551
577
  }
552
- teammates.forEach((row, index) => lines.push({ text: teammateRowText(row.key, index === selection), select: index }));
578
+ agents.forEach((row, index) => lines.push({ text: teammateRowText(row.key, index === selection), select: index }));
553
579
  lines.push({ text: "", select: -1 });
554
580
  lines.push({ text: style.dim("== agent roles (persistent and session definitions) =="), select: -1 });
555
581
  if (roles.length === 0) {
556
582
  lines.push({ text: style.dim("No agent definitions discovered."), select: -1 });
557
583
  }
558
- roles.forEach((row, index) => lines.push({ text: roleRowText(row.key, teammates.length + index === selection), select: teammates.length + index }));
584
+ roles.forEach((row, index) => lines.push({ text: roleRowText(row.key, agents.length + index === selection), select: agents.length + index }));
559
585
  return lines;
560
586
  };
561
587
 
@@ -572,9 +598,9 @@ export function openTeamConsole(ctx: {
572
598
  }
573
599
  listOffset = clampConsoleScroll(listOffset, content.length, viewport);
574
600
  const footer = confirmName
575
- ? theme.fg("warning", `Shut down @${confirmName}? Its claimed task returns to the board · y yes / n no`)
601
+ ? theme.fg("warning", `Shut down @${confirmName}? Its claimed task returns to the tasks · y yes / n no`)
576
602
  : style.dim(page === "roster"
577
- ? "↑↓ select · enter open · tab board · x shutdown · m model · esc/q close"
603
+ ? "↑↓ select · enter open · tab tasks · x shutdown · m model · esc/q close"
578
604
  : "↑↓ select · enter open · tab roster · esc/q close");
579
605
  return renderPiPanel({
580
606
  width,
@@ -587,7 +613,7 @@ export function openTeamConsole(ctx: {
587
613
  };
588
614
 
589
615
  const currentPageSelection = (rowCount: number): number => {
590
- const selection = page === "roster" ? selectedRoster : selectedBoard;
616
+ const selection = page === "roster" ? selectedRoster : selectedTasks;
591
617
  return rowCount === 0 ? 0 : Math.min(selection, rowCount - 1);
592
618
  };
593
619
 
@@ -638,7 +664,7 @@ export function openTeamConsole(ctx: {
638
664
  }
639
665
  const rows = currentRows();
640
666
  if (matchesKey(data, Key.tab)) {
641
- page = page === "roster" ? "board" : "roster";
667
+ page = page === "roster" ? "tasks" : "roster";
642
668
  listOffset = 0;
643
669
  return;
644
670
  }
@@ -658,7 +684,7 @@ export function openTeamConsole(ctx: {
658
684
  if (data === "x" || data === "X") {
659
685
  const selection = currentPageSelection(rows.length);
660
686
  const row = rows[selection];
661
- if (page === "roster" && row?.kind === "teammate" && getTeammate(row.key)?.status !== "stopped") {
687
+ if (page === "roster" && row?.kind === "agent" && getAgent(row.key)?.status !== "stopped") {
662
688
  confirmName = row.key;
663
689
  }
664
690
  return;
@@ -701,7 +727,7 @@ export function openTeamConsole(ctx: {
701
727
  function bumpSelection(delta: number, rowCount: number): void {
702
728
  if (rowCount === 0) return;
703
729
  if (page === "roster") selectedRoster = clampIndex(selectedRoster + delta, rowCount);
704
- else selectedBoard = clampIndex(selectedBoard + delta, rowCount);
730
+ else selectedTasks = clampIndex(selectedTasks + delta, rowCount);
705
731
  }
706
732
 
707
733
  function scrollList(delta: number): void {
@@ -737,7 +763,7 @@ export function openTeamConsole(ctx: {
737
763
  }
738
764
 
739
765
  async function shutdownFromConsole(ctx: { ui: ExtensionUIContext }, name: string): Promise<void> {
740
- const result = await shutdownTeammate(name);
766
+ const result = await shutdownAgent(name);
741
767
  if (result.ok) notifyPi(ctx.ui, result.body, "info");
742
768
  else notifyPi(ctx.ui, result.error, "error");
743
769
  }