@fradser/pi-agent-teams 0.5.3 → 0.6.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/.memory/MEMORY.md CHANGED
@@ -1,3 +1,5 @@
1
1
  # Memory Index
2
2
 
3
- - `openai_teammate_api_reference.md` — OpenAI external Teammate / Workspace Agents API reference (Workspace Agents API, Files API, file search, vector stores, Threads API). Reference only, not for Pi extension usage guidance.
3
+ - `openai_teammate_api_reference.md` — OpenAI external Teammate / Workspace Agents API reference (Workspace Agents API, Files API, file search, vector stores, Threads API). Reference only, not for Pi extension usage guidance.
4
+
5
+ - `team-centric-design.md` — Agent Teams current architecture: named resident RPC teammates, shared task board with atomic self-claim, direct P2P inboxes, verify-gated completion, harness-driven wake-ups. Supersedes the deleted run-centric DAG design.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: node-terminal-followups
3
- description: Background Agent Teams runs notify the main session after each non-summary teammate reaches a terminal outcome, including pre-spawn setup failures, then send a separate run summary when all nodes settle
3
+ description: Background Agent Teams runs notify the main session after each non-summary teammate reaches a terminal outcome, with completion shown separately from the agent report and no automatic run summary
4
4
  type: project
5
5
  ---
6
6
 
@@ -13,5 +13,6 @@ The leader needs incremental results while independent teammates are still worki
13
13
  - `run-machine.ts` sends one node follow-up for each non-summary node in a background run when it reports `completed`/`failed`, when its child process reaches a terminal outcome, or when pre-spawn setup fails (agent/state-file/worktree setup).
14
14
  - `Node.nodeFollowUpSent` prevents duplicate follow-ups when a worker report is followed by process-close finalization; retry clears the marker.
15
15
  - The node follow-up uses the worker's full terminal report/body and does not stop remaining teammates.
16
- - The existing run-settled follow-up remains separate and is sent once after all nodes settle; foreground (`background=false`) runs continue to return their result inline without automatic node follow-ups.
16
+ - Run settlement remains internal to leader state and foreground (`background=false`) runs continue to return their result inline, but background settlement does not enqueue a separate run-summary follow-up.
17
+ - The transport content contains only `<agent-message from="<agent>">` plus the worker's full body. The completion notice is rendered separately as `Teammate @<agent> finished.` and is never embedded in the report content.
17
18
  - Worker turn budgets default to 100 assistant turns as a high safety cap; explicit lower budgets remain available for deliberately bounded tasks.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: teammate-resident-teams
3
+ description: Agent Teams (packages/agent-teams) — team-centric: named resident RPC teammates, shared task board with atomic self-claim, unified addressed messaging, verify-gated completion, harness-driven wake-ups
4
+ type: project
5
+ ---
6
+
7
+ `packages/agent-teams` is a Claude-Code-style collaborative organization layer for Pi: the Pi session is the team leader; teammates are named resident child Pi processes in RPC mode; coordination runs through a shared local task board plus direct peer inbox messaging. It replaces the former run-centric DAG design (`teammate_run`/fanout/retry are deleted).
8
+
9
+ **Why:**
10
+ Exploratory / competitive-hypothesis work (multi-hypothesis debugging, three-way review with cross-challenge) cannot be represented cleanly by a static DAG because workers must exchange findings mid-task. One-step delegation avoids the old registry ceremony: `teammate_spawn(name, agent, prompt?)` carries the kickoff task and the board is optional. A later grilling decision identified tool count, conceptual overlap, and parameter breadth as separate problems: the surface was reduced from ten names to seven by making `send_message` the one addressed message primitive, registering `task_list` once for both sides, and moving model/worktree from spawn overrides into agent frontmatter.
11
+
12
+ **How to apply:**
13
+ 1. **Seven tool names**: leader gets `teammate_spawn(name, agent, prompt?)`, `teammate_shutdown(name)`, `send_message(to, message, status?)`, `task_create`, and shared `task_list`; workers get the same `send_message`, shared `task_list`, `task_claim`, and `task_submit`. There is no `teammate_message`, cancel/retry/run/fanout, or polling tool.
14
+ 2. **Unified messaging**: `send_message(to="leader", message, status?)` is a worker report written to the validated outbox; `send_message(to="<peer>", message)` writes the recipient inbox directly; leader-addressed teammate messages route through a running control stream or idle wake queue. `status` is invalid except for `to="leader"`. The first non-empty message line is the console title.
15
+ 3. **Declarative role attributes**: agent frontmatter carries `model`, optional `verify`, and boolean `worktree`.Definitions live in one project directory with filename-based scopes: `<name>.local.md` is project-local (personal, never committed) and dedupes against its `<name>.md` counterpart (project scope, git-managed, team-shared); user-level sits in `~/.pi/agent/agents`; precedence is project-local > project > user > bundled and `resolveAgent` exposes scope + gitManaged for provenance. Spawn has no cwd/model/worktree override; create a role variant instead. `worktree: true` gives every instance of that role a dedicated Git worktree and diff capture at shutdown.
16
+ 4. **Residents/wake-ups (constitution: detect and notify, never terminate)**: long-lived RPC children, 8-process session cap; wake-up sequences have NO turn-count or duration caps — no configuration may automatically terminate a working teammate. Turn counts and silence durations are telemetry only. The 500ms harness poll drains outboxes, applies intents, routes inboxes, wakes only for queued deliveries or a paced claimable-task notice, and runs the stall heartbeat: 30 minutes without any RPC output (`PI_TEAMMATE_STALL_NOTICE_MS`, 0 disables) sends the leader one actionable notice per silence episode — that notice is the LAST automatic action; continuing, steering, shutting down, or respawning a context-carrying successor (original kickoff + mailbox reports + board claims + liveText tail) belongs to the leader model alone. Any stream event or prompt delivery resets `lastOutputAt` and clears `stallNoticeSentAt`; steering a silent working teammate returns `stalledMs` so the send_message result warns delivery is uncertain.
17
+ 5. **Board**: only leader writes `~/.pi/agent/tasks/<sessionKey>/board.json`; workers use exclusive-create claim/submission marker files. Task completion uses effective task verify or agent verify; failed verification returns stderr to the claimer. The board persists across restarts; runtime state does not.
18
+ 6. **Reliability/UI**: per-spawn identity validation, event/message dedup, crash releases claimed tasks, role worktree diff at shutdown. Widget above the input box shows ONLY working/starting teammates as spinner rows with live activity and hides when nobody is working; idle/stopped teammates live only in the /teammate console roster; spawn prints a legacy-style `[agent] started · @name · task` transcript line; `/teammate` offers roster/board pages and message/task detail.
19
+ 7. **Node-native TS**: relative imports use explicit `.ts` extensions (root tsconfig permits them), allowing node to strip-types modules in package tests.
20
+
21
+ **Related:** [[pi-cli-print-json-usage]] [[no-global-input-interception]] [[pi-package-conventions]]
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # Agent Teams Pi Package
2
2
 
