@fradser/pi-agent-teams 0.10.0 → 1.0.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/README.md CHANGED
@@ -1,89 +1,112 @@
1
1
  # @fradser/pi-agent-teams
2
2
 
3
- Agent Teams provides one final coordination surface for Pi:
3
+ Agent Teams combines subagent execution, the task set, and point-to-point messages.
4
+ The four intent tools are the only child tool surface:
4
5
 
5
- ```text
6
- agent
7
- work
8
- agent_event
9
- /agent-teams
6
+ ```ts
7
+ agent_start({ identity: "reviewer", prompt: "Audit authentication" });
8
+ agent_input({ session: "session:reviewer:<incarnation>", prompt: "Review the correction", delivery: "next" });
9
+ agent_input({ session: "session:reviewer:<incarnation>", prompt: "Keep this read-only", delivery: "steer" });
10
+ agent_list({});
11
+ agent_stop({ session: "session:reviewer:<incarnation>" });
10
12
  ```
11
13
 
12
- `agent` manages Agent sessions through strict actions:
14
+ Next input has a bounded FIFO queue and rechecks the original host's task policy.
15
+ A label changes display only; an identity selects saved instructions and private data.
16
+ Read [the intent contract](../subagents/README.md#intent-tools) for results, cancellation, and compatibility.
17
+ The `/agent-teams` console remains available.
13
18
 
14
- ```ts
15
- agent({ action: "delegate", name: "reviewer", prompt: "Audit authentication" })
16
- agent({ action: "start", name: "reviewer" })
17
- agent({ action: "inspect", name: "reviewer", session: "session:reviewer-...:spawn-..." })
18
- agent({ action: "stop", session: "session:reviewer-...:spawn-..." })
19
- ```
19
+ ## The retired `agent` tool
20
20
 
21
- A delegate action can include an inline definition. It is session-local unless
22
- `persist: true` is explicitly supplied in that definition. Choose explicit minimal
23
- canonical tool IDs (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`,
24
- `powershell`); extension aliases such as `ffgrep` are not inherited. Omitted
25
- `tools` or `tools: []` grants only `agent_event` and `work`, never default shell
26
- or file access. Delegate/start results and inspection expose the recorded
27
- `session.tools` grant, with a coordination-only warning when applicable.
28
- Inspection uses the session's recorded grant, not a changed role definition;
29
- legacy records without a grant leave it unknown rather than claiming no tools.
21
+ The `agent` tool and its six actions are gone, and no compatibility adapter
22
+ remains. A recorded branch that named `agent` receives the intent family on its
23
+ next session start. No user data moves. Read
24
+ [ADR-0008](../../docs/adr/0008-retire-the-legacy-agent-tool.md) for the decision.
30
25
 
31
- For an unknown role, provide a compact inline definition:
26
+ An unknown role takes a compact inline definition:
32
27
 
33
28
  ```ts
34
- agent({ action: "delegate", name: "reader", prompt: "Read README.md and cite its install command",
35
- definition: { description: "Read evidence", prompt: "Read the assigned file and cite evidence", tools: ["read"] } })
29
+ agent_start({ label: "reader", prompt: "Read README.md and cite its install command",
30
+ description: "Read evidence", role_prompt: "Read the assigned file and cite evidence", tools: ["read"] })
36
31
  ```
37
32
 
