@fradser/pi-agent-teams 0.6.0 → 0.8.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.
@@ -14,7 +14,7 @@ Exploratory / competitive-hypothesis work (multi-hypothesis debugging, three-way
14
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
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
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.
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 the reviewer's findings to the claimer. The board persists across restarts; runtime state does not.
18
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
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
20
 
package/README.md CHANGED
@@ -6,7 +6,7 @@ Claude-Code-style collaborative agent teams for Pi: named resident teammates, a
6
6
 
7
7
  ## What This Package Does
8
8
 
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.
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, judges verify prompts with fresh one-shot reviewers, and wakes idle teammates only when work or mail exists.
10
10
 
11
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.
12
12
 
@@ -27,21 +27,39 @@ Agent definitions are Markdown files with frontmatter; the body is the role prom
27
27
  name: isolated-security-auditor
28
28
  description: Read-only security reviewer in an isolated worktree
29
29
  tools: read,bash
30
- model: provider/model # optional
31
- verify: npm test # optional role-default completion gate
30
+ model: provider/model # optional; "inherit" = the leader's current model at spawn time
31
+ verify: review prompt # optional role-default gate; a fresh reviewer answers VERDICT: PASS/FAIL
32
32
  worktree: true # optional role-default Git isolation
33
33
  ---
34
34
  Review the assigned scope for exploitable security problems. Do not edit files.
35
35
  ```
36
36
 
37
- Discovery precedence per name (later overrides earlier):
37
+ Teammate model resolution at spawn time, in precedence order: an explicit
38
+ `provider/model` pin wins; `inherit` pins the leader session's current model;
39
+ without a pin, the unified teammate model set from `/agent-teams` (press `m`
40
+ in the roster page for a type-to-filter picker) applies to this session;
41
+ otherwise Pi picks its own default.
42
+
43
+ Discovery precedence per name (later overrides earlier): user < project <
44
+ project-local, with generated session roles filling only the names no file
45
+ defines. A re-spawn that supplies an explicit inline definition replaces a
46
+ previously generated session role of the same name; definition files are never
47
+ overwritten by inline input.
48
+
49
+ There are no built-in roles. When a needed role has no definition, the
50
+ leader creates it in memory for the current session: it derives the definition
51
+ from the task using the abstract role reference in `references/agent-roles.md`
52
+ (definition anatomy, archetype axes, invariants), registers it, and then spawns.
53
+ The role is not written to disk or reused in a later session unless the user
54
+ explicitly requests persistence; only then may the leader set `definition.persist`
55
+ and choose a project or project-local scope.
38
56
 
39
57
  | Scope | Location | Git semantics |
40
58
  |---|---|---|
41
- | bundled | package `agents/` | shipped with the package |
42
59
  | user | `~/.pi/agent/agents/*.md` | system-level, never committed |
43
60
  | project | `<cwd>/.pi/agents/<name>.md` | **git-managed** — commit for the team |
44
61
  | project-local | `<cwd>/.pi/agents/<name>.local.md` | **local** — personal override, gitignore by convention |
62
+ | session | in-memory registry | **ephemeral** — disappears at session start, no source file |
45
63
 
46
64
  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.
47
65
 
@@ -76,7 +94,7 @@ task_create({
76
94
  subject: "Fix auth middleware findings",
77
95
  description: "Address the confirmed security findings in packages/api/src/auth.ts",
78
96
  dependsOn: ["t_1"],
79
- verify: "npm test"
97
+ verify: "Every scenario in features/gallery.feature holds in the built gallery; no horizontal overflow at 400px."
80
98
  })
81
99
  ```
82
100
 
@@ -90,9 +108,9 @@ send_message({ to, message, status? })
90
108
  - Peer mail: `to: "<teammate-name>"`; `status` is invalid.
91
109
  - Leader steering or direct assignment: `to: "<teammate-name>"`; a working recipient gets a control-stream steer, an idle recipient wakes with the message.
92
110
  - 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.
111
+ - Peer traffic never enters the leader model context; inspect it in `/agent-teams` instead.
94
112
 
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.
113
+ Only the leader creates tasks. Idle teammates self-claim pending tasks whose dependencies are met. A task-level `verify` prompt overrides the claiming agent's frontmatter `verify`. The harness runs it as a fresh one-shot reviewer that inspects the work independently and answers `VERDICT: PASS` or `VERDICT: FAIL - <reasons>`; PASS completes the task, FAIL feeds the findings back to the claimer for fix-and-resubmit. Write acceptance criteria a reviewer can check, not shell commands.
96
114
 
97
115
  ## Tools
98
116
 
@@ -105,7 +123,7 @@ Only the leader creates tasks. Idle teammates self-claim pending tasks whose dep
105
123
  | `task_list()` | Both | One shared board-view definition; leader view also includes the roster |
106
124
  | `task_claim(taskId?)` | Worker | Atomically self-claim a pending, unblocked task |
107
125
  | `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 |
126
+ | `/agent-teams` | User | Management console: session teammates, persistent agent roles, board, details, shutdown |
109
127
 
110
128
  That is **7 unique tool names**. There are no `teammate_run`, `teammate_fanout`, `teammate_cancel`, `teammate_retry`, or `teammate_message` tools.
111
129
 
@@ -114,8 +132,10 @@ That is **7 unique tool names**. There are no `teammate_run`, `teammate_fanout`,
114
132
  - **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
133
  - **Sent means written**: peer `send_message` succeeds only after the recipient inbox write succeeds. The harness owns delivery into a recipient turn.
116
134
  - **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.
135
+ - **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. A gate that keeps failing parks the task with its holder after the second consecutive failure and escalates to the leader once instead of looping.
118
136
  - **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.
137
+ - **One-shot board notices**: an idle teammate is told about a claimable task exactly once; declined tasks never re-wake it, and released tasks re-arm. Notices are paced at least five minutes apart per teammate (`PI_TEAMMATE_NOTICE_PACE_MS` overrides in milliseconds).
138
+ - **One end-of-life line per teammate**: the first terminal report of a spawn incarnation renders the finish entry; shutting that incarnation down afterwards adds no second event row.
119
139
  - **Failure semantics**: an unexpected crash marks the teammate stopped, reports a diagnostic, and releases its claimed tasks.
120
140
 
121
141
  ## State and Sessions
@@ -127,17 +147,17 @@ Runtime state lives in `~/.pi/agent/teammate/<sessionKey>/` and dies with the se
127
147
  ```
128
148
  agent-teams/
129
149
  ├── index.ts — package-root extension entry point
130
- ├── agents/ — bundled declarative agent definitions
150
+ ├── references/ — role templates consulted when generating new agents
131
151
  ├── src/
132
152
  │ ├── index.ts — composition root and session lifecycle
133
- │ ├── tools.ts — leader tools and /teammate command
153
+ │ ├── tools.ts — leader tools and /agent-teams command
134
154
  │ ├── worker.ts — worker tools plus the shared task_list registration
135
155
  │ ├── team-machine.ts — resident lifecycle, mail routing, intents, verify, wake-ups
136
156
  │ ├── state.ts — roster, board, leader inbox state machine
137
157
  │ ├── spawner.ts — resident RPC process spawner (uncapped sequences)
138
158
  │ ├── agents.ts — agent discovery and frontmatter parsing
139
159
  │ ├── statefile.ts — runtime/board/mail IO and marker-file intents
140
- │ ├── ui.ts — passive widget and /teammate console
160
+ │ ├── ui.ts — passive widget and /agent-teams management console
141
161
  │ ├── guidance.ts — static leader and worker protocols
142
162
  │ └── follow-up-queue.ts — serialized automatic report delivery
143
163
  ├── features/ — BDD contract
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fradser/pi-agent-teams",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
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",
@@ -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.2.0"
42
+ "@fradser/pi-kit": "0.4.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
+ ```
package/src/agents.ts CHANGED
@@ -2,17 +2,21 @@
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/<name>.md
8
- * 4. project-local — <cwd>/.pi/agents/<name>.local.md (personal override)
5
+ * 1. user — ~/.pi/agent/agents/ (respects PI_CODING_AGENT_DIR)
6
+ * 2. project — <cwd>/.pi/agents/<name>.md
7
+ * 3. project-local — <cwd>/.pi/agents/<name>.local.md (personal override)
8
+ *
9
+ * There are no built-in roles: references/agent-roles.md ships template
10
+ * shapes that the leader consults when generating a new definition on demand.
9
11
  *
10
12
  * Frontmatter fields:
11
13
  * name — unique agent id (required)
12
14
  * description — routing contract: when the leader should choose this agent
13
15
  * tools — comma-separated list or YAML list of Pi tool ids
14
- * model — optional provider/model pin
15
- * verify — optional role-default completion gate command (zero exit passes)
16
+ * model — optional provider/model pin, or "inherit" for the leader's
17
+ * current model at spawn time
18
+ * verify — optional role-default completion gate: a review prompt a
19
+ * fresh one-shot reviewer answers with VERDICT: PASS/FAIL
16
20
  * worktree — optional role-default Git worktree isolation (true/false)
17
21
  * The Markdown body is the worker's role prompt.
18
22
  */
@@ -22,7 +26,7 @@ import * as path from "node:path";
22
26
  import { fileURLToPath } from "node:url";
23
27
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
24
28
 
25
- export type AgentScope = "bundled" | "user" | "project" | "project-local";
29
+ export type AgentScope = "user" | "project" | "project-local" | "session";
26
30
 
27
31
  export interface AgentDefinition {
28
32
  name: string;
@@ -40,11 +44,104 @@ export interface AgentDefinition {
40
44
  /** True only for the shared project scope that is expected to be git-managed.
41
45
  * user and project-local definitions are machine/personal by definition. */
42
46
  gitManaged: boolean;
43
- /** Absolute path of the definition file. */
44
- source: string;
47
+ /** Absolute path of the definition file; absent for session-only roles. */
48
+ source?: string;
49
+ }
50
+
51
+ export interface AgentDefinitionInput {
52
+ name: string;
53
+ description: string;
54
+ tools: string[];
55
+ model?: string;
56
+ verify?: string;
57
+ worktree?: boolean;
58
+ prompt: string;
59
+ }
60
+
61
+ /** Reserved model value meaning "use the leader session's current model",
62
+ * resolved at spawn time (Claude-Code-style inherit). */
63
+ export const MODEL_INHERIT_ALIAS = "inherit";
64
+
65
+ const sessionAgents = new Map<string, AgentDefinition>();
66
+ const AGENT_NAME_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/i;
67
+
68
+ function normalizeAgentInput(input: AgentDefinitionInput): AgentDefinitionInput {
69
+ const name = input.name.trim();
70
+ if (!AGENT_NAME_PATTERN.test(name)) {
71
+ throw new Error(`Invalid agent name "${name}". Use letters, digits, dots, dashes, underscores.`);
72
+ }
73
+ const prompt = input.prompt.trim();
74
+ if (!prompt) throw new Error(`Agent "${name}" prompt must not be empty.`);
75
+ return {
76
+ ...input,
77
+ name,
78
+ description: input.description.trim(),
79
+ tools: input.tools.map((tool) => tool.trim()).filter(Boolean),
80
+ model: input.model?.trim() || undefined,
81
+ verify: input.verify?.trim() || undefined,
82
+ worktree: input.worktree ?? false,
83
+ prompt,
84
+ };
85
+ }
86
+
87
+ /** Register a generated role for this session without writing a definition file. */
88
+ export function registerSessionAgent(input: AgentDefinitionInput): AgentDefinition {
89
+ const normalized = normalizeAgentInput(input);
90
+ const definition: AgentDefinition = {
91
+ ...normalized,
92
+ worktree: normalized.worktree ?? false,
93
+ scope: "session",
94
+ gitManaged: false,
95
+ source: undefined,
96
+ };
97
+ sessionAgents.set(definition.name, definition);
98
+ return definition;
99
+ }
100
+
101
+ /** Persist a generated role only when the user explicitly requests it. */
102
+ export function persistAgentDefinition(
103
+ input: AgentDefinitionInput,
104
+ scope: "project" | "project-local",
105
+ cwd = process.cwd(),
106
+ ): AgentDefinition {
107
+ const normalized = normalizeAgentInput(input);
108
+ const directory = agentsDir("project", cwd);
109
+ fs.mkdirSync(directory, { recursive: true });
110
+ const filename = `${normalized.name}${scope === "project-local" ? LOCAL_DEFINITION_SUFFIX : ".md"}`;
111
+ const source = path.join(directory, filename);
112
+ const frontmatter = [
113
+ "---",
114
+ `name: ${normalized.name}`,
115
+ `description: ${normalized.description.replace(/[\\r\\n]+/g, " ")}`,
116
+ `tools: ${normalized.tools.join(", ")}`,
117
+ ...(normalized.model ? [`model: ${normalized.model}`] : []),
118
+ ...(normalized.verify ? [`verify: ${normalized.verify.replace(/[\\r\\n]+/g, " ")}`] : []),
119
+ `worktree: ${normalized.worktree ? "true" : "false"}`,
120
+ "---",
121
+ normalized.prompt,
122
+ "",
123
+ ].join("\n");
124
+ fs.writeFileSync(source, frontmatter, { encoding: "utf8", mode: 0o600 });
125
+ sessionAgents.delete(normalized.name);
126
+ return {
127
+ ...normalized,
128
+ worktree: normalized.worktree ?? false,
129
+ scope,
130
+ gitManaged: scope === "project",
131
+ source,
132
+ };
133
+ }
134
+
135
+ /** Drop generated session roles; persistent user/project definitions remain discoverable. */
136
+ export function clearSessionAgents(): void {
137
+ sessionAgents.clear();
45
138
  }
46
139
 
47
- const BUNDLED_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "agents");
140
+ /** Shipped role templates: reference material for generating new definitions
141
+ * on demand — never discovered and never spawnable directly. */
142
+ export const AGENT_REFERENCE_PATH = path.resolve(
143
+ path.dirname(fileURLToPath(import.meta.url)), "..", "references", "agent-roles.md",
144
+ );
48
145
 
49
146
  /** Files named `xxx.local.md` in the project agents directory are personal
50
147
  * overrides: same teammate name as `xxx.md`, project-local scope, never git-
@@ -62,13 +159,40 @@ function parseFrontmatter(raw: string): { fields: Record<string, string | string
62
159
  const fields: Record<string, string | string[]> = {};
63
160
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
64
161
  if (!match) return { fields, body: raw.trim() };
65
- for (const line of match[1].split("\n")) {
162
+ const lines = match[1].split("\n");
163
+ for (let index = 0; index < lines.length; index++) {
164
+ const line = lines[index];
66
165
  const separator = line.indexOf(":");
67
166
  if (separator <= 0) continue;
68
167
  const key = line.slice(0, separator).trim();
69
168
  // Strip inline YAML comments (# ...) and surrounding whitespace/quotes.
70
169
  let value = line.slice(separator + 1).replace(/\s+#.*$/, "").trim();
71
170
  if (key === "tools") {
171
+ if (value === "") {
172
+ // YAML block sequence: "- item" lines, flush-left or indented,
173
+ // possibly interleaved with blank or comment-only lines.
174
+ const items: string[] = [];
175
+ index += 1;
176
+ while (index < lines.length) {
177
+ const trimmed = lines[index].trim();
178
+ if (trimmed === "" || trimmed.startsWith("#")) {
179
+ index += 1;
180
+ continue;
181
+ }
182
+ if (!/^-.+/.test(trimmed)) break;
183
+ const rawItem = trimmed.replace(/^-\s*/, "");
184
+ const item = rawItem
185
+ .replace(/\s+#.*$/, "")
186
+ .trim()
187
+ .replace(/^["']|["']$/g, "");
188
+ // "- # comment" is a comment-only entry, not a tool.
189
+ if (item && !rawItem.trimStart().startsWith("#")) items.push(item);
190
+ index += 1;
191
+ }
192
+ index -= 1; // The for-loop steps past the last consumed line.
193
+ fields.tools = items;
194
+ continue;
195
+ }
72
196
  const tools = value
73
197
  .replace(/^\[|\]$/g, "")
74
198
  .split(",")
@@ -133,13 +257,15 @@ function scopeIsGitManaged(scope: AgentScope): boolean {
133
257
 
134
258
  /**
135
259
  * Resolve all agent definitions visible from the given cwd, with later scopes
136
- * overriding earlier ones per name: project-local > project > user > bundled.
260
+ * overriding earlier ones per name: project-local > project > user.
137
261
  */
138
262
  export function discoverAgents(cwd?: string): Map<string, AgentDefinition> {
139
263
  const agents = new Map<string, AgentDefinition>();
140
- loadDir("bundled", BUNDLED_AGENTS_DIR, agents);
141
264
  loadDir("user", agentsDir("user", cwd), agents);
142
265
  loadDir("project", agentsDir("project", cwd), agents);
266
+ // Generated roles fill gaps only: filesystem scopes keep precedence over
267
+ // session memory.
268
+ for (const [name, definition] of sessionAgents) if (!agents.has(name)) agents.set(name, definition);
143
269
  return agents;
144
270
  }
145
271
 
@@ -158,14 +284,14 @@ export function hasAgent(name: string, cwd?: string): boolean {
158
284
  */
159
285
  export function formatAgentGuidance(cwd?: string): string {
160
286
  const agents = discoverAgents(cwd);
161
- if (agents.size === 0) return "(none found in bundled, user, project, or project-local scopes)";
287
+ if (agents.size === 0) return "(none defined yet — create one on demand from the shipped templates)";
162
288
  const lines: string[] = [];
163
289
  for (const agent of agents.values()) {
164
290
  const extras = [
165
291
  agent.model ? `Model: ${agent.model}` : "",
166
292
  agent.verify ? `Verify: ${agent.verify}` : "",
167
293
  agent.worktree ? "Worktree: true" : "",
168
- agent.gitManaged ? "git-managed" : "local",
294
+ agent.scope === "session" ? "in-memory" : agent.gitManaged ? "git-managed" : "local",
169
295
  ].filter(Boolean);
170
296
  lines.push(`- **${agent.name}** (${agent.scope})`);
171
297
  if (agent.description) lines.push(` ${agent.description}`);
@@ -3,6 +3,7 @@ export const TEAMMATE_REPORT_MESSAGE_TYPE = "agent-teams-report";
3
3
  export interface FollowUpReport {
4
4
  teammate?: string;
5
5
  agent?: string;
6
+ spawnId?: string;
6
7
  body: string;
7
8
  finished?: boolean;
8
9
  runId?: string;
package/src/guidance.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { formatAgentGuidance } from "./agents.ts";
1
+ import { AGENT_REFERENCE_PATH, formatAgentGuidance } from "./agents.ts";
2
2
 
3
3
  export const WORKER_GUIDANCE = `
4
4
  ## Resident Teammate Protocol
@@ -21,8 +21,9 @@ you consume nothing.
21
21
  - The shared task board is coordination state. Read it with task_list, claim
22
22
  pending tasks whose dependencies are met with task_claim (claims are
23
23
  atomic; losing a race means try another), and submit outcomes with
24
- task_submit. Completion may pass through a verify gate: if it fails, stderr
25
- feedback arrives in your inbox — fix and resubmit.
24
+ task_submit. Completion may pass through a verify gate: a fresh reviewer
25
+ checks the work independently, and on VERDICT: FAIL its findings arrive in
26
+ your inbox — fix and resubmit.
26
27
  - Coordinate file ownership with peers through send_message before writing.
27
28
 
28
29
  Do not use leader tools (spawning or shutting down teammates, creating
@@ -39,22 +40,40 @@ delegation, synthesis, and the final user-facing answer. Teammates are named
39
40
  resident child processes with isolated contexts; they do not see this
40
41
  conversation unless you put the needed context in their prompts.
41
42
 
42
- ### Agents are declarative files
43
-
44
- Agents live in Markdown files with frontmatter (name, description, tools,
45
- optional model, optional verify, optional worktree); the body is the role
46
- prompt. Discovery precedence per name: project-local
47
- \`<cwd>/.pi/agents/<name>.local.md\` > project
48
- \`<cwd>/.pi/agents/<name>.md\` > user \`~/.pi/agent/agents\` > bundled package
49
- agents. Same-name project/project-local pairs deduplicate into one
50
- definition, with project-local winning; project definitions are git-managed,
51
- while project-local and user definitions are personal/non-git-managed. Model
52
- and worktree behavior are role attributes: define a role variant when they
53
- need to differ.
43
+ ### Agents are declarative files, with ephemeral generated roles by default
44
+
45
+ Persistent agents live in Markdown files with frontmatter (name, description,
46
+ tools, optional model — a provider/model pin or "inherit" for the leader's
47
+ current model at spawn time — optional verify, optional worktree); the body
48
+ is the role prompt. There are no built-in roles. Generated roles are session-scoped
49
+ and held in memory by default: they have no filesystem source and disappear on
50
+ the next session. Do not write
51
+ an agent definition unless the user explicitly asks to keep the role for future
52
+ sessions. Discovery precedence per name:
53
+ project-local \`<cwd>/.pi/agents/<name>.local.md\` > project
54
+ \`<cwd>/.pi/agents/<name>.md\` > user \`~/.pi/agent/agents\`. Same-name
55
+ project/project-local pairs deduplicate into one definition, with
56
+ project-local winning; project definitions are git-managed, while
57
+ project-local and user definitions are personal/non-git-managed. Model and
58
+ worktree behavior are role attributes: define a role variant when they need
59
+ to differ.
54
60
 
55
61
  Available agents:
56
62
  ${agents}
57
63
 
64
+ Definitions are resolved live at spawn time: if a spawn says a listed role is
65
+ gone, its definition file changed mid-session — recreate it on demand instead
66
+ of retrying.
67
+
68
+ When an assignment or user request needs an agent whose name has no
69
+ definition yet, create it in memory first: derive it from the shipped abstract
70
+ role reference at \`${AGENT_REFERENCE_PATH}\` — its definition anatomy,
71
+ archetype axes, and invariants (read it before inventing a novel role) — tailor
72
+ it to the task, register the session role, and spawn immediately. Only after an
73
+ explicit request to keep the role for future sessions should you set
74
+ \`definition.persist=true\` (optionally choosing \`persistScope\`) so the spawn
75
+ writes \`<cwd>/.pi/agents/<name>.md\` or \`<name>.local.md\`.
76
+
58
77
  ### Build a team in one step per teammate
59
78
 
60
79
  Use teammate_spawn(name, agent, optional kickoff prompt) to start a resident
@@ -63,19 +82,26 @@ inbox messages and claimable board tasks, and can message peers directly.
63
82
  A session-wide cap of 8 living teammates applies. An agent with
64
83
  worktree: true receives its own git worktree; its diff is captured at shutdown.
65
84
 
85
+ Users may also ask for these conversationally ("add a reviewer teammate",
86
+ "create a scribe role") — apply the same create-on-demand step above when
87
+ the role does not exist yet. The human-facing management surface is the
88
+ /agent-teams command; never simulate or describe menu flows to the user.
89
+
66
90
  ### Coordinate through one messaging primitive and the board
67
91
 
68
92
  send_message is the only messaging tool. Address a teammate by name to steer
69
93
  it or hand work to it; working teammates receive it immediately and idle
70
94
  teammates wake automatically. The reserved recipient name "leader" is only
71
95
  for worker reports, not for leader calls. Peer traffic never reaches your
72
- context — inspect it in /teammate instead.
96
+ context — inspect it in /agent-teams instead.
73
97
 
74
98
  Create shared work with task_create(subject, description?, dependsOn?,
75
99
  verify?). Idle teammates notice claimable work automatically and self-claim;
76
- dependencies unlock downstream tasks without your involvement. An explicit
77
- verify command makes completion deterministic: zero exit completes, failure
78
- feeds stderr back to the claimer for fix-and-resubmit.
100
+ dependencies unlock downstream tasks without your involvement. The verify
101
+ prompt is judged by a fresh one-shot reviewer that inspects the work itself:
102
+ VERDICT: PASS completes, FAIL feeds the reviewer's findings back to the
103
+ claimer for fix-and-resubmit. Write gates as acceptance criteria a reviewer
104
+ can check (behavior, constraints, evidence), not as shell commands.
79
105
 
80
106
  ### Teammates are autonomous: recover, never punish
81
107
 
@@ -85,7 +111,7 @@ working teammate produces no output for a while; that notice is information,
85
111
  not a verdict — decide whether to keep waiting, steer again with send_message,
86
112
  or teammate_shutdown it. To carry wedged work forward, spawn a successor whose
87
113
  prompt composes context from the original kickoff, the teammate's past reports
88
- (leader mailbox or /teammate detail view), its board claims, and any live
114
+ (leader mailbox or /agent-teams detail view), its board claims, and any live
89
115
  transcript tail. The harness never reclaims, restarts, or replaces a teammate
90
116
  on its own.
91
117
 
package/src/index.ts CHANGED
@@ -8,13 +8,14 @@
8
8
  import { getMarkdownTheme, keyHint } from "@earendil-works/pi-coding-agent";
9
9
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
10
  import { buildTeamLeaderGuidance, WORKER_GUIDANCE } from "./guidance.ts";
11
- import { initTeamMachine, removeRuntimeDir, shutdownTeamMachine, teardownTeammates } from "./team-machine.ts";
11
+ import { clearSessionAgents } from "./agents.ts";
12
+ import { initTeamMachine, markTeammateFinished, removeRuntimeDir, shutdownTeamMachine, teardownTeammates } from "./team-machine.ts";
12
13
  import { cleanupExpiredStateDirs } from "./statefile.ts";
13
14
  import { resetState } from "./state.ts";
14
15
  import { ensureTeamWidget, refreshTeamUI, stopUiTimers } from "./ui.ts";
15
16
  import { registerLeaderTools, registerTeamCommand } from "./tools.ts";
16
17
  import { registerWorkerCapabilities, workerBinding } from "./worker.ts";
17
- import { formatAgentMessagePrefix } from "@fradser/pi-kit";
18
+ import { formatAgentMessagePrefix, formatExpandHint } from "@fradser/pi-kit";
18
19
  import { FollowUpQueue, groupReportsByTeammate, TEAMMATE_REPORT_MESSAGE_TYPE, type FollowUpReport } from "./follow-up-queue.ts";
19
20
  import { Box, Markdown, Text } from "@earendil-works/pi-tui";
20
21
 
@@ -61,7 +62,7 @@ export default function (pi: ExtensionAPI) {
61
62
  }
62
63
  if (!expanded) {
63
64
  const groups = groupReportsByTeammate(reports);
64
- const hint = theme.fg("dim", ` · ${keyHint("app.tools.expand", "to expand")}`);
65
+ const hint = formatExpandHint(keyHint("app.tools.expand", "to expand"), theme);
65
66
  for (const group of groups) {
66
67
  const prefix = theme.fg("customMessageLabel", theme.bold(formatAgentMessagePrefix("from", group.reports.length)));
67
68
  const name = theme.fg(reportColor(group.teammate), `@${group.teammate}`);
@@ -88,7 +89,7 @@ export default function (pi: ExtensionAPI) {
88
89
  if (event.message.role !== "custom" || event.message.customType !== TEAMMATE_REPORT_MESSAGE_TYPE) return;
89
90
  const reports = extractReports(event.message.details);
90
91
  for (const report of reports) {
91
- if (!report.finished) continue;
92
+ if (!markTeammateFinished(report)) continue;
92
93
  pi.appendEntry(TEAMMATE_FINISHED_ENTRY_TYPE, {
93
94
  teammate: report.teammate ?? report.agent,
94
95
  agent: report.agent,
@@ -97,6 +98,7 @@ export default function (pi: ExtensionAPI) {
97
98
  });
98
99
 
99
100
  pi.on("session_start", async (_event, ctx) => {
101
+ clearSessionAgents();
100
102
  resetState();
101
103
  followUpQueue?.reset();
102
104
  leaderCtx = ctx;