3
- Run-centric multi-agent system for Pi — declarative agents, single-call DAG dispatch, bounded child-process nodes, and one-way worker reports to the leader.
3
+ Claude-Code-style collaborative agent teams for Pi: named resident teammates, a shared local task board with self-claim, and direct peer-to-peer messaging.
4
4
 
5
5
  **Display Name**: Agent Teams
6
6
 
7
7
  ## What This Package Does
8
8
 
9
- Agents are declarative Markdown files (bundled, user, and project scopes). A run is a dependency-aware task graph dispatched in **one call**: root nodes start immediately, concurrency is bounded, overlapping shared-workspace writes are deferred (advisory coordination across all runs in the session) unless worktree-isolated, and downstream nodes auto-start when their dependencies complete. Each node is a bounded child Pi process with per-spawn identity validation.
9
+ The team leader (your Pi session) spawns named teammates as long-lived child Pi processes in RPC mode. Each teammate has an isolated context, a peer inbox, and access to a shared task board. The harness—not the leader model—polls for activity: it drains leader reports, routes peer mail, applies task claims and submissions, runs deterministic verify gates, and wakes idle teammates only when work or mail exists.
10
+
11
+ The message surface is deliberately singular: `send_message` is the only messaging primitive. Workers report with `to: "leader"`; teammates address peers by name; the leader addresses teammates by name. The routing implementation varies by destination (outbox, inbox, or control stream), but callers use one schema.
10
12
 
11
13
  ## Install
12
14
 
@@ -16,96 +18,130 @@ pi install /path/to/pi-packages/packages/agent-teams
16
18
 
17
19
  Then run `/reload` in Pi.
18
20
 
19
- ## Agents are declarative files
21
+ ## Agents Are Declarative Files
20
22
 
21
- Agent definitions are Markdown files with frontmatter; the body is the worker's role prompt.
23
+ Agent definitions are Markdown files with frontmatter; the body is the role prompt.
22
24
 
23
25
  ```markdown
24
26
  ---
25
- name: security-auditor
26
- description: Read-only security reviewer; use after security-sensitive edits
27
+ name: isolated-security-auditor
28
+ description: Read-only security reviewer in an isolated worktree
27
29
  tools: read,bash
28
- model: provider/model # optional
30
+ model: provider/model # optional
31
+ verify: npm test # optional role-default completion gate
32
+ worktree: true # optional role-default Git isolation
29
33
  ---
30
34
  Review the assigned scope for exploitable security problems. Do not edit files.
31
35
  ```
32
36
 