38
- `agent start` creates an unassigned resident without a synthetic Work ID or an
39
- initial model turn. Presence stays `starting` until a one-shot native readiness
40
- acknowledgement, then becomes `idle` and eligible for exact-session assignment.
41
- The public start call awaits that bounded acknowledgement before returning, so
42
- its exact returned session accepts an immediate assignment without a caller wait.
43
- Rejected readiness, timeout, or early exit fails the start call; a process ID is
44
- not readiness. Reassigning the same still-claimed Work to its exact current owner
45
- returns the existing Assignment Attempt without a second delivery. Later claimable-board notices can still wake
46
- the resident and activate deliberate autonomous claims. Start is not a permanent
47
- assignment-only scheduling mode.
48
-
49
- `work` is activated only in valid assignment/board contexts.
50
-
51
- `work` owns Work lifecycle. Leaders create/list/assign/release/reopen completed
52
- Work/supersede; Workers list/claim/submit/release their current Work.
53
-
54
- Failed, interrupted, or crash-released Work returns to the board as
33
+ Only a user-authored disk `agent.md` declares durable identity. Disk definitions
34
+ always outrank inline input; invocation tools may narrow their grant, never widen it.
35
+ There are no persistence, Memory opt-in, worktree or promotion parameters. Choose minimal
36
+ canonical tool IDs (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`,
37
+ `powershell`); extension aliases such as `ffgrep` are not inherited. Omitted
38
+ `tools` or `tools: []` grants only `message` and `task` for temporary team children,
39
+ never default shell or file access. Disk identities also receive `agent_memory`.
40
+ Start receipts and inspection expose the recorded effective grant, with a
41
+ coordination-only warning when applicable.
42
+ Inspection uses the session's recorded grant, not a changed role definition;
43
+ a record without a grant leaves it unknown rather than claiming no tools.
44
+
45
+ `agent_start` always takes a prompt and awaits the correlated
46
+ native input acknowledgement, not merely a writable pipe. It never creates an idle
47
+ child. Rejected, handled, timed-out or closed input never reports acceptance.
48
+ A receipt is not a result.
49
+
50
+ Every temporary or durable child remains **IDLE after a settled result** and accepts
51
+ next input in that exact incarnation. Steer guides active work; `agent_stop` and
52
+ parent shutdown close it. Later tasks notices can wake an idle team participant.
53
+ No child survives the parent Pi process. Hosted next input awaits native input acceptance
54
+ in the same process and history, publishing a new attempt before a later independent
55
+ input. Hosted steer awaits native steering acceptance without changing the
56
+ current sequence or attempt. An uncertain acknowledgement retains authority: inspect
57
+ or stop before retrying, rather than submitting competing input.
58
+
59
+ Canonical definitions are `<agent-dir>/agents/<name>/agent.md`,
60
+ `<project>/.pi/agents/<name>/agent.md`, and
61
+ `<project>/.pi/agents/<name>.local/agent.md`; precedence is project-local > project >
62
+ user > session. Every disk scope receives private Memory, an ordinary scratch directory
63
+ and project history at `<agent-dir>/agents/<name>/projects/<slug>/session/`.
64
+ Project overrides do not create a user-scope definition. Temporary roles use the
65
+ original project cwd with no-session and no private data. Durable children also launch
66
+ in the original project; scratch is data, not a checkout:
67
+ project edits change the original working tree directly. There is no Git isolation,
68
+ branch, automatic commit, merge or write confinement.
69
+
70
+ Identity, data provisioning and offline list entries belong to
71
+ [@fradser/pi-subagents](../subagents/VISION.md); team coordination does not create,
72
+ promote or clear that package's role registry.
73
+
74
+ `task` is activated only in valid assignment/tasks contexts and owns create/list/
75
+ update/complete/reopen. A Task describes what must be done, not who must do it;
76
+ participants take available Tasks with `update status=in_progress`.
77
+
78
+ Failed, interrupted, or crash-released Work returns to the tasks as
55
79
  `pending/recovery-required`: the evidence stays available for inspection, and the
56
- harness deliberately excludes it from autonomous claim notices. Only an explicit
57
- leader `work assign` opens the next attempt, so a transient failure is never
58
- silently retried by whichever resident happens to be idle. A retained superseded
59
- holder keeps `work` disclosed for exactly one failed cancellation acknowledgement.
80
+ harness deliberately excludes it from autonomous notices. A participant must
81
+ proceed with a stated recovery reason, so a transient failure is never silently
82
+ retried. A retained superseded holder keeps `task` disclosed for exactly one failed
83
+ cancellation acknowledgement.
60
84
 
61
85
  ```ts
62
- work({ action: "create", subject: "Fix storage", resources: ["firmware/storage"] })
63
- work({ action: "assign", id: "fix-storage", target: { session: "session:worker-...:spawn-..." } })
64
- work({ action: "claim" })
65
- work({ action: "submit", outcome: "success", result: "Verification evidence" })
86
+ task({ action: "create", subject: "Fix storage", resources: ["firmware/storage"] })
87
+ task({ action: "update", id: "fix-storage", status: "in_progress" })
88
+ task({ action: "complete", id: "fix-storage", outcome: "success", result: "Verification evidence" })
66
89
  ```
67
90
 
68
- `agent_event` is communication-only. It accepts a message, a recipient or bound
69
- reply route, and optional `inform`/`request` intent. Work lifecycle authority is
91
+ `message` is communication-only. It accepts a body, a recipient or bound
92
+ reply route, and optional `inform`/`request` kind. Work lifecycle authority is
70
93
  never inferred from message text.
71
94
 
72
95
  ```ts
73
- agent_event({ to: "session:reviewer-...:spawn-...", message: "The policy changed", intent: "inform" })
96
+ message({ to: "session:reviewer:spawn-...", body: "The policy changed", kind: "inform" })
74
97
  ```
75
98
 
76
99
  Automatic final answers (including autonomously claimed Work) and explicit Work
77
100
  submission use the same attempt-bound acceptance pipeline. Explicit submission
78
101
  ends the worker turn without a second automatic submission. Successful results
79
102
  reach the Leader once, after execution settles and any verification gate passes.
80
- Execution failures are reported after settlement without claiming acceptance.
81
- `work list` includes bounded result evidence for deliberate recovery.
103
+ Execution failures are reported after settlement without claiming acceptance. A refused task transition does not produce an accepted completion or failure report.
104
+ `task list` includes bounded result evidence for deliberate recovery.
82
105
  Verification freezes execution and retains deferred mail as Work history.
83
106
  An ordinary final answer is a successful candidate, not independent verification;
84
107
  ungated acceptance does not independently verify its claims. If requested work
85
108
  cannot be performed, including missing tools, workers must explicitly submit
86
- `work({ action: "submit", outcome: "failed", result: "Blocker and unverified work" })`.
109
+ `task({ action: "complete", outcome: "failed", result: "Blocker and unverified work" })`.
87
110
  Blocker prose is not classified as failure. Failed submission produces one failed
88
111
  report, not completion or dependent unlock; a rejecting verification gate also
89
112
  prevents acceptance. Leaders should delegate concrete acceptance criteria and an
@@ -92,30 +115,43 @@ acknowledgment unless a decision or action changes.
92
115
  Do not narrate repeated reports. This guidance does not suppress event transport
93
116
  or infer lifecycle changes from communication.
94
117
 
118
+ A clean process exit while the resident is idle and owes no result stays in the console mailbox.
119
+ It records the stopped state and confirmed stop time without an unexpected-stop notification or another leader turn.
120
+ Unread mail addressed to the current incarnation prevents passive closure, even before inbox routing.
121
+ Malformed or incomplete unread records also prevent passive closure. Valid JSON alone does not prove that a message is valid.
122
+ The close check changes no inbox bytes, offsets, or delivery records. Stale or already-delivered messages do not create a new alarm.
123
+ Process failures, interrupted assignments, and unresolved results still reach the leader. Accepted results remain available after closure.
124
+
95
125
  After delegation, continue independent work or end the turn: results resume the
96
- session automatically. Use `agent inspect` for deliberate diagnosis, not repeated
97
- polling or sleep loops. `agent_event` never creates or retries Work; messaging a
126
+ session automatically. `agent_list` is a status summary, not a way to wait, and a
127
+ child's complete answer arrives with its Session Result rather than on request.
128
+ polling or sleep loops. `message` never creates or retries Work; messaging a
98
129
  completed assignment returns its recorded result without waking the worker.
99
130
  Worker communication and submissions are bound to the Assignment Attempt that
100
131
  started the turn, so an old turn cannot report or submit as replacement Work, and
101
132
  nonterminal coordination from a retired attempt stays in session history as
102
133
  evidence without acting as a current instruction.
103
- Recovery reuses the existing Work ID. Request explicit `work release` and await
104
- authoritative release before `work assign` to the chosen exact session; a queued
105
- release is not yet released authority. Do not create competing recovery Work via
106
- `agent delegate`. Read-only/no-write messages are steering, not cancellation or
134
+ Recovery reuses the existing Task ID. A message does not release authority: wait
135
+ for the attempt to end or stop its stale exact session before taking the Task with
136
+ a stated recovery reason. Do not create competing recovery work through another startup. Read-only/no-write messages are steering, not cancellation or
107
137
  proof that the current tool batch stopped. Native steering takes effect at safe
108
138
  tool boundaries, before peer follow-up; it cannot undo already executed writes.
109
- Parked verification requires explicit Work release and reassignment; ordinary
139
+ Parked verification requires explicit Task recovery; ordinary
110
140
  messages remain deferred. Exact session handles are incarnation-bound, so a stale
111
141
  handle cannot stop or inspect a replacement resident. Work persists for later inspection,
112
142
  but a different session file does not import another session's tasks automatically.
113
143
 
114
144
  ## Coordination and delivery
115
145
 
146
+ Leader and worker guidance use the owned `agent_teams_leader` and `agent_teams_worker` prompt sections at run start.
147
+ These sections remain present with either Pi's default preamble or an authored `customPrompt`.
148
+ They do not replace the complete system prompt or add context through custom messages.
149
+ Custom-message starts have no user prompt; worker disclosure checks the current assignment without assuming a task notice.
150
+ Historical report filtering and report delivery remain separate from these prompt contributions.
151
+
116
152
  Leader guidance uses the same delivery contract before and after a team starts:
117
153
 
118
- - Delegate separable work with a task-scoped baseline, acceptance criteria and safe local checks. Use `work create` with `dependsOn` for prerequisites and shared `resources` tags for conflicting writes; then assign eligible Work. Independent delegation is not a substitute for dependencies.
154
+ - Delegate separable work with a task-scoped baseline, acceptance criteria and safe local checks. Use `task create` with `dependsOn` for prerequisites and shared `resources` tags for conflicting writes; participants take eligible Tasks. Independent delegation is not a substitute for dependencies.
119
155
  - Implementers own assigned local checks. One integration owner checks shared behavior against the integrated candidate. Determine isolation needs before verification; commands running locally may still access user data or production credentials.
120
156
  - Record a revision or scoped snapshot for the candidate and exclude unrelated dirty changes. Review and integration checks judge that same candidate; later edits invalidate affected evidence rather than automatically requiring every Agent to rerun every suite.
121
157
  - A completed review Work Item can report REWORK. It is not a PASS on the implementation. Review-only work ends with that report, without editing the implementation or waiting for repairs; the implementation owner owns remediation. Implementation delivery stays pending until required Work and blocking reviews have returned, findings are resolved, and applicable evidence matches the final candidate. If only results remain outstanding, yield without announcing completion.
@@ -125,6 +161,38 @@ Leader guidance uses the same delivery contract before and after a team starts:
125
161
 
126
162
  Read [the role reference](references/agent-roles.md#assignment-brief) when preparing an assignment brief.
127
163
 
164
+ ## Child environment
165
+
166
+ A spawned Agent receives an explicit non-secret environment, never the leader's
167
+ complete one. The allowlist covers runtime and locale, editor and pager, Git and
168
+ `SSH_AUTH_SOCK`, proxy and trust anchors, Node and language toolchain locations,
169
+ and the Pi process markers and configuration. `PI_CODING_AGENT_DIR` passes
170
+ through so the child resolves the same `auth.json` and `models.json` as the
171
+ leader; that is where Pi keeps OAuth credentials and literal `apiKey` values, so
172
+ a child needs the config directory rather than the parent's credential set.
173
+
174
+ Add an exact name to `PI_TEAMMATE_ENV_ALLOW` (comma-separated) when a provider
175
+ resolves credentials through environment interpolation:
176
+
177
+ ```bash
178
+ export PI_TEAMMATE_ENV_ALLOW="MY_PROVIDER_API_KEY,AWS_PROFILE"
179
+ ```
180
+
181
+ Opting in a credential-shaped name works and is reported, so a widening stays
182
+ visible. The allowlist variable itself does not travel to the child, so a nested
183
+ spawn reaches its own strict default. Spawn bindings the harness supplies always
184
+ win over both the allowlist and the leader environment.
185
+
186
+ The withheld count, the credential-shaped names, and the recovery instruction
187
+ appear in the `/agent-teams` detail view. That stays passive console telemetry
188
+ and never wakes the leader. Names embedding a private URL — npm's
189
+ `npm_config_//registry.example.com/:_authToken` shape — are redacted to a
190
+ recognizable prefix, and no withheld value is ever rendered.
191
+
192
+ This is leakage defense, not containment. A child granted `bash` can still read
193
+ `~/.pi/agent/auth.json`, `~/.aws/credentials`, or a project `.env` from disk;
194
+ kernel write and read confinement is a separate layer that does not exist yet.
195
+
128
196
  ## Message display
129
197
 
130
198
  Collapsed `[message]` rows use the current terminal width for their preview and
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fradser/pi-agent-teams",
3
- "version": "0.10.0",
4
- "description": "Multi-agent team system for Pi — resident teammates, shared task board, and peer-to-peer messaging",
3
+ "version": "1.0.0",
4
+ "description": "Agent-to-Agent communication for Pi — point-to-point messaging, notices, Agent Presence, and the Team Console. A bundle: it registers `message` and loads the pi-subagents and pi-tasks extensions, so installing it gives the subagent intent tools plus `task` and `message` with one registrant per tool.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "teammates",
@@ -35,10 +35,18 @@
35
35
  },
