@fradser/pi-agent-teams 0.5.3 → 0.7.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,134 @@ 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
+ There are no built-in roles. When a needed role has no definition, the
40
+ leader creates one on demand: it derives the definition from the task using
41
+ the abstract role reference in `references/agent-roles.md` (definition
42
+ anatomy, archetype axes, invariants), writes the file, and then spawns.
43
+
44
+ | Scope | Location | Git semantics |
45
+ |---|---|---|
46
+ | user | `~/.pi/agent/agents/*.md` | system-level, never committed |
47
+ | project | `<cwd>/.pi/agents/<name>.md` | **git-managed** — commit for the team |
48
+ | project-local | `<cwd>/.pi/agents/<name>.local.md` | **local** — personal override, gitignore by convention |
34
49
 
35
- ## Dispatch a run in one call
50
+ 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.
51
+
52
+ ## Build a Team
36
53
 
37
54
  ```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
55
+ teammate_spawn({
56
+ name: "security",
57
+ agent: "reviewer",
58
+ prompt: "Review the auth middleware"
47
59
  })
60
+
61
+ teammate_spawn({ name: "backend", agent: "worker" })
48
62
  ```
49
63
 
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.
64
+ - Spawn has exactly three parameters: `name`, `agent`, and optional kickoff `prompt`.
65
+ - Without a kickoff prompt, the teammate idles until messaged or until claimable work appears.
66
+ - Teammates stay alive between tasks and consume no model tokens while idle.
67
+ - A session-wide cap of 8 living teammates applies.
68
+ - An agent declaring `worktree: true` gets its own Git worktree; its diff is captured at shutdown.
69
+ - `teammate_shutdown({ name })` stops one teammate and releases its claimed task back to the board.
59
70
 
60
- ## Tools
71
+ ## Coordinate Through One Message Primitive and the Board
61
72
 
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 |
73
+ ```text
74
+ send_message({
75
+ to: "security",
76
+ message: "Please challenge the backend finding against the current middleware."
77
+ })
78
+
79
+ task_create({
80
+ subject: "Fix auth middleware findings",
81
+ description: "Address the confirmed security findings in packages/api/src/auth.ts",
82
+ dependsOn: ["t_1"],
83
+ verify: "npm test"
84
+ })
85
+ ```
86
+
87
+ `send_message` has one schema everywhere:
88
+
89
+ ```text
90
+ send_message({ to, message, status? })
91
+ ```
92
+
93
+ - Worker report: `to: "leader"`; use optional `status: "completed" | "failed"` for a terminal report.
94
+ - Peer mail: `to: "<teammate-name>"`; `status` is invalid.
95
+ - Leader steering or direct assignment: `to: "<teammate-name>"`; a working recipient gets a control-stream steer, an idle recipient wakes with the message.
96
+ - The first non-empty line of `message` becomes the console title.
97
+ - Peer traffic never enters the leader model context; inspect it in `/agent-teams` instead.
98
+
99
+ 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.
100
+
101
+ ## Tools
68
102
 
69
- Spawned workers receive one additional capability, `teammate_message`, for progress notes and final reports to the team leader.
103
+ | Tool / command | Side | Description |
104
+ |---|---|---|
105
+ | `teammate_spawn(name, agent, prompt?)` | Leader | Start one named resident teammate; model/worktree come from agent frontmatter |
106
+ | `teammate_shutdown(name)` | Leader | Stop one teammate and release its claimed work |
107
+ | `send_message(to, message, status?)` | Both | The only messaging primitive; `to: "leader"` is reserved for worker reports |
108
+ | `task_create(subject, description?, dependsOn?, verify?)` | Leader | Add a shared board task |
109
+ | `task_list()` | Both | One shared board-view definition; leader view also includes the roster |
110
+ | `task_claim(taskId?)` | Worker | Atomically self-claim a pending, unblocked task |
111
+ | `task_submit(taskId, status, result?)` | Worker | Submit a claimed task outcome; completion passes through verify |
112
+ | `/agent-teams` | User | Management console: session teammates, persistent agent roles, board, details, shutdown |
70
113
 
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.
114
+ That is **7 unique tool names**. There are no `teammate_run`, `teammate_fanout`, `teammate_cancel`, `teammate_retry`, or `teammate_message` tools.
72
115
 
73
- ## Reliability protocol
116
+ ## Reliability Protocol
74
117
 
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.
118
+ - **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.
119
+ - **Sent means written**: peer `send_message` succeeds only after the recipient inbox write succeeds. The harness owns delivery into a recipient turn.
120
+ - **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.
121
+ - **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.
122
+ - **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.
123
+ - **Failure semantics**: an unexpected crash marks the teammate stopped, reports a diagnostic, and releases its claimed tasks.
81
124
 
82
- ## State and sessions
125
+ ## State and Sessions
83
126
 
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.
127
+ 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
128
 
86
129
  ## Structure
87
130
 