33
- Discovery precedence per name: **project `.pi/agents/` > user `~/.pi/agent/agents/` > bundled package `agents/`**. Bundled agents: `worker`, `reviewer`, `specialist`, `observer`. Available agents are dynamically injected into prompt guidance via `before_agent_start`.
37
+ Discovery precedence per name (later overrides earlier):
38
+
39
+ | Scope | Location | Git semantics |
40
+ |---|---|---|
41
+ | bundled | package `agents/` | shipped with the package |
42
+ | user | `~/.pi/agent/agents/*.md` | system-level, never committed |
43
+ | project | `<cwd>/.pi/agents/<name>.md` | **git-managed** — commit for the team |
44
+ | project-local | `<cwd>/.pi/agents/<name>.local.md` | **local** — personal override, gitignore by convention |
45
+
46
+ A teammate definition is just a Markdown file you own. Shared roles live in `.pi/agents/<name>.md`; a personal tweak to that exact role is `<name>.local.md` in the SAME directory — same teammate name, local scope wins, and discovery deduplicates the pair into one entry (never two). `resolveAgent` reports each definition's `scope` and `gitManaged`; guidance lists both so the leader knows provenance.
34
47
 
35
- ## Dispatch a run in one call
48
+ ## Build a Team
36
49
 
37
50
  ```text
38
- teammate_run({
39
- tasks: [
40
- { id: "inspect", agent: "reviewer", prompt: "Review the auth middleware", paths: ["packages/api/src"], access: "read" },
41
- { id: "fix", agent: "worker", prompt: "Apply the review findings", paths: ["packages/api"], access: "write", dependsOn: ["inspect"] },
42
- { id: "verify", agent: "reviewer", prompt: "Verify the fix", paths: ["packages/api"], access: "read", dependsOn: ["fix"] },
43
- ],
44
- concurrency: 2,
45
- worktree: false,
46
- background: true
51
+ teammate_spawn({
52
+ name: "security",
53
+ agent: "reviewer",
54
+ prompt: "Review the auth middleware"
47
55
  })
56
+
57
+ teammate_spawn({ name: "backend", agent: "worker" })
48
58
  ```
49
59
 
50
- - `access` defaults to `read`; declare `write` explicitly.
51
- - `dependsOn` edges must form a DAG (duplicate ids, unknown references, and cycles are rejected before any worker starts).
52
- - `worktree: true` runs every node in its own git worktree and captures each diff for integration review.
53
- - `background` defaults to `true`: teammates always run in the background — the call returns the run id immediately, the model turn stays free, and each teammate's terminal outcome sends its full deliverable in an immediate follow-up while the remaining teammates continue. A final run-completion follow-up is sent after all nodes settle. The team leader does not sleep or busy-wait while tasks execute. Pass `background=false` to block and gather inline (it detaches after 5 minutes so the turn is never hung).
54
- - `turnBudget` defaults to a high safety cap of 100 assistant turns; use an explicit lower value only when a task needs tighter bounds. This protects against edge cases without interrupting normal work.
55
- - `timeoutMs` is a run-level hard cap: when exceeded, the run fails and live workers are terminated.
56
- - A session-wide cap of 8 worker processes applies in addition to each run's `concurrency` limit.
57
- - Multi-node runs append a `__summary` node by default (`summarize=false` to skip). Single-task runs stay compact unless `summarize=true`.
58
- - Completing a node hands its result to pending dependents through the spawned worker prompt. Workers have no peer-message or inbound-reply path.
60
+ - Spawn has exactly three parameters: `name`, `agent`, and optional kickoff `prompt`.
61
+ - Without a kickoff prompt, the teammate idles until messaged or until claimable work appears.
62
+ - Teammates stay alive between tasks and consume no model tokens while idle.
63
+ - A session-wide cap of 8 living teammates applies.
64
+ - An agent declaring `worktree: true` gets its own Git worktree; its diff is captured at shutdown.
65
+ - `teammate_shutdown({ name })` stops one teammate and releases its claimed task back to the board.
59
66
 
60
- ## Tools
67
+ ## Coordinate Through One Message Primitive and the Board
68
+
69
+ ```text
70
+ send_message({
71
+ to: "security",
72
+ message: "Please challenge the backend finding against the current middleware."
73
+ })
74
+
75
+ task_create({
76
+ subject: "Fix auth middleware findings",
77
+ description: "Address the confirmed security findings in packages/api/src/auth.ts",
78
+ dependsOn: ["t_1"],
79
+ verify: "npm test"
80
+ })
81
+ ```
82
+
83
+ `send_message` has one schema everywhere:
61
84
 