36
36
  "pi": {
37
37
  "extensions": [
38
- "./index.ts"
38
+ "./index.ts",
39
+ "./node_modules/@fradser/pi-subagents/index.ts",
40
+ "./node_modules/@fradser/pi-tasks/index.ts"
39
41
  ]
40
42
  },
41
43
  "dependencies": {
42
- "@fradser/pi-kit": "0.5.1"
44
+ "@fradser/pi-kit": "0.6.0",
45
+ "@fradser/pi-subagents": "0.2.0",
46
+ "@fradser/pi-tasks": "0.2.0"
47
+ },
48
+ "devDependencies": {
49
+ "@earendil-works/pi-coding-agent": "1.5.0",
50
+ "@earendil-works/pi-tui": "1.5.0"
43
51
  }
44
- }
52
+ }
@@ -3,8 +3,11 @@
3
3
  Abstract reference for generating new agent definitions on demand. This file
4
4
  is NOT discoverable and CANNOT be spawned directly. Derive a concrete
5
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).
6
+ to the task, and pass it as an inline session role. Only a user-authored disk
7
+ `agent.md` declares durable identity: `<agent-dir>/agents/<name>/agent.md`,
8
+ `<cwd>/.pi/agents/<name>/agent.md`, or `<cwd>/.pi/agents/<name>.local/agent.md`.
9
+ Project-local overrides project, which overrides user, which overrides session.
10
+ Creating a disk definition requires explicit user authorization, not a tool flag.
8
11
 