88
131
  ```
89
132
  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)
133
+ ├── index.ts — package-root extension entry point
134
+ ├── references/ — role templates consulted when generating new agents
93
135
  ├── 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
136
+ │ ├── index.ts — composition root and session lifecycle
137
+ │ ├── tools.ts — leader tools and /agent-teams command
138
+ │ ├── worker.ts — worker tools plus the shared task_list registration
139
+ │ ├── team-machine.ts — resident lifecycle, mail routing, intents, verify, wake-ups
140
+ │ ├── state.ts — roster, board, leader inbox state machine
141
+ │ ├── spawner.ts — resident RPC process spawner (uncapped sequences)
142
+ │ ├── agents.ts — agent discovery and frontmatter parsing
143
+ │ ├── statefile.ts — runtime/board/mail IO and marker-file intents
144
+ │ ├── ui.ts — passive widget and /agent-teams management console
145
+ │ ├── guidance.ts — static leader and worker protocols
146
+ │ └── follow-up-queue.ts — serialized automatic report delivery
147
+ ├── features/ — BDD contract
148
+ └── tests/ — package tests
109
149
  ```
110
150
 
111
151
  ## License
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.7.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",
@@ -24,7 +24,7 @@
24
24
  "files": [
25
25
  "index.ts",
26
26
  "src",
27
- "agents",
27
+ "references",
28
28
  ".memory",
29
29
  "README.md"
30
30
  ],
@@ -39,6 +39,6 @@
39
39
  ]
40
40
  },
41
41
  "dependencies": {
42
- "@fradser/pi-kit": "0.1.1"
42
+ "@fradser/pi-kit": "0.3.0"
43
43
  }
44
44
  }
@@ -0,0 +1,77 @@
1
+ # Agent Role Reference
2
+
3
+ Abstract reference for generating new agent definitions on demand. This file
4
+ is NOT discoverable and CANNOT be spawned directly. Derive a concrete
5
+ definition from the anatomy, archetype axes, and invariants below, tailor it
6
+ to the task, and write it to `<cwd>/.pi/agents/<name>.md` (git-managed
7
+ project role) or `<name>.local.md` (personal override).
8
+
9
+ ## Definition anatomy
10
+
11
+ Frontmatter:
12
+
13
+ - `name` — unique id among discovered roles.
14
+ - `description` — the routing contract: when the leader should choose this
15
+ role, phrased as "use when ...". This is the most load-bearing field.
16
+ - `tools` — the minimal set; follows the mutability axis (see below).
17
+ - optional `model`, `verify`, `worktree`.
18
+
19
+ The Markdown body is the role prompt, built from five parts:
20
+
21
+ 1. Identity — "You are a `<role>` agent."
22
+ 2. Method — the procedure to execute, the criteria to judge against, or the
23
+ observation targets, depending on the judgment axis.
24
+ 3. Boundaries — what must never be touched or changed.
25
+ 4. Evidence — what every claim or finding must carry.
26
+ 5. Terminal report — delivery via `send_message(to="leader", message=...)`;
27
+ the last message to the leader MUST
28
+ carry status="completed" (or status="failed"); work without a terminal
29
+ status counts as unfinished work.
30
+
31
+ ## Archetype axes
32
+
33
+ Pick a position on two axes before writing anything:
34
+
35
+ | Axis | Values |
36
+ |---|---|
37
+ | Mutability | mutating (`edit`,`write` allowed) · read-only |
38
+ | Judgment | execute a given procedure · evaluate against criteria · apply domain expertise · observe and report |
39
+
40
+ Read-only plus each judgment value yields the four classical shapes:
41
+
42
+ | Shape | Typical tools | Body emphasis |
43
+ |---|---|---|
44
+ | executor | `read,bash,edit,write` | implement end to end, then self-verify by running tests or commands; report changes made, verification performed, remaining risks |
45
+ | reviewer | `read,bash` | only confirmed findings, with severity, evidence, exact paths, and a minimal fix recommendation; when nothing is found, say so and list the checks performed |
46
+ | specialist | `read,bash` | deep domain analysis; state assumptions, the evidence used, and the bounds of expertise for this task |
47
+ | observer | `read` | snapshots or readings; summarize observed state and anomalies with timestamps and evidence; change nothing |
48
+
49
+ These shapes are starting points, not a fixed menu: a generated role may sit
50
+ anywhere on the axes (for example a mutating specialist with a verify gate),
51
+ as long as every invariant below holds.
52
+
53
+ ## Invariants
54
+
55
+ Non-negotiable in every generated definition:
56
+
57
+ - Tool sets match the mutability axis: read-only roles never receive
58
+ `edit` or `write`.
59
+ - Every finding or claim carries evidence: exact paths, command output,
60
+ timestamps — never bare assertions.
61
+ - Scope is bounded: mutating roles state "do not touch files outside the
62
+ assigned scope"; read-only roles state the equivalent prohibition.
63
+ - Terminal-status discipline applies to all shapes alike.
64
+
65
+ ## Skeleton
66
+
67
+ ```markdown
68
+ ---
69
+ name: <role-name>
70
+ description: <one-line capability>; use when <routing condition>
71
+ tools: <minimal set from the mutability axis>
72
+ ---
73
+ You are a <role-name> agent. <Method: the procedure to follow, the criteria
74
+ to evaluate against, or the targets to observe>. <Boundary statement>.
75
+ Report <expected artifacts> with <evidence requirements> via
76
+ send_message(to="leader", message=...) with status="completed".
77
+ ```
@@ -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
+ }