62
- | Tool | Description |
63
- |---|---|
64
- | `teammate_run` | Dispatch a dependency-aware task graph in one call |
65
- | `teammate_cancel` | Cancel a run, or one node (`nodeId`) while the rest continues |
66
- | `teammate_retry` | Re-run only the failed/cancelled nodes of a settled run |
67
- | `/teammate` | Full-screen console: run/node status, node detail, worker reports, cancel node |
85
+ ```text
86
+ send_message({ to, message, status? })
87
+ ```
88
+
89
+ - Worker report: `to: "leader"`; use optional `status: "completed" | "failed"` for a terminal report.
90
+ - Peer mail: `to: "<teammate-name>"`; `status` is invalid.
91
+ - Leader steering or direct assignment: `to: "<teammate-name>"`; a working recipient gets a control-stream steer, an idle recipient wakes with the message.
92
+ - The first non-empty line of `message` becomes the console title.
93
+ - Peer traffic never enters the leader model context; inspect it in `/teammate` instead.
94
+
95
+ Only the leader creates tasks. Idle teammates self-claim pending tasks whose dependencies are met. A task-level `verify` command overrides the claiming agent's frontmatter `verify`; zero exit completes the task, while failure returns stderr to the claimer for fix-and-resubmit.
96
+
97
+ ## Tools
68
98
 
69
- Spawned workers receive one additional capability, `teammate_message`, for progress notes and final reports to the team leader.
99
+ | Tool / command | Side | Description |
100
+ |---|---|---|
101
+ | `teammate_spawn(name, agent, prompt?)` | Leader | Start one named resident teammate; model/worktree come from agent frontmatter |
102
+ | `teammate_shutdown(name)` | Leader | Stop one teammate and release its claimed work |
103
+ | `send_message(to, message, status?)` | Both | The only messaging primitive; `to: "leader"` is reserved for worker reports |
104
+ | `task_create(subject, description?, dependsOn?, verify?)` | Leader | Add a shared board task |
105
+ | `task_list()` | Both | One shared board-view definition; leader view also includes the roster |
106
+ | `task_claim(taskId?)` | Worker | Atomically self-claim a pending, unblocked task |
107
+ | `task_submit(taskId, status, result?)` | Worker | Submit a claimed task outcome; completion passes through verify |
108
+ | `/teammate` | User | Full-screen roster and board console, message/task details, shutdown |
70
109
 
71
- Messaging is deliberately one-way: workers append validated reports to their own outbox, and the leader drains them into one leader inbox. There is no peer mailbox, worker inbox, leader broadcast tool, or worker-to-worker delivery path. Dependency results are delivered through the DAG prompt (`=== UPSTREAM HANDOFF ===`) when the dependent starts.
110
+ That is **7 unique tool names**. There are no `teammate_run`, `teammate_fanout`, `teammate_cancel`, `teammate_retry`, or `teammate_message` tools.
72
111
 
73
- ## Reliability protocol
112
+ ## Reliability Protocol
74
113
 
75
- - **Per-spawn identity validation**: every worker event must match the node's current spawn id; stale events from an older process cannot affect a newer spawn.
76
- - **One-way message storage**: worker event ids and per-spawn identities are validated and deduplicated; every accepted report lands in the single leader inbox. No read flags, receipts, peer mailboxes, or leader-to-worker inboxes exist.
77
- - **One canonical terminal result per node**: built by the harness from node state + captured output after the child closes; a worker's message with status alone is not final delivery.
78
- - **Advisory write-conflict coordination (session-wide)**: the scheduler never starts a shared-workspace write node while another shared-workspace write node with overlapping paths is running — checked across **all runs in the session**, not just the same run. This is scheduling-level coordination, not isolation.
79
- - **`access`/`paths` are metadata, not enforcement**: they drive conflict scheduling and prompts; a worker's real capabilities come from its agent definition's `tools` list. A `read` node whose agent has `write`/`bash` tools can still write. Use `worktree: true` or a restricted agent tool list when you need actual isolation.
80
- - **Failure semantics**: a failed node cancels its not-yet-started transitive dependents and fails the run; other nodes finish. Cancelling a run or node reports the outcome in the tool call itself — it does not fire an extra completion follow-up.
114
+ - **Per-spawn identity validation**: every leader report must match the teammate's current spawn id; stale callbacks and events cannot affect a replacement with the same name.
115
+ - **Sent means written**: peer `send_message` succeeds only after the recipient inbox write succeeds. The harness owns delivery into a recipient turn.
116
+ - **One writer per state file**: only the leader process writes runtime and board snapshots (atomic tmp+rename). Workers append leader reports to their outbox, peer mail to recipient inboxes, and task intent via exclusive-create marker files.
117
+ - **Completion is gated, not self-reported**: a task completes only after its effective verify gate passes when one exists; no gate means the submission itself completes it.
118
+ - **No caps, heartbeat only**: teammates run without turn-count or duration ceilings. The harness heartbeat tracks silence per working teammate and — after 30 minutes without any RPC output (`PI_TEAMMATE_STALL_NOTICE_MS`, 0 disables) — sends the leader one actionable notice per silence episode. The notice is the last automatic action: continuing, steering, shutting down, or respawning a context-carrying successor belongs to the leader alone. Any output or prompt delivery re-arms it, and steering a silent teammate warns that delivery is uncertain.
119
+ - **Failure semantics**: an unexpected crash marks the teammate stopped, reports a diagnostic, and releases its claimed tasks.
81
120
 
