@fradser/pi-agent-teams 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -6
- package/package.json +3 -3
- package/references/agent-roles.md +77 -0
- package/src/agents.ts +42 -10
- package/src/follow-up-queue.ts +1 -0
- package/src/guidance.ts +26 -12
- package/src/index.ts +25 -3
- package/src/team-machine.ts +25 -3
- package/src/tools.ts +72 -91
- package/src/types.ts +1 -1
- package/src/ui.ts +219 -62
- 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
package/README.md
CHANGED
|
@@ -36,9 +36,13 @@ Review the assigned scope for exploitable security problems. Do not edit files.
|
|
|
36
36
|
|
|
37
37
|
Discovery precedence per name (later overrides earlier):
|
|
38
38
|
|
|
39
|
+
There are no built-in roles. When a needed role has no definition, the
|
|
40
|
+
leader creates one on demand: it derives the definition from the task using
|
|
41
|
+
the abstract role reference in `references/agent-roles.md` (definition
|
|
42
|
+
anatomy, archetype axes, invariants), writes the file, and then spawns.
|
|
43
|
+
|
|
39
44
|
| Scope | Location | Git semantics |
|
|
40
45
|
|---|---|---|
|
|
41
|
-
| bundled | package `agents/` | shipped with the package |
|
|
42
46
|
| user | `~/.pi/agent/agents/*.md` | system-level, never committed |
|
|
43
47
|
| project | `<cwd>/.pi/agents/<name>.md` | **git-managed** — commit for the team |
|
|
44
48
|
| project-local | `<cwd>/.pi/agents/<name>.local.md` | **local** — personal override, gitignore by convention |
|
|
@@ -90,7 +94,7 @@ send_message({ to, message, status? })
|
|
|
90
94
|
- Peer mail: `to: "<teammate-name>"`; `status` is invalid.
|
|
91
95
|
- Leader steering or direct assignment: `to: "<teammate-name>"`; a working recipient gets a control-stream steer, an idle recipient wakes with the message.
|
|
92
96
|
- The first non-empty line of `message` becomes the console title.
|
|
93
|
-
- Peer traffic never enters the leader model context; inspect it in `/
|
|
97
|
+
- Peer traffic never enters the leader model context; inspect it in `/agent-teams` instead.
|
|
94
98
|
|
|
95
99
|
Only the leader creates tasks. Idle teammates self-claim pending tasks whose dependencies are met. A task-level `verify` command overrides the claiming agent's frontmatter `verify`; zero exit completes the task, while failure returns stderr to the claimer for fix-and-resubmit.
|
|
96
100
|
|
|
@@ -105,7 +109,7 @@ Only the leader creates tasks. Idle teammates self-claim pending tasks whose dep
|
|
|
105
109
|
| `task_list()` | Both | One shared board-view definition; leader view also includes the roster |
|
|
106
110
|
| `task_claim(taskId?)` | Worker | Atomically self-claim a pending, unblocked task |
|
|
107
111
|
| `task_submit(taskId, status, result?)` | Worker | Submit a claimed task outcome; completion passes through verify |
|
|
108
|
-
| `/
|
|
112
|
+
| `/agent-teams` | User | Management console: session teammates, persistent agent roles, board, details, shutdown |
|
|
109
113
|
|
|
110
114
|
That is **7 unique tool names**. There are no `teammate_run`, `teammate_fanout`, `teammate_cancel`, `teammate_retry`, or `teammate_message` tools.
|
|
111
115
|
|
|
@@ -127,17 +131,17 @@ Runtime state lives in `~/.pi/agent/teammate/<sessionKey>/` and dies with the se
|
|
|
127
131
|
```
|
|
128
132
|
agent-teams/
|
|
129
133
|
├── index.ts — package-root extension entry point
|
|
130
|
-
├──
|
|
134
|
+
├── references/ — role templates consulted when generating new agents
|
|
131
135
|
├── src/
|
|
132
136
|
│ ├── index.ts — composition root and session lifecycle
|
|
133
|
-
│ ├── tools.ts — leader tools and /
|
|
137
|
+
│ ├── tools.ts — leader tools and /agent-teams command
|
|
134
138
|
│ ├── worker.ts — worker tools plus the shared task_list registration
|
|
135
139
|
│ ├── team-machine.ts — resident lifecycle, mail routing, intents, verify, wake-ups
|
|
136
140
|
│ ├── state.ts — roster, board, leader inbox state machine
|
|
137
141
|
│ ├── spawner.ts — resident RPC process spawner (uncapped sequences)
|
|
138
142
|
│ ├── agents.ts — agent discovery and frontmatter parsing
|
|
139
143
|
│ ├── statefile.ts — runtime/board/mail IO and marker-file intents
|
|
140
|
-
│ ├── ui.ts — passive widget and /
|
|
144
|
+
│ ├── ui.ts — passive widget and /agent-teams management console
|
|
141
145
|
│ ├── guidance.ts — static leader and worker protocols
|
|
142
146
|
│ └── follow-up-queue.ts — serialized automatic report delivery
|
|
143
147
|
├── 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.7.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.3.0"
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Agent Role Reference
|
|
2
|
+
|
|
3
|
+
Abstract reference for generating new agent definitions on demand. This file
|
|
4
|
+
is NOT discoverable and CANNOT be spawned directly. Derive a concrete
|
|
5
|
+
definition from the anatomy, archetype axes, and invariants below, tailor it
|
|
6
|
+
to the task, and write it to `<cwd>/.pi/agents/<name>.md` (git-managed
|
|
7
|
+
project role) or `<name>.local.md` (personal override).
|
|
8
|
+
|
|
9
|
+
## Definition anatomy
|
|
10
|
+
|
|
11
|
+
Frontmatter:
|
|
12
|
+
|
|
13
|
+
- `name` — unique id among discovered roles.
|
|
14
|
+
- `description` — the routing contract: when the leader should choose this
|
|
15
|
+
role, phrased as "use when ...". This is the most load-bearing field.
|
|
16
|
+
- `tools` — the minimal set; follows the mutability axis (see below).
|
|
17
|
+
- optional `model`, `verify`, `worktree`.
|
|
18
|
+
|
|
19
|
+
The Markdown body is the role prompt, built from five parts:
|
|
20
|
+
|
|
21
|
+
1. Identity — "You are a `<role>` agent."
|
|
22
|
+
2. Method — the procedure to execute, the criteria to judge against, or the
|
|
23
|
+
observation targets, depending on the judgment axis.
|
|
24
|
+
3. Boundaries — what must never be touched or changed.
|
|
25
|
+
4. Evidence — what every claim or finding must carry.
|
|
26
|
+
5. Terminal report — delivery via `send_message(to="leader", message=...)`;
|
|
27
|
+
the last message to the leader MUST
|
|
28
|
+
carry status="completed" (or status="failed"); work without a terminal
|
|
29
|
+
status counts as unfinished work.
|
|
30
|
+
|
|
31
|
+
## Archetype axes
|
|
32
|
+
|
|
33
|
+
Pick a position on two axes before writing anything:
|
|
34
|
+
|
|
35
|
+
| Axis | Values |
|
|
36
|
+
|---|---|
|
|
37
|
+
| Mutability | mutating (`edit`,`write` allowed) · read-only |
|
|
38
|
+
| Judgment | execute a given procedure · evaluate against criteria · apply domain expertise · observe and report |
|
|
39
|
+
|
|
40
|
+
Read-only plus each judgment value yields the four classical shapes:
|
|
41
|
+
|
|
42
|
+
| Shape | Typical tools | Body emphasis |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| executor | `read,bash,edit,write` | implement end to end, then self-verify by running tests or commands; report changes made, verification performed, remaining risks |
|
|
45
|
+
| reviewer | `read,bash` | only confirmed findings, with severity, evidence, exact paths, and a minimal fix recommendation; when nothing is found, say so and list the checks performed |
|
|
46
|
+
| specialist | `read,bash` | deep domain analysis; state assumptions, the evidence used, and the bounds of expertise for this task |
|
|
47
|
+
| observer | `read` | snapshots or readings; summarize observed state and anomalies with timestamps and evidence; change nothing |
|
|
48
|
+
|
|
49
|
+
These shapes are starting points, not a fixed menu: a generated role may sit
|
|
50
|
+
anywhere on the axes (for example a mutating specialist with a verify gate),
|
|
51
|
+
as long as every invariant below holds.
|
|
52
|
+
|
|
53
|
+
## Invariants
|
|
54
|
+
|
|
55
|
+
Non-negotiable in every generated definition:
|
|
56
|
+
|
|
57
|
+
- Tool sets match the mutability axis: read-only roles never receive
|
|
58
|
+
`edit` or `write`.
|
|
59
|
+
- Every finding or claim carries evidence: exact paths, command output,
|
|
60
|
+
timestamps — never bare assertions.
|
|
61
|
+
- Scope is bounded: mutating roles state "do not touch files outside the
|
|
62
|
+
assigned scope"; read-only roles state the equivalent prohibition.
|
|
63
|
+
- Terminal-status discipline applies to all shapes alike.
|
|
64
|
+
|
|
65
|
+
## Skeleton
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
---
|
|
69
|
+
name: <role-name>
|
|
70
|
+
description: <one-line capability>; use when <routing condition>
|
|
71
|
+
tools: <minimal set from the mutability axis>
|
|
72
|
+
---
|
|
73
|
+
You are a <role-name> agent. <Method: the procedure to follow, the criteria
|
|
74
|
+
to evaluate against, or the targets to observe>. <Boundary statement>.
|
|
75
|
+
Report <expected artifacts> with <evidence requirements> via
|
|
76
|
+
send_message(to="leader", message=...) with status="completed".
|
|
77
|
+
```
|
package/src/agents.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
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)
|
|
@@ -22,7 +24,7 @@ import * as path from "node:path";
|
|
|
22
24
|
import { fileURLToPath } from "node:url";
|
|
23
25
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
24
26
|
|
|
25
|
-
export type AgentScope = "
|
|
27
|
+
export type AgentScope = "user" | "project" | "project-local";
|
|
26
28
|
|
|
27
29
|
export interface AgentDefinition {
|
|
28
30
|
name: string;
|
|
@@ -44,7 +46,11 @@ export interface AgentDefinition {
|
|
|
44
46
|
source: string;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
/** Shipped role templates: reference material for generating new definitions
|
|
50
|
+
* on demand — never discovered and never spawnable directly. */
|
|
51
|
+
export const AGENT_REFERENCE_PATH = path.resolve(
|
|
52
|
+
path.dirname(fileURLToPath(import.meta.url)), "..", "references", "agent-roles.md",
|
|
53
|
+
);
|
|
48
54
|
|
|
49
55
|
/** Files named `xxx.local.md` in the project agents directory are personal
|
|
50
56
|
* overrides: same teammate name as `xxx.md`, project-local scope, never git-
|
|
@@ -62,13 +68,40 @@ function parseFrontmatter(raw: string): { fields: Record<string, string | string
|
|
|
62
68
|
const fields: Record<string, string | string[]> = {};
|
|
63
69
|
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
|
|
64
70
|
if (!match) return { fields, body: raw.trim() };
|
|
65
|
-
|
|
71
|
+
const lines = match[1].split("\n");
|
|
72
|
+
for (let index = 0; index < lines.length; index++) {
|
|
73
|
+
const line = lines[index];
|
|
66
74
|
const separator = line.indexOf(":");
|
|
67
75
|
if (separator <= 0) continue;
|
|
68
76
|
const key = line.slice(0, separator).trim();
|
|
69
77
|
// Strip inline YAML comments (# ...) and surrounding whitespace/quotes.
|
|
70
78
|
let value = line.slice(separator + 1).replace(/\s+#.*$/, "").trim();
|
|
71
79
|
if (key === "tools") {
|
|
80
|
+
if (value === "") {
|
|
81
|
+
// YAML block sequence: "- item" lines, flush-left or indented,
|
|
82
|
+
// possibly interleaved with blank or comment-only lines.
|
|
83
|
+
const items: string[] = [];
|
|
84
|
+
index += 1;
|
|
85
|
+
while (index < lines.length) {
|
|
86
|
+
const trimmed = lines[index].trim();
|
|
87
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!/^-.+/.test(trimmed)) break;
|
|
92
|
+
const rawItem = trimmed.replace(/^-\s*/, "");
|
|
93
|
+
const item = rawItem
|
|
94
|
+
.replace(/\s+#.*$/, "")
|
|
95
|
+
.trim()
|
|
96
|
+
.replace(/^["']|["']$/g, "");
|
|
97
|
+
// "- # comment" is a comment-only entry, not a tool.
|
|
98
|
+
if (item && !rawItem.trimStart().startsWith("#")) items.push(item);
|
|
99
|
+
index += 1;
|
|
100
|
+
}
|
|
101
|
+
index -= 1; // The for-loop steps past the last consumed line.
|
|
102
|
+
fields.tools = items;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
72
105
|
const tools = value
|
|
73
106
|
.replace(/^\[|\]$/g, "")
|
|
74
107
|
.split(",")
|
|
@@ -133,11 +166,10 @@ function scopeIsGitManaged(scope: AgentScope): boolean {
|
|
|
133
166
|
|
|
134
167
|
/**
|
|
135
168
|
* Resolve all agent definitions visible from the given cwd, with later scopes
|
|
136
|
-
* overriding earlier ones per name: project-local > project > user
|
|
169
|
+
* overriding earlier ones per name: project-local > project > user.
|
|
137
170
|
*/
|
|
138
171
|
export function discoverAgents(cwd?: string): Map<string, AgentDefinition> {
|
|
139
172
|
const agents = new Map<string, AgentDefinition>();
|
|
140
|
-
loadDir("bundled", BUNDLED_AGENTS_DIR, agents);
|
|
141
173
|
loadDir("user", agentsDir("user", cwd), agents);
|
|
142
174
|
loadDir("project", agentsDir("project", cwd), agents);
|
|
143
175
|
return agents;
|
|
@@ -158,7 +190,7 @@ export function hasAgent(name: string, cwd?: string): boolean {
|
|
|
158
190
|
*/
|
|
159
191
|
export function formatAgentGuidance(cwd?: string): string {
|
|
160
192
|
const agents = discoverAgents(cwd);
|
|
161
|
-
if (agents.size === 0) return "(none
|
|
193
|
+
if (agents.size === 0) return "(none defined yet — create one on demand from the shipped templates)";
|
|
162
194
|
const lines: string[] = [];
|
|
163
195
|
for (const agent of agents.values()) {
|
|
164
196
|
const extras = [
|
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
|
|
@@ -39,22 +39,31 @@ delegation, synthesis, and the final user-facing answer. Teammates are named
|
|
|
39
39
|
resident child processes with isolated contexts; they do not see this
|
|
40
40
|
conversation unless you put the needed context in their prompts.
|
|
41
41
|
|
|
42
|
-
### Agents are declarative files
|
|
42
|
+
### Agents are declarative files, created on demand
|
|
43
43
|
|
|
44
44
|
Agents live in Markdown files with frontmatter (name, description, tools,
|
|
45
45
|
optional model, optional verify, optional worktree); the body is the role
|
|
46
|
-
prompt. Discovery precedence per name:
|
|
47
|
-
\`<cwd>/.pi/agents/<name>.local.md\` > project
|
|
48
|
-
\`<cwd>/.pi/agents/<name>.md\` > user \`~/.pi/agent/agents
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
prompt. There are no built-in roles. Discovery precedence per name:
|
|
47
|
+
project-local \`<cwd>/.pi/agents/<name>.local.md\` > project
|
|
48
|
+
\`<cwd>/.pi/agents/<name>.md\` > user \`~/.pi/agent/agents\`. Same-name
|
|
49
|
+
project/project-local pairs deduplicate into one definition, with
|
|
50
|
+
project-local winning; project definitions are git-managed, while
|
|
51
|
+
project-local and user definitions are personal/non-git-managed. Model and
|
|
52
|
+
worktree behavior are role attributes: define a role variant when they need
|
|
53
|
+
to differ.
|
|
54
54
|
|
|
55
55
|
Available agents:
|
|
56
56
|
${agents}
|
|
57
57
|
|
|
58
|
+
When an assignment or user request needs an agent whose name has no
|
|
59
|
+
definition yet, create it first: derive it from the shipped abstract role
|
|
60
|
+
reference at \`${AGENT_REFERENCE_PATH}\` — its definition anatomy, archetype
|
|
61
|
+
axes, and invariants (read it before inventing a novel role) — tailor it to
|
|
62
|
+
the task, and write
|
|
63
|
+
the definition to \`<cwd>/.pi/agents/<name>.md\` for a git-managed project
|
|
64
|
+
role or \`<name>.local.md\` for a personal override; discovery picks it up
|
|
65
|
+
immediately and spawning can proceed.
|
|
66
|
+
|
|
58
67
|
### Build a team in one step per teammate
|
|
59
68
|
|
|
60
69
|
Use teammate_spawn(name, agent, optional kickoff prompt) to start a resident
|
|
@@ -63,13 +72,18 @@ inbox messages and claimable board tasks, and can message peers directly.
|
|
|
63
72
|
A session-wide cap of 8 living teammates applies. An agent with
|
|
64
73
|
worktree: true receives its own git worktree; its diff is captured at shutdown.
|
|
65
74
|
|
|
75
|
+
Users may also ask for these conversationally ("add a reviewer teammate",
|
|
76
|
+
"create a scribe role") — apply the same create-on-demand step above when
|
|
77
|
+
the role does not exist yet. The human-facing management surface is the
|
|
78
|
+
/agent-teams command; never simulate or describe menu flows to the user.
|
|
79
|
+
|
|
66
80
|
### Coordinate through one messaging primitive and the board
|
|
67
81
|
|
|
68
82
|
send_message is the only messaging tool. Address a teammate by name to steer
|
|
69
83
|
it or hand work to it; working teammates receive it immediately and idle
|
|
70
84
|
teammates wake automatically. The reserved recipient name "leader" is only
|
|
71
85
|
for worker reports, not for leader calls. Peer traffic never reaches your
|
|
72
|
-
context — inspect it in /
|
|
86
|
+
context — inspect it in /agent-teams instead.
|
|
73
87
|
|
|
74
88
|
Create shared work with task_create(subject, description?, dependsOn?,
|
|
75
89
|
verify?). Idle teammates notice claimable work automatically and self-claim;
|
|
@@ -85,7 +99,7 @@ working teammate produces no output for a while; that notice is information,
|
|
|
85
99
|
not a verdict — decide whether to keep waiting, steer again with send_message,
|
|
86
100
|
or teammate_shutdown it. To carry wedged work forward, spawn a successor whose
|
|
87
101
|
prompt composes context from the original kickoff, the teammate's past reports
|
|
88
|
-
(leader mailbox or /
|
|
102
|
+
(leader mailbox or /agent-teams detail view), its board claims, and any live
|
|
89
103
|
transcript tail. The harness never reclaims, restarts, or replaces a teammate
|
|
90
104
|
on its own.
|
|
91
105
|
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { resetState } from "./state.ts";
|
|
|
14
14
|
import { ensureTeamWidget, refreshTeamUI, stopUiTimers } from "./ui.ts";
|
|
15
15
|
import { registerLeaderTools, registerTeamCommand } from "./tools.ts";
|
|
16
16
|
import { registerWorkerCapabilities, workerBinding } from "./worker.ts";
|
|
17
|
-
import { formatAgentMessagePrefix } from "@fradser/pi-kit";
|
|
17
|
+
import { formatAgentMessagePrefix, formatExpandHint } from "@fradser/pi-kit";
|
|
18
18
|
import { FollowUpQueue, groupReportsByTeammate, TEAMMATE_REPORT_MESSAGE_TYPE, type FollowUpReport } from "./follow-up-queue.ts";
|
|
19
19
|
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
20
20
|
|
|
@@ -23,6 +23,9 @@ const STATE_DIR_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
23
23
|
let leaderPi: ExtensionAPI | undefined;
|
|
24
24
|
let leaderCtx: ExtensionContext | undefined;
|
|
25
25
|
let followUpQueue: FollowUpQueue | undefined;
|
|
26
|
+
// One "Teammate finished" announcement per spawn incarnation; repeated
|
|
27
|
+
// terminal reports from the same resident stay ordinary report rows.
|
|
28
|
+
const announcedFinishKeys = new Set<string>();
|
|
26
29
|
|
|
27
30
|
const REPORT_COLORS = ["success", "warning", "error", "mdLink"] as const;
|
|
28
31
|
export const TEAMMATE_FINISHED_ENTRY_TYPE = "agent-teams-teammate-finished";
|
|
@@ -61,7 +64,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
61
64
|
}
|
|
62
65
|
if (!expanded) {
|
|
63
66
|
const groups = groupReportsByTeammate(reports);
|
|
64
|
-
const hint =
|
|
67
|
+
const hint = formatExpandHint(keyHint("app.tools.expand", "to expand"), theme);
|
|
65
68
|
for (const group of groups) {
|
|
66
69
|
const prefix = theme.fg("customMessageLabel", theme.bold(formatAgentMessagePrefix("from", group.reports.length)));
|
|
67
70
|
const name = theme.fg(reportColor(group.teammate), `@${group.teammate}`);
|
|
@@ -88,7 +91,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
88
91
|
if (event.message.role !== "custom" || event.message.customType !== TEAMMATE_REPORT_MESSAGE_TYPE) return;
|
|
89
92
|
const reports = extractReports(event.message.details);
|
|
90
93
|
for (const report of reports) {
|
|
91
|
-
if (!report
|
|
94
|
+
if (!markTeammateFinished(announcedFinishKeys, report)) continue;
|
|
92
95
|
pi.appendEntry(TEAMMATE_FINISHED_ENTRY_TYPE, {
|
|
93
96
|
teammate: report.teammate ?? report.agent,
|
|
94
97
|
agent: report.agent,
|
|
@@ -99,6 +102,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
99
102
|
pi.on("session_start", async (_event, ctx) => {
|
|
100
103
|
resetState();
|
|
101
104
|
followUpQueue?.reset();
|
|
105
|
+
announcedFinishKeys.clear();
|
|
102
106
|
leaderCtx = ctx;
|
|
103
107
|
followUpQueue = new FollowUpQueue({
|
|
104
108
|
isIdle: () => Boolean(leaderCtx?.isIdle()),
|
|
@@ -158,3 +162,21 @@ function extractReports(details: unknown): FollowUpReport[] {
|
|
|
158
162
|
if (typed && "teammate" in typed) return [typed as FollowUpReport];
|
|
159
163
|
return [];
|
|
160
164
|
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Announce a finished entry only on the first terminal report of a spawn
|
|
168
|
+
* incarnation. Reports without a spawn identity key on the session scope so
|
|
169
|
+
* crash diagnostics always stay visible. Returns true when the caller should
|
|
170
|
+
* append the finished entry.
|
|
171
|
+
*/
|
|
172
|
+
export function markTeammateFinished(
|
|
173
|
+
seen: Set<string>,
|
|
174
|
+
report: Pick<FollowUpReport, "teammate" | "agent" | "spawnId" | "finished">,
|
|
175
|
+
): boolean {
|
|
176
|
+
if (!report.finished) return false;
|
|
177
|
+
const name = report.teammate ?? report.agent ?? "teammate";
|
|
178
|
+
const key = `${name}:${report.spawnId ?? "session"}`;
|
|
179
|
+
if (seen.has(key)) return false;
|
|
180
|
+
seen.add(key);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
package/src/team-machine.ts
CHANGED
|
@@ -125,6 +125,8 @@ const pendingShutdowns = new Set<string>();
|
|
|
125
125
|
const verifyingTasks = new Map<string, { worker: string; spawnId: string }>();
|
|
126
126
|
/** Idle nudges already fired per teammate incarnation (one per transition). */
|
|
127
127
|
const idleNudgesSent = new Set<string>();
|
|
128
|
+
/** Self-finalize requests delivered per teammate incarnation before escalating to the leader. */
|
|
129
|
+
const selfFinalizeAttempts = new Set<string>();
|
|
128
130
|
const pendingDeliveries = new Map<string, InboxMessage[]>();
|
|
129
131
|
const liveWorktrees = new Map<string, ReturnType<typeof createWorktree>>();
|
|
130
132
|
|
|
@@ -163,6 +165,7 @@ export function shutdownTeamMachine(): void {
|
|
|
163
165
|
pendingShutdowns.clear();
|
|
164
166
|
verifyingTasks.clear();
|
|
165
167
|
idleNudgesSent.clear();
|
|
168
|
+
selfFinalizeAttempts.clear();
|
|
166
169
|
pendingDeliveries.clear();
|
|
167
170
|
}
|
|
168
171
|
|
|
@@ -408,7 +411,7 @@ function checkStalledTeammates(now = Date.now()): void {
|
|
|
408
411
|
const silence = stallSilenceMs(teammate, now);
|
|
409
412
|
if (silence === undefined) continue;
|
|
410
413
|
if (silence < STALL_NOTICE_MS || teammate.stallNoticeSentAt !== undefined) continue;
|
|
411
|
-
const body = `@${teammate.name} has produced no RPC output for ${formatSilenceDuration(silence)}. The child may be blocked in a provider or tool call; steer delivery is uncertain. Decide: keep waiting, steer again, or shut it down (and respawn a successor with context from the original kickoff, its mailbox reports, board claims, and the /
|
|
414
|
+
const body = `@${teammate.name} has produced no RPC output for ${formatSilenceDuration(silence)}. The child may be blocked in a provider or tool call; steer delivery is uncertain. Decide: keep waiting, steer again, or shut it down (and respawn a successor with context from the original kickoff, its mailbox reports, board claims, and the /agent-teams detail transcript).`;
|
|
412
415
|
updateTeammate(teammate.name, { stallNoticeSentAt: now });
|
|
413
416
|
deliverToLeader({ from: "harness", subject: `Possible stall: @${teammate.name}`, body });
|
|
414
417
|
sendUpdate({ teammate: teammate.name, agent: teammate.agent, body, finished: false });
|
|
@@ -428,9 +431,24 @@ export function hasUnfinalizedReport(name: string): boolean {
|
|
|
428
431
|
/** One light reminder per idle transition when work looks unfinished. */
|
|
429
432
|
function nudgeIfUnfinalized(name: string, spawnId: string): void {
|
|
430
433
|
const key = `${name}:${spawnId}`;
|
|
431
|
-
|
|
434
|
+
// The terminal report may have been written to the outbox file but not yet
|
|
435
|
+
// drained into the leader mailbox by the next tick; drain before deciding.
|
|
436
|
+
drainTeammateOutboxes();
|
|
437
|
+
if (!hasUnfinalizedReport(name)) return;
|
|
438
|
+
if (!selfFinalizeAttempts.has(key)) {
|
|
439
|
+
// First miss: give the worker one chance to fix its own bookkeeping
|
|
440
|
+
// before bothering the leader. The inbox message wakes it on the next tick.
|
|
441
|
+
selfFinalizeAttempts.add(key);
|
|
442
|
+
deliverFeedback(
|
|
443
|
+
name,
|
|
444
|
+
"Assignment not finalized",
|
|
445
|
+
'Your latest message to the leader carried no terminal status. Send send_message(to="leader", message=...) now with status="completed" or status="failed" summarizing your final result.',
|
|
446
|
+
);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (idleNudgesSent.has(key)) return;
|
|
432
450
|
idleNudgesSent.add(key);
|
|
433
|
-
const reminder = `@${name} is now idle but its last report was not marked status="completed" or "failed". Its conclusions may be stuck in the mailbox — ask it to finalize or inspect the /
|
|
451
|
+
const reminder = `@${name} is now idle but its last report was not marked status="completed" or "failed". Its conclusions may be stuck in the mailbox — ask it to finalize or inspect the /agent-teams console.`;
|
|
434
452
|
deliverToLeader({ from: "harness", subject: `Idle without terminal report: @${name}`, body: reminder });
|
|
435
453
|
sendUpdate({ teammate: name, body: reminder, finished: false });
|
|
436
454
|
}
|
|
@@ -464,6 +482,8 @@ export async function shutdownTeammate(name: string): Promise<{ ok: true; body:
|
|
|
464
482
|
pendingShutdowns.delete(name);
|
|
465
483
|
const released = releaseTasksOf(name, "Teammate was shut down.");
|
|
466
484
|
updateTeammate(name, { status: "stopped", activeTool: undefined });
|
|
485
|
+
idleNudgesSent.delete(`${name}:${teammate.spawnId}`);
|
|
486
|
+
selfFinalizeAttempts.delete(`${name}:${teammate.spawnId}`);
|
|
467
487
|
publishStateSnapshot();
|
|
468
488
|
notifyChange();
|
|
469
489
|
return { ok: true, body: summarizeShutdown(name, released.length, 0, undefined) };
|
|
@@ -493,6 +513,7 @@ async function handleTeammateClose(name: string, spawnId: string, result: Worker
|
|
|
493
513
|
// A stopped teammate must not keep the poll loop or its queue alive.
|
|
494
514
|
pendingDeliveries.delete(name);
|
|
495
515
|
idleNudgesSent.delete(`${name}:${teammate.spawnId}`);
|
|
516
|
+
selfFinalizeAttempts.delete(`${name}:${teammate.spawnId}`);
|
|
496
517
|
updateTeammate(name, {
|
|
497
518
|
status: "stopped",
|
|
498
519
|
activeTool: undefined,
|
|
@@ -614,6 +635,7 @@ function applyOutboxRecord(teammate: Teammate, record: unknown): boolean {
|
|
|
614
635
|
sendUpdate({
|
|
615
636
|
teammate: teammate.name,
|
|
616
637
|
agent: teammate.agent,
|
|
638
|
+
spawnId: teammate.spawnId,
|
|
617
639
|
body: record.body,
|
|
618
640
|
finished: true,
|
|
619
641
|
});
|
package/src/tools.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { formatAgentMessagePrefix, formatAgentTaskName, formatToolEventLabel } from "@fradser/pi-kit";
|
|
1
|
+
import { keyHint, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { formatAgentMessagePrefix, formatAgentTaskName, formatExpandHint, formatToolEventLabel, safeDisplayText } from "@fradser/pi-kit";
|
|
3
3
|
import {
|
|
4
4
|
createBoardTask,
|
|
5
5
|
formatSilenceDuration,
|
|
@@ -12,13 +12,8 @@ import { listTasks, livingTeammates } from "./state.ts";
|
|
|
12
12
|
import { LEADER_RECIPIENT, SendMessageParams, TeammateShutdownParams, TeammateSpawnParams, TaskCreateParams } from "./types.ts";
|
|
13
13
|
import { registerTaskListTool } from "./worker.ts";
|
|
14
14
|
import { openTeamConsole, refreshTeamUI } from "./ui.ts";
|
|
15
|
+
import { discoverAgents } from "./agents.ts";
|
|
15
16
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
16
|
-
import * as fs from "node:fs";
|
|
17
|
-
import * as path from "node:path";
|
|
18
|
-
|
|
19
|
-
function isValidAgentName(name: string): boolean {
|
|
20
|
-
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && !name.includes("..") && name !== "con";
|
|
21
|
-
}
|
|
22
17
|
|
|
23
18
|
function rosterSummary(): string {
|
|
24
19
|
const alive = livingTeammates();
|
|
@@ -39,7 +34,7 @@ export function registerLeaderTools(pi: ExtensionAPI): void {
|
|
|
39
34
|
renderCall: () => new Text("", 0, 0),
|
|
40
35
|
renderResult(result, _options, theme, context) {
|
|
41
36
|
const text = result.content[0]?.type === "text" ? (result.content[0] as { type: string; text: string }).text : "";
|
|
42
|
-
if (
|
|
37
|
+
if (context.isError) {
|
|
43
38
|
return new Text(theme.fg("error", text.split("\n")[0] || "Failed to spawn teammate."), 0, 0);
|
|
44
39
|
}
|
|
45
40
|
const params = context.args as { name: string; agent: string; prompt?: string };
|
|
@@ -71,17 +66,24 @@ export function registerLeaderTools(pi: ExtensionAPI): void {
|
|
|
71
66
|
parameters: TeammateShutdownParams,
|
|
72
67
|
renderShell: "self",
|
|
73
68
|
renderCall: () => new Text("", 0, 0),
|
|
74
|
-
renderResult(result,
|
|
69
|
+
renderResult(result, options, theme, context) {
|
|
75
70
|
const text = result.content[0]?.type === "text" ? (result.content[0] as { type: string; text: string }).text : "";
|
|
76
|
-
if (
|
|
71
|
+
if (context.isError) {
|
|
77
72
|
return new Text(theme.fg("error", text.split("\n")[0] || "Failed to shut down teammate."), 0, 0);
|
|
78
73
|
}
|
|
79
74
|
const name = String((context.args as { name?: string }).name ?? "");
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
const title = theme.fg("toolTitle", theme.bold(formatToolEventLabel("event", `@${name} shut down`, "agent")));
|
|
76
|
+
const render = (width: number) => {
|
|
77
|
+
if (width <= 0) return [];
|
|
78
|
+
if (!options.expanded) {
|
|
79
|
+
return [truncateToWidth(`${title}${formatExpandHint(keyHint("app.tools.expand", "to expand"), theme)}`, width)];
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
truncateToWidth(title, width),
|
|
83
|
+
...text.split("\n").map((line) => truncateToWidth(theme.fg("customMessageText", safeDisplayText(line)), width)),
|
|
84
|
+
];
|
|
84
85
|
};
|
|
86
|
+
return { render, invalidate: () => {} };
|
|
85
87
|
},
|
|
86
88
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
87
89
|
const result = await shutdownTeammate(params.name);
|
|
@@ -97,10 +99,26 @@ export function registerLeaderTools(pi: ExtensionAPI): void {
|
|
|
97
99
|
label: "Send Message",
|
|
98
100
|
description: "The only messaging primitive. Address a living teammate by name; working teammates receive a steer immediately and idle teammates wake with the message. status is reserved for worker reports to leader.",
|
|
99
101
|
parameters: SendMessageParams,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
// Canonical lifecycle rows (same as packages/monitor): empty call slot,
|
|
103
|
+
// ONE delivery row owned by renderResult.
|
|
104
|
+
renderShell: "self",
|
|
105
|
+
renderCall: () => new Text("", 0, 0),
|
|
106
|
+
renderResult(result, _options, theme, context) {
|
|
107
|
+
const text = result.content[0]?.type === "text" ? (result.content[0] as { type: string; text: string }).text : "";
|
|
108
|
+
if (context.isError) {
|
|
109
|
+
return new Text(theme.fg("error", text.split("\n")[0] || "Failed to send message."), 0, 0);
|
|
110
|
+
}
|
|
111
|
+
const to = String((context.args as { to?: string }).to ?? "");
|
|
112
|
+
const details = result.details as { queued?: boolean; stalledMs?: number } | undefined;
|
|
113
|
+
const suffix = details?.queued ? "queued" : "delivered";
|
|
114
|
+
let line = `${theme.fg("toolTitle", theme.bold(formatAgentMessagePrefix("to")))}${theme.fg("accent", `@${to}`)}${theme.fg("dim", ` · ${suffix}`)}`;
|
|
115
|
+
if (!details?.queued && details?.stalledMs !== undefined) {
|
|
116
|
+
line += theme.fg("dim", ` · stalled ${formatSilenceDuration(details.stalledMs)}`);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
render: (width: number) => width > 0 ? [truncateToWidth(line, width)] : [],
|
|
120
|
+
invalidate: () => {},
|
|
121
|
+
};
|
|
104
122
|
},
|
|
105
123
|
async execute(_toolCallId, params) {
|
|
106
124
|
if (params.to === LEADER_RECIPIENT) throw new Error('The leader cannot send a message to itself.');
|
|
@@ -112,7 +130,12 @@ export function registerLeaderTools(pi: ExtensionAPI): void {
|
|
|
112
130
|
if (!result.queued && result.stalledMs !== undefined) {
|
|
113
131
|
text += `\nWarning: @${params.to} has been stalled with no output for ${formatSilenceDuration(result.stalledMs)} — the control-stream write succeeded, but the teammate may be wedged. Consider teammate_shutdown if it stays silent.`;
|
|
114
132
|
}
|
|
115
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text }],
|
|
135
|
+
details: result.stalledMs !== undefined && !result.queued
|
|
136
|
+
? { queued: result.queued, stalledMs: result.stalledMs }
|
|
137
|
+
: { queued: result.queued },
|
|
138
|
+
};
|
|
116
139
|
},
|
|
117
140
|
});
|
|
118
141
|
|
|
@@ -122,6 +145,22 @@ export function registerLeaderTools(pi: ExtensionAPI): void {
|
|
|
122
145
|
label: "Create Task",
|
|
123
146
|
description: "Create one shared board task. Resident teammates self-claim it when dependencies are met; an optional verify command gates completion.",
|
|
124
147
|
parameters: TaskCreateParams,
|
|
148
|
+
// Canonical lifecycle rows (same as packages/monitor): empty call slot,
|
|
149
|
+
// ONE created row owned by renderResult.
|
|
150
|
+
renderShell: "self",
|
|
151
|
+
renderCall: () => new Text("", 0, 0),
|
|
152
|
+
renderResult(result, _options, theme, context) {
|
|
153
|
+
const text = result.content[0]?.type === "text" ? (result.content[0] as { type: string; text: string }).text : "";
|
|
154
|
+
if (context.isError) {
|
|
155
|
+
return new Text(theme.fg("error", text.split("\n")[0] || "Failed to create task."), 0, 0);
|
|
156
|
+
}
|
|
157
|
+
const subject = String((context.args as { subject?: string }).subject ?? "");
|
|
158
|
+
const line = theme.fg("toolTitle", theme.bold(formatToolEventLabel("created", subject, "board")));
|
|
159
|
+
return {
|
|
160
|
+
render: (width: number) => width > 0 ? [truncateToWidth(line, width)] : [],
|
|
161
|
+
invalidate: () => {},
|
|
162
|
+
};
|
|
163
|
+
},
|
|
125
164
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
126
165
|
const tasks = listTasks();
|
|
127
166
|
if (params.dependsOn?.some((dep) => !tasks.some((task) => task.id === dep))) {
|
|
@@ -137,81 +176,23 @@ export function registerLeaderTools(pi: ExtensionAPI): void {
|
|
|
137
176
|
registerTaskListTool(pi);
|
|
138
177
|
}
|
|
139
178
|
|
|
179
|
+
function teamStatusSummary(): string {
|
|
180
|
+
const roles = [...discoverAgents().keys()].map((name) => `@${name}`);
|
|
181
|
+
const rolesLine = roles.length > 0
|
|
182
|
+
? `${roles.length} persistent agent role${roles.length === 1 ? "" : "s"}: ${roles.join(", ")}`
|
|
183
|
+
: "No agent roles discovered.";
|
|
184
|
+
return `${rosterSummary()}\n${rolesLine}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
140
187
|
export function registerTeamCommand(pi: ExtensionAPI): void {
|
|
141
188
|
pi.registerCommand("agent-teams", {
|
|
142
|
-
description: "Agent Teams
|
|
189
|
+
description: "Agent Teams management console: session teammates and persistent agent roles",
|
|
143
190
|
handler: async (_args, ctx) => {
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (ctx.mode !== "tui") { ctx.ui.notify(rosterSummary(), "info"); return; }
|
|
149
|
-
await openTeamConsole(ctx);
|
|
150
|
-
refreshTeamUI(ctx);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
await createAgentFromHistory(ctx, choice === "local");
|
|
191
|
+
publishStateSnapshot();
|
|
192
|
+
if (ctx.mode !== "tui") { ctx.ui.notify(teamStatusSummary(), "info"); return; }
|
|
193
|
+
await openTeamConsole(ctx);
|
|
194
|
+
refreshTeamUI(ctx);
|
|
154
195
|
},
|
|
155
196
|
});
|
|
156
197
|
}
|
|
157
198
|
|
|
158
|
-
interface GeneratedAgent { name?: string; description?: string; tools?: string[]; prompt?: string; content?: string; }
|
|
159
|
-
|
|
160
|
-
function parseGeneratedAgents(raw: string): GeneratedAgent[] {
|
|
161
|
-
const cleaned = raw.trim().replace(/^```(?:json)?\s*|\s*```$/g, "");
|
|
162
|
-
const start = cleaned.indexOf("[");
|
|
163
|
-
const end = cleaned.lastIndexOf("]");
|
|
164
|
-
if (start < 0 || end <= start) return [];
|
|
165
|
-
try {
|
|
166
|
-
const value: unknown = JSON.parse(cleaned.slice(start, end + 1));
|
|
167
|
-
return Array.isArray(value) ? value.filter((item): item is GeneratedAgent => Boolean(item && typeof item === "object")) : [];
|
|
168
|
-
} catch { return []; }
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function renderAgentDefinition(agent: GeneratedAgent): string {
|
|
172
|
-
const name = agent.name!.trim();
|
|
173
|
-
const description = (agent.description || "Agent created from session history").replace(/[\r\n]+/g, " ").trim();
|
|
174
|
-
const tools = Array.isArray(agent.tools) ? agent.tools.filter((tool) => typeof tool === "string" && tool.trim()).join(", ") : "";
|
|
175
|
-
return `---\nname: ${name}\ndescription: ${description}${tools ? `\ntools: ${tools}` : ""}\n---\n\n${(agent.prompt || "").trim()}`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function createAgentFromHistory(ctx: ExtensionContext, local: boolean): Promise<void> {
|
|
179
|
-
const branch = ctx.sessionManager?.getBranch?.() ?? [];
|
|
180
|
-
const history = branch.map((entry: any) => {
|
|
181
|
-
const text = typeof entry?.text === "string" ? entry.text : typeof entry?.content === "string" ? entry.content : "";
|
|
182
|
-
return text ? text : Array.isArray(entry?.content) ? entry.content.map((part: any) => part?.text ?? "").join("") : "";
|
|
183
|
-
}).filter(Boolean).slice(-20).join("\n\n");
|
|
184
|
-
const generated = await generateAgentPrompt(ctx, history);
|
|
185
|
-
if (!generated) return;
|
|
186
|
-
const dir = path.join(ctx.cwd || process.cwd(), ".pi", "agents");
|
|
187
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
188
|
-
const definitions = parseGeneratedAgents(generated)
|
|
189
|
-
.filter((item) => typeof item.name === "string" && (item.content?.trim() || item.prompt?.trim()))
|
|
190
|
-
.map((item) => ({ name: item.name!.trim(), content: item.content?.trim() || renderAgentDefinition(item) }));
|
|
191
|
-
if (definitions.length === 0) { ctx.ui.notify("Model returned no usable agent definitions", "error"); return; }
|
|
192
|
-
let created = 0;
|
|
193
|
-
for (const definition of definitions) {
|
|
194
|
-
if (!isValidAgentName(definition.name)) continue;
|
|
195
|
-
const filename = `${definition.name}${local ? ".local" : ""}.md`;
|
|
196
|
-
try { fs.writeFileSync(path.join(dir, filename), definition.content, { encoding: "utf8", flag: "wx" }); created++; } catch (error) {
|
|
197
|
-
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
ctx.ui.notify(`Created ${created} agent definition${created === 1 ? "" : "s"}`, created ? "info" : "warning");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function generateAgentPrompt(ctx: ExtensionContext, history: string): Promise<string | undefined> {
|
|
204
|
-
const model = ctx.model;
|
|
205
|
-
const registry = ctx.modelRegistry;
|
|
206
|
-
if (!model || !registry?.complete) {
|
|
207
|
-
ctx.ui.notify("No model available to generate an agent", "error");
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
const response = await registry.complete(model, {
|
|
211
|
-
systemPrompt: "Design a complementary resident coding agent team from session history. Return a JSON array of 2-4 complementary agent definitions. Each item must have name, description, tools, and prompt fields. Names use letters, digits, dots, dashes, underscores. Return JSON only, with no code fences.",
|
|
212
|
-
messages: [{ role: "user", content: `Session history:\n${history || "(empty)"}`, timestamp: Date.now() }],
|
|
213
|
-
}, { maxTokens: 2000, temperature: 0, cacheRetention: "none" });
|
|
214
|
-
const content = response.content?.filter((part: any) => part.type === "text").map((part: any) => part.text).join("\\n").trim();
|
|
215
|
-
if (!content) { ctx.ui.notify("Model returned no agent definition", "error"); return undefined; }
|
|
216
|
-
return content;
|
|
217
|
-
}
|
package/src/types.ts
CHANGED
|
@@ -165,7 +165,7 @@ export interface InboxMessage {
|
|
|
165
165
|
/** Spawn one named resident teammate. */
|
|
166
166
|
export const TeammateSpawnParams = Type.Object({
|
|
167
167
|
name: Type.String({ minLength: 1, description: "Teammate name, unique among living teammates; used for messaging and claiming" }),
|
|
168
|
-
agent: Type.String({ description: "Agent definition name (
|
|
168
|
+
agent: Type.String({ description: "Agent definition name (from user, project, or project-local scopes); its frontmatter pins model and optional worktree isolation" }),
|
|
169
169
|
prompt: Type.Optional(Type.String({ description: "Optional kickoff prompt delivered as the teammate's first turn; omit to let it wait for messages or board claims" })),
|
|
170
170
|
});
|
|
171
171
|
|
package/src/ui.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** Passive team widget and
|
|
1
|
+
/** Passive team widget and the /agent-teams management console
|
|
2
|
+
* (session teammates + persistent agent roles + board). */
|
|
2
3
|
|
|
3
4
|
import { truncateToWidth, Key, matchesKey } from "@earendil-works/pi-tui";
|
|
4
5
|
import { createPiThemeStyle, PI_SPINNER_FRAMES, PI_SPINNER_INTERVAL_MS } from "@fradser/pi-kit";
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
clampConsoleScroll, consoleScrollRange, maxConsoleBody, scrollConsoleDetail, wrapConsoleDetail,
|
|
8
9
|
} from "./console-viewport.ts";
|
|
9
10
|
import { fitTeammateRow, formatTeammateLabel, runningTeammateActivity } from "./activity.ts";
|
|
11
|
+
import { discoverAgents, resolveAgent, type AgentDefinition } from "./agents.ts";
|
|
10
12
|
import { getState, getTeammate, listTasks, listTeammates, livingTeammates } from "./state.ts";
|
|
11
13
|
import {
|
|
12
14
|
ensureLivePoll,
|
|
@@ -36,7 +38,7 @@ function colorFor(name: string): (typeof TEAM_COLORS)[number] {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
function ensureSpinner(): void {
|
|
39
|
-
const working = livingTeammates().some(
|
|
41
|
+
const working = livingTeammates().some(isWorking);
|
|
40
42
|
if (working && !spinnerTimer) {
|
|
41
43
|
spinnerTimer = setInterval(() => {
|
|
42
44
|
spinnerFrame = (spinnerFrame + 1) % PI_SPINNER_FRAMES.length;
|
|
@@ -72,7 +74,7 @@ export function ensureTeamWidget(ctx?: { ui?: ExtensionUIContext; mode?: string
|
|
|
72
74
|
placement: "belowEditor",
|
|
73
75
|
render: (width: number) => {
|
|
74
76
|
// Only WORKING teammates appear above the input box; idle and
|
|
75
|
-
// stopped teammates stay in the /
|
|
77
|
+
// stopped teammates stay in the /agent-teams console instead.
|
|
76
78
|
const working = livingTeammates().filter(isWorking);
|
|
77
79
|
if (working.length === 0) return [];
|
|
78
80
|
const lines: string[] = [];
|
|
@@ -171,6 +173,11 @@ function indent(text: string): string[] {
|
|
|
171
173
|
return text.split("\n").map((line) => ` ${line}`);
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
function displaySource(sourcePath: string): string {
|
|
177
|
+
const relative = path.relative(process.cwd(), sourcePath);
|
|
178
|
+
return relative.startsWith("..") ? sourcePath : `./${relative}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
174
181
|
function buildTeammateDetail(name: string): string[] {
|
|
175
182
|
const teammate = getTeammate(name);
|
|
176
183
|
if (!teammate) return ["(teammate removed from the roster)"];
|
|
@@ -231,6 +238,26 @@ function buildTaskDetail(taskId: string): string[] {
|
|
|
231
238
|
];
|
|
232
239
|
}
|
|
233
240
|
|
|
241
|
+
function buildRoleDetail(name: string): string[] {
|
|
242
|
+
const def = resolveAgent(name);
|
|
243
|
+
if (!def) return ["(agent definition not found)"];
|
|
244
|
+
return [
|
|
245
|
+
`@${def.name} (${def.scope})${def.gitManaged ? " [git-managed]" : " [local]"}`,
|
|
246
|
+
"",
|
|
247
|
+
"== role ==",
|
|
248
|
+
` Source: ${displaySource(def.source)}`,
|
|
249
|
+
` Description: ${def.description || "(none)"}`,
|
|
250
|
+
` Tools: ${def.tools.join(", ") || "(role defaults)"}`,
|
|
251
|
+
` Model: ${def.model ?? "(session default)"}`,
|
|
252
|
+
` Verify: ${def.verify ?? "(none)"}`,
|
|
253
|
+
` Worktree: ${def.worktree ? "true" : "false"}`,
|
|
254
|
+
` Living instances: ${livingTeammates().filter((teammate) => teammate.agent === def.name).length}`,
|
|
255
|
+
"",
|
|
256
|
+
"== role prompt ==",
|
|
257
|
+
...indent(cap(def.prompt)),
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
|
|
234
261
|
// ── Full-screen console ───────────────────────────────────────────
|
|
235
262
|
|
|
236
263
|
type ConsolePage = "roster" | "board";
|
|
@@ -239,11 +266,30 @@ interface RosterRow {
|
|
|
239
266
|
key: string;
|
|
240
267
|
kind: "teammate";
|
|
241
268
|
}
|
|
269
|
+
interface RoleRow {
|
|
270
|
+
key: string;
|
|
271
|
+
kind: "role";
|
|
272
|
+
}
|
|
242
273
|
interface BoardRow {
|
|
243
274
|
key: string;
|
|
244
275
|
kind: "task";
|
|
245
276
|
}
|
|
246
|
-
type ConsoleRow = RosterRow | BoardRow;
|
|
277
|
+
type ConsoleRow = RosterRow | RoleRow | BoardRow;
|
|
278
|
+
|
|
279
|
+
/** SGR mouse-wheel events carry a button code; return signed line delta. */
|
|
280
|
+
function wheelDelta(data: string): number | undefined {
|
|
281
|
+
const match = /^\x1b\[<(\d+);\d+;\d+[Mm]$/.exec(data);
|
|
282
|
+
if (!match) return undefined;
|
|
283
|
+
const button = Number.parseInt(match[1], 10);
|
|
284
|
+
if ((button & 64) === 0) return undefined;
|
|
285
|
+
const direction = button & 3;
|
|
286
|
+
if (direction === 0) return -1;
|
|
287
|
+
if (direction === 1) return 1;
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Chrome lines around the list viewport: border, header, spacer, footer, border. */
|
|
292
|
+
const LIST_CHROME_LINES = 5;
|
|
247
293
|
|
|
248
294
|
/** Full-screen Team Console — owns input via ctx.ui.custom. Pages: roster /
|
|
249
295
|
* board; each row opens a scrolling detail view. */
|
|
@@ -253,16 +299,37 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
253
299
|
let mode: "list" | "detail" = "list";
|
|
254
300
|
let selectedRoster = 0;
|
|
255
301
|
let selectedBoard = 0;
|
|
256
|
-
let
|
|
302
|
+
let listOffset = 0;
|
|
303
|
+
let detailKind: ConsoleRow["kind"] = "teammate";
|
|
304
|
+
let detailKey = "";
|
|
257
305
|
let offset = 0;
|
|
306
|
+
let confirmName: string | undefined;
|
|
258
307
|
let closed = false;
|
|
259
308
|
let renderTimer: ReturnType<typeof setInterval> | undefined;
|
|
309
|
+
|
|
310
|
+
// Definitions are reread lazily so mid-session role creation shows up,
|
|
311
|
+
// but never once per animation frame.
|
|
312
|
+
let rolesCache: AgentDefinition[] = [];
|
|
313
|
+
let rolesCacheAt = 0;
|
|
314
|
+
const ROLES_CACHE_TTL_MS = 2000;
|
|
315
|
+
const getRoles = (): AgentDefinition[] => {
|
|
316
|
+
const now = Date.now();
|
|
317
|
+
if (now - rolesCacheAt > ROLES_CACHE_TTL_MS) {
|
|
318
|
+
rolesCache = [...discoverAgents().values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
319
|
+
rolesCacheAt = now;
|
|
320
|
+
}
|
|
321
|
+
return rolesCache;
|
|
322
|
+
};
|
|
323
|
+
|
|
260
324
|
const requestRender = () => {
|
|
261
325
|
if (!closed) tui.requestRender();
|
|
262
326
|
};
|
|
263
327
|
const startLiveRefresh = () => {
|
|
264
328
|
if (renderTimer) return;
|
|
265
|
-
renderTimer = setInterval(
|
|
329
|
+
renderTimer = setInterval(() => {
|
|
330
|
+
// Redraw only while something can animate (spinner/activity/stall).
|
|
331
|
+
if (livingTeammates().some(isWorking)) requestRender();
|
|
332
|
+
}, PI_SPINNER_INTERVAL_MS);
|
|
266
333
|
renderTimer.unref?.();
|
|
267
334
|
};
|
|
268
335
|
const stopLiveRefresh = () => {
|
|
@@ -276,8 +343,12 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
276
343
|
|
|
277
344
|
const currentRows = (): ConsoleRow[] =>
|
|
278
345
|
page === "roster"
|
|
279
|
-
// The console is the
|
|
280
|
-
|
|
346
|
+
// The console is the management surface: idle and stopped stay
|
|
347
|
+
// visible, and persistent agent roles are listed after the session.
|
|
348
|
+
? [
|
|
349
|
+
...listTeammates().map((t) => ({ key: t.name, kind: "teammate" as const })),
|
|
350
|
+
...getRoles().map((def) => ({ key: def.name, kind: "role" as const })),
|
|
351
|
+
]
|
|
281
352
|
: listTasks().map((task) => ({ key: task.id, kind: "task" as const }));
|
|
282
353
|
|
|
283
354
|
const windowLines = (full: string[], width: number): { lines: string[]; range: string } => {
|
|
@@ -293,48 +364,109 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
293
364
|
const headerLine = (): string => {
|
|
294
365
|
const alive = livingTeammates();
|
|
295
366
|
const tasks = listTasks();
|
|
296
|
-
return `team ${alive.length} alive · board ${tasks.filter((task) => task.status === "pending").length}p/${tasks.filter((task) => task.status === "claimed").length}c/${tasks.filter((task) => task.status === "completed").length}d · ${page}`;
|
|
367
|
+
return `team ${alive.length} alive · ${alive.filter(isWorking).length} working · ${getRoles().length} roles · board ${tasks.filter((task) => task.status === "pending").length}p/${tasks.filter((task) => task.status === "claimed").length}c/${tasks.filter((task) => task.status === "completed").length}d · ${page}`;
|
|
297
368
|
};
|
|
298
369
|
|
|
299
|
-
|
|
370
|
+
interface ContentLine {
|
|
371
|
+
text: string;
|
|
372
|
+
/** Selectable row index, or -1 for section chrome. */
|
|
373
|
+
select: number;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const teammateRowText = (key: string, selected: boolean): string => {
|
|
377
|
+
const teammate = getTeammate(key)!;
|
|
378
|
+
const marker = selected ? style.accent("❯ ") : " ";
|
|
379
|
+
const name = theme.bold(theme.fg(colorFor(teammate.name), `@${teammate.name}`));
|
|
380
|
+
if (isWorking(teammate)) {
|
|
381
|
+
const prefix = `${marker}${name} `;
|
|
382
|
+
const width = tui.terminal.columns;
|
|
383
|
+
const available = Math.max(0, width - prefix.length - PI_SPINNER_FRAMES[spinnerFrame].length - 2);
|
|
384
|
+
const status = theme.fg("warning", formatTeammateLabel(PI_SPINNER_FRAMES[spinnerFrame], runningTeammateActivity(teammate) + stallSuffix(teammate), available));
|
|
385
|
+
return `${prefix}${status}`;
|
|
386
|
+
}
|
|
387
|
+
if (teammate.status === "idle") {
|
|
388
|
+
return `${marker}${name} ${style.dim(`○ idle${teammate.currentTaskId ? ` · ${teammate.currentTaskId}` : ""}`)}`;
|
|
389
|
+
}
|
|
390
|
+
return `${marker}${name} ${theme.fg("warning", "■ stopped")}`;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const roleRowText = (key: string, selected: boolean): string => {
|
|
394
|
+
const def = getRoles().find((candidate) => candidate.name === key)!;
|
|
395
|
+
const marker = selected ? style.accent("❯ ") : " ";
|
|
396
|
+
const name = theme.bold(theme.fg(colorFor(key), `@${key}`));
|
|
397
|
+
const live = livingTeammates().filter((teammate) => teammate.agent === key && teammate.status !== "stopped").length;
|
|
398
|
+
const liveTag = live > 0 ? style.success(`[${live} live]`) : style.dim("[0 live]");
|
|
399
|
+
return `${marker}${name} ${style.dim(def.scope)} ${style.dim(displaySource(def.source))} ${liveTag}`;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const taskRowText = (key: string, selected: boolean): string => {
|
|
403
|
+
const task = listTasks().find((candidate) => candidate.id === key)!;
|
|
404
|
+
const marker = selected ? style.accent("❯ ") : " ";
|
|
405
|
+
const label = theme.fg(colorFor(key), `[${key}]`);
|
|
406
|
+
const holder = task.claimedBy ? style.dim(` @${task.claimedBy}`) : "";
|
|
407
|
+
const statusText = task.status === "completed"
|
|
408
|
+
? style.success("✓")
|
|
409
|
+
: task.status === "claimed"
|
|
410
|
+
? theme.fg("warning", "◐ claimed")
|
|
411
|
+
: style.dim("○ pending");
|
|
412
|
+
return `${marker}${label} ${statusText} ${theme.fg("customMessageText", task.subject)}${holder}`;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const buildContent = (): ContentLine[] => {
|
|
300
416
|
const rows = currentRows();
|
|
417
|
+
const lines: ContentLine[] = [];
|
|
418
|
+
if (page === "board") {
|
|
419
|
+
if (rows.length === 0) {
|
|
420
|
+
lines.push({ text: style.dim("The task board is empty. Create tasks with task_create."), select: -1 });
|
|
421
|
+
}
|
|
422
|
+
rows.forEach((row, index) => lines.push({ text: taskRowText(row.key, index === currentPageSelection(rows.length)), select: index }));
|
|
423
|
+
return lines;
|
|
424
|
+
}
|
|
425
|
+
const teammates = rows.filter((row): row is RosterRow => row.kind === "teammate");
|
|
426
|
+
const roles = rows.filter((row): row is RoleRow => row.kind === "role");
|
|
427
|
+
const selection = currentPageSelection(rows.length);
|
|
428
|
+
lines.push({ text: style.dim("== teammates (this session) =="), select: -1 });
|
|
429
|
+
if (teammates.length === 0) {
|
|
430
|
+
lines.push({ text: style.dim("No teammates this session. Ask the model to spawn one."), select: -1 });
|
|
431
|
+
}
|
|
432
|
+
teammates.forEach((row, index) => lines.push({ text: teammateRowText(row.key, index === selection), select: index }));
|
|
433
|
+
lines.push({ text: "", select: -1 });
|
|
434
|
+
lines.push({ text: style.dim("== agent roles (persistent definitions) =="), select: -1 });
|
|
435
|
+
if (roles.length === 0) {
|
|
436
|
+
lines.push({ text: style.dim("No agent definitions discovered."), select: -1 });
|
|
437
|
+
}
|
|
438
|
+
roles.forEach((row, index) => lines.push({ text: roleRowText(row.key, teammates.length + index === selection), select: teammates.length + index }));
|
|
439
|
+
return lines;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const listViewport = (): number => Math.max(1, tui.terminal.rows - LIST_CHROME_LINES);
|
|
443
|
+
|
|
444
|
+
const renderList = (width: number): string[] => {
|
|
301
445
|
const border = style.border("─".repeat(Math.max(1, width)));
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
446
|
+
const content = buildContent();
|
|
447
|
+
const viewport = listViewport();
|
|
448
|
+
const selection = currentPageSelection(currentRows().length);
|
|
449
|
+
const selectedIndex = content.findIndex((line) => line.select === selection);
|
|
450
|
+
if (selectedIndex >= 0) {
|
|
451
|
+
if (selectedIndex < listOffset) listOffset = selectedIndex;
|
|
452
|
+
else if (selectedIndex >= listOffset + viewport) listOffset = selectedIndex - viewport + 1;
|
|
307
453
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
} else {
|
|
325
|
-
const task = listTasks().find((candidate) => candidate.id === row.key)!;
|
|
326
|
-
const label = theme.fg(colorFor(task.id), `[${task.id}]`);
|
|
327
|
-
const holder = task.claimedBy ? style.dim(` @${task.claimedBy}`) : "";
|
|
328
|
-
const statusText = task.status === "completed"
|
|
329
|
-
? style.success("✓")
|
|
330
|
-
: task.status === "claimed"
|
|
331
|
-
? theme.fg("warning", "◐ claimed")
|
|
332
|
-
: style.dim("○ pending");
|
|
333
|
-
lines.push(truncateToWidth(`${marker}${label} ${statusText} ${theme.fg("customMessageText", task.subject)}${holder}`, Math.max(10, width - 1)));
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
lines.push("", style.dim("↑↓ select · enter open · tab page · x shutdown · esc/q close"), border);
|
|
337
|
-
return lines.map((l) => truncateToWidth(l, Math.max(10, width - 1)));
|
|
454
|
+
listOffset = clampConsoleScroll(listOffset, content.length, viewport);
|
|
455
|
+
const footer = confirmName
|
|
456
|
+
? theme.fg("warning", `Shut down @${confirmName}? Its claimed task returns to the board · y yes / n no`)
|
|
457
|
+
: style.dim(page === "roster"
|
|
458
|
+
? "↑↓ select · enter open · tab board · x shutdown · esc/q close"
|
|
459
|
+
: "↑↓ select · enter open · tab roster · esc/q close");
|
|
460
|
+
return [
|
|
461
|
+
border,
|
|
462
|
+
style.accent(truncateToWidth(headerLine(), width)),
|
|
463
|
+
"",
|
|
464
|
+
...content.slice(listOffset, listOffset + viewport)
|
|
465
|
+
.map((line) => truncateToWidth(line.text, Math.max(10, width - 1))),
|
|
466
|
+
"",
|
|
467
|
+
truncateToWidth(footer, Math.max(10, width - 1)),
|
|
468
|
+
border,
|
|
469
|
+
];
|
|
338
470
|
};
|
|
339
471
|
|
|
340
472
|
const currentPageSelection = (rowCount: number): number => {
|
|
@@ -342,6 +474,9 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
342
474
|
return rowCount === 0 ? 0 : Math.min(selection, rowCount - 1);
|
|
343
475
|
};
|
|
344
476
|
|
|
477
|
+
const detailTitle = (): string =>
|
|
478
|
+
detailKind === "task" ? `[${detailKey}]` : detailKind === "role" ? `@${detailKey} · role` : `@${detailKey}`;
|
|
479
|
+
|
|
345
480
|
const renderDetail = (width: number): string[] => {
|
|
346
481
|
const border = style.border("─".repeat(Math.max(1, width)));
|
|
347
482
|
const source = detailSource();
|
|
@@ -349,7 +484,7 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
349
484
|
const footer = style.dim(` ${detail.range} · ↑↓ scroll · pgup/pgdn page · home/end jump · esc back · q close`);
|
|
350
485
|
const lines = [
|
|
351
486
|
border,
|
|
352
|
-
style.accent(truncateToWidth(`agent-teams ${detailTitle}`, width)),
|
|
487
|
+
style.accent(truncateToWidth(`agent-teams ${detailTitle()}`, width)),
|
|
353
488
|
"",
|
|
354
489
|
...detail.lines.map((line) => ` ${line}`),
|
|
355
490
|
"",
|
|
@@ -359,10 +494,11 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
359
494
|
return lines.map((line) => truncateToWidth(line, Math.max(10, width - 1)));
|
|
360
495
|
};
|
|
361
496
|
|
|
362
|
-
const detailSource = (): string[] =>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
497
|
+
const detailSource = (): string[] => {
|
|
498
|
+
if (detailKind === "task") return buildTaskDetail(detailKey);
|
|
499
|
+
if (detailKind === "role") return buildRoleDetail(detailKey);
|
|
500
|
+
return buildTeammateDetail(detailKey);
|
|
501
|
+
};
|
|
366
502
|
|
|
367
503
|
return {
|
|
368
504
|
render: (width) => (mode === "list" ? renderList(width) : renderDetail(width)),
|
|
@@ -371,9 +507,22 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
371
507
|
handleDetailInput(data);
|
|
372
508
|
return;
|
|
373
509
|
}
|
|
510
|
+
if (confirmName) {
|
|
511
|
+
if (data === "y" || data === "Y") {
|
|
512
|
+
const name = confirmName;
|
|
513
|
+
confirmName = undefined;
|
|
514
|
+
void shutdownFromConsole(ctx, name);
|
|
515
|
+
} else if (data === "n" || data === "N" || matchesKey(data, Key.escape)) {
|
|
516
|
+
confirmName = undefined;
|
|
517
|
+
} else if (data === "q" || data === "Q") {
|
|
518
|
+
closeConsole(done);
|
|
519
|
+
}
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
374
522
|
const rows = currentRows();
|
|
375
523
|
if (matchesKey(data, Key.tab)) {
|
|
376
524
|
page = page === "roster" ? "board" : "roster";
|
|
525
|
+
listOffset = 0;
|
|
377
526
|
return;
|
|
378
527
|
}
|
|
379
528
|
if (matchesKey(data, Key.down)) bumpSelection(1, rows.length);
|
|
@@ -384,20 +533,29 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
384
533
|
if (row) {
|
|
385
534
|
mode = "detail";
|
|
386
535
|
offset = 0;
|
|
387
|
-
|
|
536
|
+
detailKind = row.kind;
|
|
537
|
+
detailKey = row.key;
|
|
388
538
|
}
|
|
389
539
|
return;
|
|
390
540
|
}
|
|
391
541
|
if (data === "x" || data === "X") {
|
|
392
542
|
const selection = currentPageSelection(rows.length);
|
|
393
543
|
const row = rows[selection];
|
|
394
|
-
if (page === "roster" && row?.kind === "teammate"
|
|
544
|
+
if (page === "roster" && row?.kind === "teammate" && getTeammate(row.key)?.status !== "stopped") {
|
|
545
|
+
confirmName = row.key;
|
|
546
|
+
}
|
|
395
547
|
return;
|
|
396
548
|
}
|
|
397
|
-
|
|
398
|
-
|
|
549
|
+
const wheel = wheelDelta(data);
|
|
550
|
+
if (wheel !== undefined) {
|
|
551
|
+
scrollList(wheel * 3);
|
|
399
552
|
return;
|
|
400
553
|
}
|
|
554
|
+
if (matchesKey(data, Key.pageUp)) scrollList(-listViewport());
|
|
555
|
+
else if (matchesKey(data, Key.pageDown)) scrollList(listViewport());
|
|
556
|
+
else if (matchesKey(data, Key.home)) listOffset = 0;
|
|
557
|
+
else if (matchesKey(data, Key.end)) listOffset = clampConsoleScroll(Number.MAX_SAFE_INTEGER, buildContent().length, listViewport());
|
|
558
|
+
else if (matchesKey(data, Key.escape) || data === "q" || data === "Q") closeConsole(done);
|
|
401
559
|
},
|
|
402
560
|
invalidate: () => requestRender(),
|
|
403
561
|
dispose: () => {
|
|
@@ -418,6 +576,10 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
418
576
|
else selectedBoard = clampIndex(selectedBoard + delta, rowCount);
|
|
419
577
|
}
|
|
420
578
|
|
|
579
|
+
function scrollList(delta: number): void {
|
|
580
|
+
listOffset = clampConsoleScroll(listOffset + delta, buildContent().length, listViewport());
|
|
581
|
+
}
|
|
582
|
+
|
|
421
583
|
function handleDetailInput(data: string): void {
|
|
422
584
|
const source = detailSource();
|
|
423
585
|
const viewport = maxConsoleBody(tui.terminal.rows);
|
|
@@ -431,14 +593,9 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
431
593
|
return;
|
|
432
594
|
}
|
|
433
595
|
const total = wrapConsoleDetail(source, tui.terminal.columns).length;
|
|
434
|
-
const
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
if ((button & 64) !== 0) {
|
|
438
|
-
const direction = button & 3;
|
|
439
|
-
if (direction === 0) offset = scrollConsoleDetail(offset, -3, total, viewport);
|
|
440
|
-
else if (direction === 1) offset = scrollConsoleDetail(offset, 3, total, viewport);
|
|
441
|
-
}
|
|
596
|
+
const wheel = wheelDelta(data);
|
|
597
|
+
if (wheel !== undefined) {
|
|
598
|
+
offset = scrollConsoleDetail(offset, wheel * 3, total, viewport);
|
|
442
599
|
return;
|
|
443
600
|
}
|
|
444
601
|
if (matchesKey(data, Key.up)) offset = scrollConsoleDetail(offset, -1, total, viewport);
|
package/src/worker.ts
CHANGED
|
@@ -119,7 +119,9 @@ export function registerWorkerCapabilities(pi: ExtensionAPI): void {
|
|
|
119
119
|
body: params.message,
|
|
120
120
|
status: params.status,
|
|
121
121
|
});
|
|
122
|
-
return { content: [{ type: "text", text:
|
|
122
|
+
return { content: [{ type: "text", text: params.status
|
|
123
|
+
? "Queued message to leader."
|
|
124
|
+
: 'Queued message to leader. Note: without status="completed" or status="failed" this report does not end your assignment.' }], details: {} };
|
|
123
125
|
}
|
|
124
126
|
if (params.status) throw new Error('status is valid only when to="leader".');
|
|
125
127
|
if (params.to === binding.worker) throw new Error("You are already the recipient — no need to message yourself.");
|
package/agents/observer.md
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: observer
|
|
3
|
-
description: Read-only monitor of activity, logs, or processes; use when the task is to observe, watch, or report state rather than change it
|
|
4
|
-
tools: read
|
|
5
|
-
---
|
|
6
|
-
You are an observer agent. Observe and report on the assigned scope without
|
|
7
|
-
changing anything. Take the required snapshots or readings, summarize what you
|
|
8
|
-
observed, and report notable events or anomalies with timestamps and evidence
|
|
9
|
-
via send_message(to="leader", message=...) with status="completed". You have no edit,
|
|
10
|
-
write, or command tools.
|
package/agents/reviewer.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: reviewer
|
|
3
|
-
description: Read-only code or content reviewer; use after implementation or plan changes to check for correctness, regressions, and security
|
|
4
|
-
tools: read,bash
|
|
5
|
-
---
|
|
6
|
-
You are a reviewer agent. Read the relevant code and tests first, then review
|
|
7
|
-
the assigned scope for correctness, regressions, and security problems. Do not
|
|
8
|
-
edit files. Report only confirmed findings with severity, evidence, exact
|
|
9
|
-
paths, and a minimal fix recommendation via send_message(to="leader",
|
|
10
|
-
message=...) with status="completed". Your LAST message to the leader MUST
|
|
11
|
-
carry status="completed" (or status="failed"); a review without a terminal
|
|
12
|
-
status is an unfinished review. When no issue is confirmed, say so and list the checks
|
|
13
|
-
you performed.
|
package/agents/specialist.md
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: specialist
|
|
3
|
-
description: Read-only domain expert for a specific technical area (security, performance, data, platform); use when a task needs deep domain judgment rather than broad implementation
|
|
4
|
-
tools: read,bash
|
|
5
|
-
---
|
|
6
|
-
You are a specialist agent. Apply deep domain judgment to the assigned scope:
|
|
7
|
-
investigate, analyze, and produce findings or recommendations with evidence via
|
|
8
|
-
send_message(to="leader", message=...) with status="completed". Do not edit files.
|
|
9
|
-
State your assumptions, the evidence you used, and the bounds of your expertise
|
|
10
|
-
for this task.
|
package/agents/worker.md
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: worker
|
|
3
|
-
description: General executor that implements changes and verifies them; use when a task needs code or file modifications, or when no more specific agent fits
|
|
4
|
-
tools: read,bash,edit,write
|
|
5
|
-
---
|
|
6
|
-
You are a worker agent. Implement the assigned task end to end within your
|
|
7
|
-
agent tool scope, then verify your own work by running the
|
|
8
|
-
relevant tests or commands. Follow the task's ordered procedure. Report what
|
|
9
|
-
you changed, what you verified, and any risks or follow-up work via
|
|
10
|
-
send_message(to="leader", message=...) with status="completed". Do not touch
|
|
11
|
-
files outside the assigned scope.
|