9
12
  ## Definition anatomy
10
13
 
@@ -15,12 +18,19 @@ Frontmatter:
15
18
  role, phrased as "use when ...". This is the most load-bearing field.
16
19
  - `tools` — the explicit minimal set; follows the mutability axis (see below).
17
20
  Canonical built-ins are `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`,
18
- `powershell` (kept in sync with `src/worker-tools.ts`).
19
- Omitted tools or `[]` grant only coordination tools (`agent_event`, `work`),
21
+ `powershell` (declared by `@fradser/pi-subagents`).
22
+ Omitted tools or `[]` grant only coordination tools (`message`, `task`),
20
23
  not file or shell access. No aliases or implicit default `bash`.
21
- - optional `model`, `verify`, `worktree`. `verify` is an independent completion-gate
22
- reviewer prompt, not a shell command. State the evidence and acceptance criteria
23
- the gate should inspect; a concrete local gate is not a second broad project review.
24
+ - optional `model`, `verify`. `verify` is an independent completion-gate
25
+ reviewer prompt, not a shell command. State evidence and acceptance criteria; a concrete
26
+ local gate is not a second broad project review.
27
+
28
+ Every disk scope has private Memory, an ordinary scratch workspace and project-scoped
29
+ Pi history under the configured agent directory. Inline session roles have none and
30
+ run in the original project cwd with no-session. Scratch is not a project checkout:
31
+ project edits change the original working tree directly, with no write confinement,
32
+ Git isolation or automatic commit. Never invent persistence, Memory or workspace flags.
33
+ Results leave both kinds IDLE for `send`; only `stop` or parent shutdown closes them.
24
34
 