82
- ## State and sessions
121
+ ## State and Sessions
83
122
 
84
- State is session-scoped and dies with the session (`~/.pi/agent/teammate/<sessionKey>/`); runs do not survive restarts. `background: true` runs notify via one follow-up; worker messages and node outcomes are recorded in the leader transcript and visible in `/teammate`. Agents, by contrast, live in files and survive across sessions — that is the persistent layer.
123
+ Runtime state lives in `~/.pi/agent/teammate/<sessionKey>/` and dies with the session (`state.json`, report outboxes, peer inboxes, roster). The task board lives in `~/.pi/agent/tasks/<sessionKey>/board.json` and persists for later inspection; nothing auto-deletes it. Claims held by dead teammates return to pending on reload.
85
124
 
86
125
  ## Structure
87
126
 
88
127
  ```
89
128
  agent-teams/
90
- ├── index.ts — package-root extension entry point
91
- ├── package.json — Pi package manifest
92
- ├── agents/ — bundled agent definitions (worker/reviewer/specialist/observer)
129
+ ├── index.ts — package-root extension entry point
130
+ ├── agents/ — bundled declarative agent definitions
93
131
  ├── src/
94
- │ ├── index.ts — composition root: Pi hooks and session lifecycle
95
- │ ├── tools.ts — leader tool and /teammate command registration
96
- │ ├── run-machine.ts — DAG scheduler, worker lifecycle, persistence, session cap
97
- │ ├── ui.ts — passive widget and /teammate console
98
- │ ├── worker.ts — worker identity binding and report capability
99
- │ ├── guidance.ts — static leader/worker protocol guidance
100
- │ ├── agents.ts — declarative agent discovery (frontmatter parsing, scope precedence)
101
- │ ├── state.ts — runs/nodes state machine, leader inbox, dirty tracking
102
- │ ├── spawner.ts — child Pi worker spawner (JSON-mode, usage accounting)
103
- │ ├── statefile.ts — leader snapshot + worker outbox IO
104
- │ ├── worktree.ts — git worktree isolation
105
- │ ├── console-viewport.ts — console viewport math (wrap, scroll clamp, ranges)
106
- │ └── terminal.ts — canonical per-node terminal result builder
107
- ├── features/ — BDD contract
108
- └── tests/ — package E2E tests
132
+ │ ├── index.ts — composition root and session lifecycle
133
+ │ ├── tools.ts — leader tools and /teammate command
134
+ │ ├── worker.ts — worker tools plus the shared task_list registration
135
+ │ ├── team-machine.ts — resident lifecycle, mail routing, intents, verify, wake-ups
136
+ │ ├── state.ts — roster, board, leader inbox state machine
137
+ │ ├── spawner.ts — resident RPC process spawner (uncapped sequences)
138
+ │ ├── agents.ts — agent discovery and frontmatter parsing
139
+ │ ├── statefile.ts — runtime/board/mail IO and marker-file intents
140
+ │ ├── ui.ts — passive widget and /teammate console
141
+ │ ├── guidance.ts — static leader and worker protocols
142
+ │ └── follow-up-queue.ts — serialized automatic report delivery
143
+ ├── features/ — BDD contract
144
+ └── tests/ — package tests
109
145
  ```
110
146
 
111
147
  ## License
@@ -6,5 +6,5 @@ tools: read
6
6
  You are an observer agent. Observe and report on the assigned scope without
7
7
  changing anything. Take the required snapshots or readings, summarize what you
8
8
  observed, and report notable events or anomalies with timestamps and evidence
9
- via teammate_message to team-leader with status="completed". You have no edit,
9
+ via send_message(to="leader", message=...) with status="completed". You have no edit,
10
10
  write, or command tools.
@@ -6,6 +6,8 @@ tools: read,bash
6
6
  You are a reviewer agent. Read the relevant code and tests first, then review
7
7
  the assigned scope for correctness, regressions, and security problems. Do not
8
8
  edit files. Report only confirmed findings with severity, evidence, exact
9
- paths, and a minimal fix recommendation via teammate_message to team-leader with
10
- status="completed". When no issue is confirmed, say so and list the checks
9
+ paths, and a minimal fix recommendation via send_message(to="leader",
10
+ message=...) with status="completed". Your LAST message to the leader MUST
11
+ carry status="completed" (or status="failed"); a review without a terminal
12
+ status is an unfinished review. When no issue is confirmed, say so and list the checks
11
13
  you performed.
@@ -5,6 +5,6 @@ tools: read,bash
5
5
  ---
6
6
  You are a specialist agent. Apply deep domain judgment to the assigned scope:
7
7
  investigate, analyze, and produce findings or recommendations with evidence via
8
- teammate_message to team-leader with status="completed". Do not edit files.
8
+ send_message(to="leader", message=...) with status="completed". Do not edit files.
9
9
  State your assumptions, the evidence you used, and the bounds of your expertise
10
10
  for this task.
package/agents/worker.md CHANGED
@@ -3,9 +3,9 @@ name: worker
3
3
  description: General executor that implements changes and verifies them; use when a task needs code or file modifications, or when no more specific agent fits
4
4
  tools: read,bash,edit,write
5
5
  ---
6
- You are a worker agent. Implement the assigned task end to end within the
7
- declared paths and access scope, then verify your own work by running the
6
+ You are a worker agent. Implement the assigned task end to end within your
7
+ agent tool scope, then verify your own work by running the
8
8
  relevant tests or commands. Follow the task's ordered procedure. Report what
9
9
  you changed, what you verified, and any risks or follow-up work via
10
- teammate_message to team-leader with status="completed". Do not touch
10
+ send_message(to="leader", message=...) with status="completed". Do not touch
11
11
  files outside the assigned scope.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fradser/pi-agent-teams",
3
- "version": "0.5.3",
4
- "description": "Multi-agent team system for Pi — DAG orchestration, leader inbox, and one-way worker reports",
3
+ "version": "0.6.0",
4
+ "description": "Multi-agent team system for Pi — resident teammates, shared task board, and peer-to-peer messaging",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "teammates",
@@ -39,6 +39,6 @@
39
39
  ]
40
40
  },
41
41
  "dependencies": {
42
- "@fradser/pi-kit": "0.1.1"
42
+ "@fradser/pi-kit": "0.2.0"
43
43
  }
44
44
  }
