@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.
- package/.memory/team-centric-design.md +1 -1
- package/README.md +33 -13
- package/package.json +3 -3
- package/references/agent-roles.md +77 -0
- package/src/agents.ts +141 -15
- package/src/follow-up-queue.ts +1 -0
- package/src/guidance.ts +46 -20
- package/src/index.ts +6 -4
- package/src/picker-keys.ts +30 -0
- package/src/state.ts +42 -5
- package/src/team-machine.ts +406 -54
- package/src/tools.ts +79 -93
- package/src/types.ts +23 -4
- package/src/ui.ts +369 -69
- package/src/worker.ts +3 -1
- package/agents/observer.md +0 -10
- package/agents/reviewer.md +0 -13
- package/agents/specialist.md +0 -10
- package/agents/worker.md +0 -11
|
@@ -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
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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: "
|
|
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 `/
|
|
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`
|
|
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
|
-
| `/
|
|
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
|
-
├──
|
|
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 /
|
|
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 /
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3. project
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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 = "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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}`);
|
package/src/follow-up-queue.ts
CHANGED
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:
|
|
25
|
-
|
|
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
|
-
|
|
45
|
-
optional model
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
definition
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 /
|
|
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.
|
|
77
|
-
|
|
78
|
-
feeds
|
|
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 /
|
|
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 {
|
|
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 =
|
|
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
|
|
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;
|