@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 +3 -1
- package/.memory/node-terminal-followups.md +3 -2
- package/.memory/team-centric-design.md +21 -0
- package/README.md +99 -63
- package/agents/observer.md +1 -1
- package/agents/reviewer.md +4 -2
- package/agents/specialist.md +1 -1
- package/agents/worker.md +3 -3
- package/package.json +3 -3
- package/src/activity.ts +86 -0
- package/src/agents.ts +58 -21
- package/src/follow-up-queue.ts +93 -26
- package/src/guidance.ts +81 -60
- package/src/index.ts +92 -28
- package/src/spawner.ts +194 -350
- package/src/state.ts +252 -565
- package/src/statefile.ts +209 -46
- package/src/team-machine.ts +946 -0
- package/src/tools.ts +177 -173
- package/src/types.ts +153 -189
- package/src/ui.ts +306 -230
- package/src/worker.ts +172 -34
- package/src/worktree.ts +20 -8
- package/.memory/run-centric-design.md +0 -22
- package/src/run-machine.ts +0 -606
- package/src/terminal.ts +0 -34
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,
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
21
|
+
## Agents Are Declarative Files
|
|
20
22
|
|
|
21
|
-
Agent definitions are Markdown files with frontmatter; the body is the
|
|
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
|
|
27
|
+
name: isolated-security-auditor
|
|
28
|
+
description: Read-only security reviewer in an isolated worktree
|
|
27
29
|
tools: read,bash
|
|
28
|
-
model: provider/model
|
|
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
|
|
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
|
-
##
|
|
48
|
+
## Build a Team
|
|
36
49
|
|
|
37
50
|
```text
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
- `
|
|
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
|
-
##
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
112
|
+
## Reliability Protocol
|
|
74
113
|
|
|
75
|
-
- **Per-spawn identity validation**: every
|
|
76
|
-
- **
|
|
77
|
-
- **One
|
|
78
|
-
- **
|
|
79
|
-
-
|
|
80
|
-
- **Failure semantics**:
|
|
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
|
|
121
|
+
## State and Sessions
|
|
83
122
|
|
|
84
|
-
|
|
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
|
|
91
|
-
├──
|
|
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
|
|
95
|
-
│ ├── tools.ts
|
|
96
|
-
│ ├──
|
|
97
|
-
│ ├──
|
|
98
|
-
│ ├──
|
|
99
|
-
│ ├──
|
|
100
|
-
│ ├── agents.ts
|
|
101
|
-
│ ├──
|
|
102
|
-
│ ├──
|
|
103
|
-
│ ├──
|
|
104
|
-
│
|
|
105
|
-
|
|
106
|
-
|
|
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
|
package/agents/observer.md
CHANGED
|
@@ -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
|
|
9
|
+
via send_message(to="leader", message=...) with status="completed". You have no edit,
|
|
10
10
|
write, or command tools.
|
package/agents/reviewer.md
CHANGED
|
@@ -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
|
|
10
|
-
status="completed".
|
|
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.
|
package/agents/specialist.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
7
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Multi-agent team system for Pi —
|
|
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.
|
|
42
|
+
"@fradser/pi-kit": "0.2.0"
|
|
43
43
|
}
|
|
44
44
|
}
|
package/src/activity.ts
ADDED
|
@@ -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
|
|
6
|
-
* 2. user
|
|
7
|
-
* 3. project
|
|
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
|
-
/**
|
|
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 (
|
|
30
|
-
scope:
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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)"}${
|
|
172
|
+
lines.push(` Tools: ${agent.tools.join(", ") || "(role defaults)"}${extras.length > 0 ? ` | ${extras.join(" | ")}` : ""}`);
|
|
136
173
|
}
|
|
137
174
|
return lines.join("\n");
|
|
138
175
|
}
|