@@ -0,0 +1,86 @@
1
+ import { Markdown, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
+ import type { MarkdownTheme } from "@earendil-works/pi-tui";
3
+ import { buildMarkdownThemeCallbacks, createPiThemeStyle } from "@fradser/pi-kit";
4
+ import type { Teammate } from "./types.ts";
5
+
6
+ const ACTIVITY_THEME: MarkdownTheme = {
7
+ ...buildMarkdownThemeCallbacks(createPiThemeStyle({
8
+ fg: (_color, text) => text,
9
+ })),
10
+ hr: () => "---",
11
+ };
12
+
13
+ /** Render streamed activity as one compact Markdown line for the widget. */
14
+ export function renderActivityMarkdown(text: string, theme: MarkdownTheme = ACTIVITY_THEME): string {
15
+ const markdown = new Markdown(text, 0, 0, theme);
16
+ return markdown.render(Math.max(1, visibleWidth(text) + 1)).join(" ").replace(/\s+/g, " ").trim();
17
+ }
18
+
19
+ function extractLatestLine(text: string | undefined): string | undefined {
20
+ if (!text) return undefined;
21
+ const lines = text.split("\n");
22
+ for (let i = lines.length - 1; i >= 0; i--) {
23
+ const trimmed = lines[i].replace(/\s+/g, " ").trim();
24
+ if (trimmed.length > 0 && !trimmed.startsWith("... [truncated") && !trimmed.startsWith("…[truncated")) {
25
+ return trimmed;
26
+ }
27
+ }
28
+ return undefined;
29
+ }
30
+
31
+ /** Extract the current teammate activity without imposing a display width. */
32
+ export function runningTeammateActivity(teammate: Teammate): string {
33
+ const tool = teammate.activeTool?.replace(/\s+/g, " ").trim();
34
+ if (tool) return tool;
35
+
36
+ return extractLatestLine(teammate.liveThinking)
37
+ ?? extractLatestLine(teammate.liveText)
38
+ ?? "Working...";
39
+ }
40
+
41
+ export interface TeammateRowWidths {
42
+ lineWidth: number;
43
+ nameWidth: number;
44
+ activityWidth: number;
45
+ }
46
+
47
+ /** Reserve enough width for the live activity before truncating the identity. */
48
+ export function teammateRowWidths(spinner: string, agent: string, width: number): TeammateRowWidths {
49
+ const lineWidth = Math.max(10, width - 1);
50
+ const fixedWidth = visibleWidth(` ${spinner} · `);
51
+ const availableWidth = Math.max(2, lineWidth - fixedWidth);
52
+ const nameWidth = Math.max(1, Math.min(visibleWidth(agent), Math.floor(availableWidth * 0.4)));
53
+ return {
54
+ lineWidth,
55
+ nameWidth,
56
+ activityWidth: Math.max(1, availableWidth - nameWidth),
57
+ };
58
+ }
59
+
60
+ /** Fit one widget row to its terminal width while keeping it on one line. */
61
+ export function fitTeammateRow(
62
+ spinner: string,
63
+ agent: string,
64
+ activity: string,
65
+ width: number,
66
+ formatActivity: (text: string) => string = (text) => text,
67
+ markdownTheme: MarkdownTheme = ACTIVITY_THEME,
68
+ ): string {
69
+ const sizes = teammateRowWidths(spinner, agent, width);
70
+ const nameText = truncateToWidth(agent, sizes.nameWidth);
71
+ const activityText = formatActivity(truncateToWidth(renderActivityMarkdown(activity, markdownTheme), sizes.activityWidth));
72
+ const line = ` ${spinner} ${nameText} · ${activityText}`;
73
+ return truncateToWidth(line, sizes.lineWidth);
74
+ }
75
+
76
+ /** Fit a console status label to the requested activity width. */
77
+ export function formatTeammateLabel(
78
+ spinner: string,
79
+ activity: string,
80
+ maxActivityWidth?: number,
81
+ markdownTheme: MarkdownTheme = ACTIVITY_THEME,
82
+ ): string {
83
+ if (maxActivityWidth === undefined) return `${spinner} ${renderActivityMarkdown(activity, markdownTheme)}`;
84
+ if (maxActivityWidth <= 0) return spinner;
85
+ return `${spinner} ${truncateToWidth(renderActivityMarkdown(activity, markdownTheme), maxActivityWidth)}`;
86
+ }
package/src/agents.ts CHANGED
@@ -2,42 +2,58 @@
2
2
  * Declarative agent definitions: Markdown files with YAML-style frontmatter.
3
3
  *
4
4
  * Discovery scopes (later scopes override earlier ones for the same name):
5
- * 1. bundled — agents shipped with this package (agents/)
6
- * 2. user — ~/.pi/agent/agents/ (respects PI_CODING_AGENT_DIR)
7
- * 3. project — <cwd>/.pi/agents/
5
+ * 1. bundled — agents shipped with this package (agents/)
6
+ * 2. user — ~/.pi/agent/agents/ (respects PI_CODING_AGENT_DIR)
7
+ * 3. project — <cwd>/.pi/agents/<name>.md
8
+ * 4. project-local — <cwd>/.pi/agents/<name>.local.md (personal override)
8
9
  *
9
10
  * Frontmatter fields:
10
11
  * name — unique agent id (required)
11
12
  * description — routing contract: when the leader should choose this agent
12
13
  * tools — comma-separated list or YAML list of Pi tool ids
13
14
  * model — optional provider/model pin
15
+ * verify — optional role-default completion gate command (zero exit passes)
16
+ * worktree — optional role-default Git worktree isolation (true/false)
14
17
  * The Markdown body is the worker's role prompt.
15
18
  */
16
19
 
17
20
  import * as fs from "node:fs";
18
- import * as os from "node:os";
19
21
  import * as path from "node:path";
20
22
  import { fileURLToPath } from "node:url";
23
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
24
+
25
+ export type AgentScope = "bundled" | "user" | "project" | "project-local";
21
26
 
22
27
  export interface AgentDefinition {
23
28
  name: string;
24
29
  description: string;
25
30
  tools: string[];
26
31
  model?: string;
27
- /** The Markdown body, trimmed; used as the worker role prompt. */
32
+ /** Role-default completion gate; task-level verify overrides this. */
33
+ verify?: string;
34
+ /** Whether this role always receives a dedicated Git worktree. */
35
+ worktree: boolean;
36
+ /** The Markdown body, trimmed; used as the teammate's role prompt. */
28
37
  prompt: string;
29
- /** Scope that supplied this definition ("bundled" | "user" | "project"). */
30
- scope: "bundled" | "user" | "project";
38
+ /** Scope that supplied this definition (precedence: project-local > project > user > bundled). */
39
+ scope: AgentScope;
40
+ /** True only for the shared project scope that is expected to be git-managed.
41
+ * user and project-local definitions are machine/personal by definition. */
42
+ gitManaged: boolean;
31
43
  /** Absolute path of the definition file. */
32
44
  source: string;
33
45
  }
34
46
 
35
47
  const BUNDLED_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "agents");
36
48
 
49
+ /** Files named `xxx.local.md` in the project agents directory are personal
50
+ * overrides: same teammate name as `xxx.md`, project-local scope, never git-
51
+ * managed by convention. */
52
+ export const LOCAL_DEFINITION_SUFFIX = ".local.md";
53
+
37
54
  function agentsDir(scope: "user" | "project", cwd?: string): string {
38
55
  if (scope === "user") {
39
- const base = process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
40
- return path.join(base, "agents");
56
+ return path.join(getAgentDir(), "agents");
41
57
  }
42
58
  return path.join(cwd || process.cwd(), ".pi", "agents");
43
59
  }
@@ -66,14 +82,20 @@ function parseFrontmatter(raw: string): { fields: Record<string, string | string
66
82
  return { fields, body: match[2].trim() };
67
83
  }