25
35
  The Markdown body is the role prompt, built from five parts:
26
36
 
@@ -30,8 +40,8 @@ The Markdown body is the role prompt, built from five parts:
30
40
  3. Boundaries — what must never be touched or changed.
31
41
  4. Evidence — what every claim or finding must carry.
32
42
  5. Final result — an ordinary final answer is submitted automatically for the
33
- current Work Item after execution settles. A Worker may explicitly use
34
- `work({ action: "submit", outcome, result? })`; communication events never
43
+ current Task after execution settles. A Worker may explicitly use
44
+ `task({ action: "complete", outcome, result? })`; communication events never
35
45
  carry completion authority.
36
46
 
37
47
  ## Assignment brief
@@ -41,8 +51,8 @@ Keep task-specific coordination in the Work description, not a permanently growi
41
51
  - **Scope and baseline** — owned files or resources, originating requirements, and
42
52
  the exact task delta, including untracked files and pre-existing dirty changes to exclude.
43
53
  - **Dependencies** — required predecessor Work and any shared resource ownership.
44
- Use `work create` with `dependsOn` for dependent work, then assign when eligible;
45
- `agent delegate` is for independent work.
54
+ Use `task create` with `dependsOn` for dependent work; participants take eligible
55
+ Tasks. `agent start` with a prompt hands over independent execution.
46
56
  - **Verification owner** — assigned safe local checks for this Worker; one owner
47
57
  integrates and checks shared behavior. Determine isolation needs before commands run.
48
58
  - **Candidate and result** — identify the revision or scoped snapshot checked, commands
@@ -98,7 +108,7 @@ Non-negotiable in every generated definition:
98
108
  - Final results carry evidence; an ordinary final answer is a successful candidate,