68
84
 
69
- function loadDir(scope: "bundled" | "user" | "project", dir: string, agents: Map<string, AgentDefinition>): void {
85
+ function loadDir(baseScope: AgentScope, dir: string, agents: Map<string, AgentDefinition>): void {
70
86
  let entries: fs.Dirent[];
71
87
  try {
72
88
  entries = fs.readdirSync(dir, { withFileTypes: true });
73
89
  } catch {
74
90
  return; // A missing agents directory is not an error.
75
91
  }
76
- for (const entry of entries) {
92
+ // Dedup by name within the directory: plain .md first so a same-name
93
+ // xxx.local.md (personal override) overwrites it in the map.
94
+ const ordered = [...entries].sort((left, right) =>
95
+ Number(left.name.endsWith(LOCAL_DEFINITION_SUFFIX)) - Number(right.name.endsWith(LOCAL_DEFINITION_SUFFIX))
96
+ || left.name.localeCompare(right.name),
97
+ );
98
+ for (const entry of ordered) {
77
99
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
78
100
  const source = path.join(dir, entry.name);
79
101
  let raw: string;
@@ -83,25 +105,35 @@ function loadDir(scope: "bundled" | "user" | "project", dir: string, agents: Map
83
105
  continue;
84
106
  }
85
107
  const { fields, body } = parseFrontmatter(raw);
86
- const name = typeof fields.name === "string" ? fields.name : entry.name.replace(/\.md$/, "");
108
+ const isLocal = baseScope === "project" && entry.name.endsWith(LOCAL_DEFINITION_SUFFIX);
109
+ const baseName = isLocal
110
+ ? entry.name.slice(0, -LOCAL_DEFINITION_SUFFIX.length)
111
+ : entry.name.replace(/\.md$/, "");
112
+ const name = typeof fields.name === "string" ? fields.name : baseName;
87
113
  if (!name || !body) continue;
88
- const tools = Array.isArray(fields.tools) ? fields.tools : [];
89
- const model = typeof fields.model === "string" && fields.model ? fields.model : undefined;
90
114
  agents.set(name, {
91
115
  name,
92
116
  description: typeof fields.description === "string" ? fields.description : "",
93
- tools,
94
- model,
117
+ tools: Array.isArray(fields.tools) ? fields.tools : [],
118
+ model: typeof fields.model === "string" && fields.model ? fields.model : undefined,
119
+ verify: typeof fields.verify === "string" && fields.verify ? fields.verify : undefined,
120
+ worktree: fields.worktree === "true",
95
121
  prompt: body,
96
- scope,
122
+ scope: isLocal ? "project-local" : baseScope,
123
+ gitManaged: scopeIsGitManaged(isLocal ? "project-local" : baseScope),
97
124
  source,
98
125
  });
99
126
  }
100
127
  }
101
128
 
129
+ /** Only the shared project layer is expected to be git-managed. */
130
+ function scopeIsGitManaged(scope: AgentScope): boolean {
131
+ return scope === "project";
132
+ }
133
+
102
134
  /**
103
135
  * Resolve all agent definitions visible from the given cwd, with later scopes
104
- * overriding earlier ones per name: project > user > bundled.
136
+ * overriding earlier ones per name: project-local > project > user > bundled.
105
137
  */
106
138
  export function discoverAgents(cwd?: string): Map<string, AgentDefinition> {
107
139
  const agents = new Map<string, AgentDefinition>();
@@ -126,13 +158,18 @@ export function hasAgent(name: string, cwd?: string): boolean {
126
158
  */
127
159
  export function formatAgentGuidance(cwd?: string): string {
128
160
  const agents = discoverAgents(cwd);
129
- if (agents.size === 0) return "(none found in bundled, user, or project scopes)";
161
+ if (agents.size === 0) return "(none found in bundled, user, project, or project-local scopes)";
130
162
  const lines: string[] = [];
131
163
  for (const agent of agents.values()) {
132
- const model = agent.model ? ` | Model: ${agent.model}` : "";
164
+ const extras = [
165
+ agent.model ? `Model: ${agent.model}` : "",
166
+ agent.verify ? `Verify: ${agent.verify}` : "",
167
+ agent.worktree ? "Worktree: true" : "",
168
+ agent.gitManaged ? "git-managed" : "local",
169
+ ].filter(Boolean);
133
170
  lines.push(`- **${agent.name}** (${agent.scope})`);
134
171
  if (agent.description) lines.push(` ${agent.description}`);
135
- lines.push(` Tools: ${agent.tools.join(", ") || "(role defaults)"}${model}`);
172
+ lines.push(` Tools: ${agent.tools.join(", ") || "(role defaults)"}${extras.length > 0 ? ` | ${extras.join(" | ")}` : ""}`);
136
173
  }
137
174
  return lines.join("\n");
138
175
  }