99
109
  not an independently verified result. Ungated completion is not independently verified.
100
110
  - If requested work cannot be performed, including missing capabilities, explicitly use
101
- `work({ action: "submit", outcome: "failed", result: "Blocker and unverified work" })`.
111
+ `task({ action: "complete", outcome: "failed", result: "Blocker and unverified work" })`.
102
112
  Do not return blocker prose as an ordinary final answer; prose is not classified as failure.
103
113
 
104
114
  ## Skeleton
@@ -122,7 +132,7 @@ are rejected. Leader direction takes precedence over your plan and peer requests
122
132
  the next safe boundary, subject to system instructions and user constraints.
123
133
  Return successful evidence as an ordinary final answer; the runtime submits it automatically
124
134
  as a successful candidate after execution settles. If you cannot perform the requested work,
125
- you must use `work({ action: "submit", outcome: "failed", result: "Blocker and unverified work" })`
135
+ you must use `task({ action: "complete", outcome: "failed", result: "Blocker and unverified work" })`
126
136
  instead of ordinary final prose. Avoid a second copy of an explicit submission.
127
137
  Ungated completion is not independently verified.
128
138
  ```
package/src/activity.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
- import { renderLiveActivityMarkdown, type PiMarkdownThemeSource } from "@fradser/pi-kit";
3
- import type { Teammate } from "./types.ts";
2
+ import { renderLiveActivityMarkdown, resolveLiveActivity, type PiMarkdownThemeSource } from "@fradser/pi-kit";
3
+
4
+ /** Bound by the extension entry, which alone owns the ExtensionAPI. Read per
5
+ * call, so a mid-session settings change reaches every teammate activity row:
6
+ * the widget, the console status line and expanded tool details alike. */
7
+ let hiddenThinking: () => boolean = () => false;
8
+ export function configureTeamActivity(reader: () => boolean): void {
9
+ hiddenThinking = reader;
10
+ }
11
+ import type { Agent } from "./types.ts";
4
12
 
5
13
  /** Console rows paint a whole status line in their own color, so the shared
6
14
  * markdown renderer runs with a passthrough theme: literal markup is still
@@ -24,14 +32,15 @@ function extractLatestLine(text: string | undefined): string | undefined {
24
32
  return undefined;
25
33
  }
26
34
 
27
- /** Extract the current teammate activity without imposing a display width. */
28
- export function runningTeammateActivity(teammate: Teammate): string {
29
- const tool = teammate.activeTool?.replace(/\s+/g, " ").trim();
30
- if (tool) return tool;
31
-
32
- return extractLatestLine(teammate.liveThinking)
33
- ?? extractLatestLine(teammate.liveText)
34
- ?? "Working...";
35
+ /** Extract the current teammate activity without imposing a display width.
36
+ * `hideThinking` comes from Pi's setting, so the shared pi-kit policy replaces
37
+ * reasoning with Pi's label on both the widget and the console row. */
38
+ export function runningTeammateActivity(teammate: Agent, hideThinking: boolean = hiddenThinking()): string {
39
+ return resolveLiveActivity({
40
+ tool: teammate.activeTool?.replace(/\s+/g, " ").trim(),
41
+ thinking: extractLatestLine(teammate.liveThinking),
42
+ text: extractLatestLine(teammate.liveText),
43
+ }, hideThinking) ?? "Working...";
35
44
  }
36
45
 
37
46
  export interface TeammateRowWidths {
@@ -54,7 +63,7 @@ export function teammateRowWidths(spinner: string, agent: string, width: number)
54
63
  }
55
64
 
56
65
  /** Fit a console status label to the requested activity width. */
57
- export function formatTeammateLabel(
66
+ export function formatAgentLabel(
58
67
  spinner: string,
59
68
  activity: string,
60
69
  maxActivityWidth?: number,
@@ -1,23 +1,22 @@
1
- import { randomUUID } from "node:crypto";
2
- import { WORKER_BUILTIN_TOOLS } from "./worker-tools.ts";
1
+ import { AGENT_BUILTIN_TOOLS } from "@fradser/pi-subagents";
3
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
- import { isValidTeammateName, listTeammates } from "./state.ts";
5
- import { resolveAgent } from "./agents.ts";
6
- import { shutdownTeammateExact, spawnTeammate } from "./team-machine.ts";
7
- import { normalizeCoordinationParams, requireParsedParams, type Teammate } from "./types.ts";
8
- import { snapshotWorkContext } from "./work-context.ts";
3
+ import { isValidAgentName, listAgents } from "./state.ts";
4
+ import { resolveAgent } from "@fradser/pi-subagents";
5
+ import { shutdownAgentExact, spawnAgent } from "./team-machine.ts";
6
+ import { normalizeCoordinationParams, requireParsedParams, type Agent } from "./types.ts";
7
+ import { snapshotWorkContext } from "@fradser/pi-subagents";
9
8
  import { exactSessionRoute, resolveExactSession } from "./recipient.ts";
10
9
 
11
10
  export interface AgentActionRuntime {
12
- spawnTeammate: typeof spawnTeammate;
13
- shutdownTeammateExact: typeof shutdownTeammateExact;
11
+ spawnAgent: typeof spawnAgent;
12
+ shutdownAgentExact: typeof shutdownAgentExact;
14
13
  }
15
14
 
16
- type Definition = Parameters<typeof spawnTeammate>[0]["definition"];
15
+ type Definition = Parameters<typeof spawnAgent>[0]["definition"];
17
16
 
18
- function session(teammate: Teammate) {
17
+ function session(teammate: Agent) {
19
18
  const tools = teammate.tools;
20
- const coordinationOnly = tools !== undefined && tools.every((tool) => tool === "agent_event" || tool === "work");
19
+ const coordinationOnly = tools !== undefined && tools.every((tool) => tool === "message" || tool === "task");
21
20
  return {
22
21
  tools,
23
22
  ...(coordinationOnly ? { warning: "coordination-only: no file or shell tools granted. Delegate execution work with explicit canonical tools; no bash is granted by default." } : {}),
@@ -29,17 +28,15 @@ function session(teammate: Teammate) {
29
28
  }
30
29
 
31
30
  function requireName(name: string): void {
32
- if (!isValidTeammateName(name)) throw new Error(`Invalid Agent name "${name}".`);
31
+ if (!isValidAgentName(name)) throw new Error(`Invalid Agent name "${name}".`);
33
32
  }
34
33
 
35
34
  export function runAgentAction(
36
35
  params: {
37
- action: "delegate" | "start" | "inspect" | "stop";
36
+ action: "start" | "inspect" | "stop";
38
37
  name?: string;
39
38
  prompt?: string;
40
39
  definition?: Definition;
41
- resources?: string[];
42
- verify?: string;
43
40
  model?: string;
44
41
  fork?: boolean;
45
42
  session?: string;
@@ -55,50 +52,78 @@ export function runAgentAction(
55
52
  if (!params.name) throw new Error("Agent inspect requires a name.");
56
53
  requireName(params.name);
57
54
  if (!params.session) throw new Error("Agent inspect requires an exact session handle.");
58
- const matched = resolveExactSession(params.session, listTeammates());
59
- if (!matched || matched.agent !== params.name) throw new Error(`No current session "${params.session}" belongs to @${params.name}.`);
55
+ const matched = resolveExactSession(params.session, listAgents());
56
+ if (!matched || matched.name !== params.name) throw new Error(`No current session "${params.session}" belongs to @${params.name}.`);
60
57
  return { action: "inspect", outcome: "inspected", agent: params.name, sessions: [session(matched)] };
61
58
  }
62
59
  if (params.action === "stop") {
63
60
  if (!params.session) throw new Error("Agent stop requires an exact session handle.");
64
- const matched = resolveExactSession(params.session, listTeammates());
61
+ const matched = resolveExactSession(params.session, listAgents());
65
62
  if (!matched) throw new Error(`No living session named "${params.session}".`);
66
- return runtime.shutdownTeammateExact(matched.name, matched.spawnId).then((result) => {
63
+ return runtime.shutdownAgentExact(matched.name, matched.spawnId).then((result) => {
67
64
  if (!result.ok) throw new Error(result.error);
68
- return { action: "stop", outcome: "stopped", session: params.session, body: result.body };
65
+ // The name comes from the resolved session, not from the request: a stop
66
+ // carries only a handle, so a receipt keyed on the request would be unnamed
67
+ // and a row drawn from it would have nothing to show.
68
+ return { action: "stop", outcome: "stopped", agent: matched.name, session: params.session, body: result.body };
69
69
  });
70
70
  }
71
- if (!params.name) throw new Error(`Agent ${params.action} requires a name.`);
71
+ if (!params.name) throw new Error("Agent start requires a name.");
72
72
  requireName(params.name);
73
- if (!params.definition && !resolveAgent(params.name, _cwd)) {
74
- throw new Error(`Unknown Agent @${params.name}. Define it inline through agent action=delegate or start; for example definition: { description: "Read evidence", prompt: "Read the assigned file and report evidence", tools: ["read"] }. Choose only needed canonical tools: ${WORKER_BUILTIN_TOOLS.join(", ")}. Omitted tools or [] grant coordination-only access.`);
73
+ if (params.definition !== undefined) {
74
+ const role = params.definition as { description?: string; prompt?: string };
75
+ const hasDescription = typeof role.description === "string" && role.description.trim().length > 0;
76
+ const hasPrompt = typeof role.prompt === "string" && role.prompt.trim().length > 0;
77
+ // Both halves or neither. Half a role produces a child that reports a result
78
+ // nobody asked for, because it has no instructions and nothing told it to
79
+ // wait.
80
+ if (hasDescription !== hasPrompt) {
81
+ throw new Error("An inline role needs both description and role_prompt, or neither.");
82
+ }
75
83
  }
76
- const prompt = params.action === "delegate" ? params.prompt?.trim() : undefined;
77
- if (params.action === "delegate" && !prompt) throw new Error("Agent delegate requires a prompt.");
78
- if (params.action === "start" && params.fork !== undefined) throw new Error("Agent start cannot fork context.");
84
+ // A prompt needs a role; an idle resident does not. A prompted child with no
85
+ // role would run without instructions and report a result nobody asked for,
86
+ // which is worse than refusing. A child started with no prompt is waiting for
87
+ // work, and requiring a role up front would make the normal two spawn shapes —
88
+ // do this one thing, or sit ready — behave differently from the tool.
89
+ if (params.prompt && !params.definition && !resolveAgent(params.name, _cwd)) {
90
+ throw new Error(`A prompt needs a role. @${params.name} has no definition: pass description and role_prompt, or use a name that already has one. Choose only needed canonical tools: ${AGENT_BUILTIN_TOOLS.join(", ")}. Omitted tools or [] grant coordination-only access.`);
91
+ }
92
+ // One spawn verb with an optional prompt. `delegate` and `start` were the same
93
+ // operation dispatched twice, and the only difference was whether a prompt was
94
+ // delivered; two names meant a caller had to pick between two identically
95
+ // shaped paths and the wrong pick was invisible.
96
+ const prompt = params.prompt?.trim();
97
+ if (params.prompt !== undefined && !prompt) throw new Error("An empty prompt is not a task. Omit it to leave the agent idle.");
79
98
  const context = params.fork ? snapshotWorkContext(sessionManager) : undefined;
80
- const result = runtime.spawnTeammate({
99
+ const result = runtime.spawnAgent({
81
100
  name: params.name,
82
- agent: params.name,
83
- ...(params.action === "delegate" ? { workId: `work:${randomUUID()}`, prompt, resources: params.resources as string[] | undefined, verify: params.verify, context } : {}),
101
+ role: params.name,
102
+ // A prompt is delivered, not recorded: spawning creates no task, so there is
103
+ // no work id, no resource lease, and no verify gate to attach. Anything that
104
+ // needs a record is a task the caller creates and the participant takes.
105
+ ...(prompt ? { prompt } : {}),
84
106
  ...(params.model ? { model: params.model } : {}),
85
107
  ...(params.definition ? { definition: params.definition } : {}),
108
+ ...(context ? { context } : {}),
86
109
  });
87
110
  if (!result.ok) throw new Error(result.error);
88
- const receipt = (teammate: Teammate) => ({
89
- action: params.action,
111
+ const receipt = (teammate: Agent) => ({
112
+ action: "start",
90
113
  outcome: "started",
91
114
  agent: params.name,
115
+ prompted: Boolean(prompt),
92
116
  session: session(teammate),
93
- ...(params.action === "delegate" ? { work: { id: teammate.workId, state: "claimed" }, assignment: { id: teammate.assignment?.id } } : {}),
94
117
  });
95
- if (params.action === "start" && result.readiness) {
118
+ // The machine offers one bounded startup receipt: native readiness without
119
+ // input, or correlated accepted input with a kickoff. Neither means completed.
120
+ if (result.readiness) {
96
121
  return result.readiness.then((error) => {
97
122
  if (error) throw new Error(error);
98
- const current = resolveExactSession(exactSessionRoute(result.teammate.name, result.teammate.spawnId), listTeammates());
123
+ const current = resolveExactSession(exactSessionRoute(result.agent.name, result.agent.spawnId), listAgents());
99
124
  if (!current || current.status === "stopped") throw new Error("Resident exited or was replaced before startup readiness completed.");
100
125
  return receipt(current);
101
126
  });
102
127
  }
103
- return receipt(result.teammate);
128
+ return receipt(result.agent);
104
129
  }