@fradser/pi-agent-teams 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.memory/MEMORY.md +1 -1
- package/.memory/follow-up-queue.md +8 -11
- package/.memory/team-centric-design.md +18 -14
- package/README.md +119 -161
- package/package.json +2 -2
- package/references/agent-roles.md +57 -12
- package/src/activity.ts +12 -33
- package/src/agent-actions.ts +101 -0
- package/src/agents.ts +1 -6
- package/src/guidance.ts +114 -199
- package/src/index.ts +117 -101
- package/src/leader-reports.ts +92 -0
- package/src/recipient.ts +44 -0
- package/src/spawner.ts +293 -38
- package/src/state.ts +250 -69
- package/src/statefile.ts +74 -31
- package/src/team-machine.ts +984 -424
- package/src/tool-copy.ts +374 -0
- package/src/tool-render.ts +47 -22
- package/src/tools.ts +182 -160
- package/src/types.ts +104 -65
- package/src/ui.ts +32 -56
- package/src/work-context.ts +88 -0
- package/src/worker-tools.ts +4 -0
- package/src/worker.ts +251 -184
- package/src/follow-up-queue.ts +0 -301
package/src/tools.ts
CHANGED
|
@@ -1,221 +1,242 @@
|
|
|
1
|
-
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import { detailField, eventToolLifecycle, formatAgentTaskName, formatToolLifecycleTitle, notifyPi } from "@fradser/pi-kit";
|
|
1
|
+
import { truncateHead, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { notifyPi } from "@fradser/pi-kit";
|
|
4
3
|
import {
|
|
4
|
+
assignExistingWork,
|
|
5
|
+
releaseExistingWork,
|
|
6
|
+
reopenExistingWork,
|
|
5
7
|
createBoardTask,
|
|
6
|
-
formatBoardTaskCreation,
|
|
7
|
-
hasAnnouncedFinish,
|
|
8
|
-
hasTerminalReport,
|
|
9
8
|
publishStateSnapshot,
|
|
10
9
|
sendLeaderMessage,
|
|
11
|
-
|
|
10
|
+
shutdownTeammateExact,
|
|
12
11
|
spawnTeammate,
|
|
13
12
|
} from "./team-machine.ts";
|
|
14
13
|
import { listTasks, livingTeammates } from "./state.ts";
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
let leaderToolApi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools"> | undefined;
|
|
18
|
-
|
|
19
|
-
/** Keep spawn as the entry point; reveal follow-on controls only when relevant. */
|
|
20
|
-
export function refreshLeaderToolDisclosure(): void {
|
|
21
|
-
if (!leaderToolApi || typeof leaderToolApi.getActiveTools !== "function" || typeof leaderToolApi.setActiveTools !== "function") return;
|
|
22
|
-
const active = leaderToolApi.getActiveTools();
|
|
23
|
-
const withoutDynamic = active.filter((tool) => !DYNAMIC_LEADER_TOOLS.includes(tool as typeof DYNAMIC_LEADER_TOOLS[number]));
|
|
24
|
-
const revealed = [
|
|
25
|
-
...(livingTeammates().length > 0 ? ["teammate_shutdown", "send_message"] : []),
|
|
26
|
-
...(listTasks().length > 0 ? ["task_list"] : []),
|
|
27
|
-
];
|
|
28
|
-
leaderToolApi.setActiveTools([...withoutDynamic, ...revealed]);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
import { LEADER_RECIPIENT, SendMessageParams, TeammateShutdownParams, TeammateSpawnParams, TaskCreateParams } from "./types.ts";
|
|
32
|
-
import { registerTaskListTool } from "./worker.ts";
|
|
15
|
+
import { AgentActionParams, LEADER_RECIPIENT, AgentEventParams, WorkToolParams } from "./types.ts";
|
|
33
16
|
import { openTeamConsole, refreshTeamUI } from "./ui.ts";
|
|
34
17
|
import { discoverAgents } from "./agents.ts";
|
|
35
|
-
import {
|
|
18
|
+
import { agentRow, leaderWorkRow, messageRow, type AgentRowArgs } from "./tool-copy.ts";
|
|
19
|
+
import { emptyToolCall, renderCoordinationRow } from "./tool-render.ts";
|
|
20
|
+
import { runAgentAction, type AgentActionRuntime } from "./agent-actions.ts";
|
|
21
|
+
import { resolveRecipient } from "./recipient.ts";
|
|
36
22
|
|
|
37
|
-
function
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
function formatWorkList(tasks: ReturnType<typeof listTasks>): string {
|
|
24
|
+
const works = tasks.map((task) => {
|
|
25
|
+
const dependencies = task.dependsOn.length > 0 ? ` · depends=${task.dependsOn.join(",")}` : "";
|
|
26
|
+
const holder = task.claimedBy ? ` · owner=@${task.claimedBy}` : "";
|
|
27
|
+
const result = task.result ? truncateHead(task.result, { maxBytes: 4096, maxLines: 40 }) : undefined;
|
|
28
|
+
const evidence = result ? `\n RESULT · ${result.content}${result.truncated ? "\n [Result preview truncated; complete evidence is retained in Work details.]" : ""}` : "";
|
|
29
|
+
return `- ${task.id} · ${task.status} · ${task.subject}${holder}${dependencies}${evidence}`;
|
|
30
|
+
});
|
|
31
|
+
return [
|
|
32
|
+
"WORK · current session",
|
|
33
|
+
`SUMMARY · ${tasks.length} work item${tasks.length === 1 ? "" : "s"}`,
|
|
34
|
+
"WORK ITEMS",
|
|
35
|
+
...(works.length > 0 ? works : ["(none)"]),
|
|
36
|
+
].join("\n");
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
function formatWorkCreation(subject: string, created: {
|
|
40
|
+
id: string;
|
|
41
|
+
claimable: boolean;
|
|
42
|
+
resourceBlocked: boolean;
|
|
43
|
+
notifiedTeammates: string[];
|
|
44
|
+
}): string {
|
|
45
|
+
const availability = created.claimable ? "pending/claimable" : "pending/blocked";
|
|
46
|
+
const routing = created.notifiedTeammates.length > 0
|
|
47
|
+
? `eligible residents notified: ${created.notifiedTeammates.map((name) => `@${name}`).join(", ")}`
|
|
48
|
+
: created.resourceBlocked
|
|
49
|
+
? "resource conflict blocks assignment"
|
|
50
|
+
: "no eligible resident notified";
|
|
51
|
+
return [
|
|
52
|
+
"WORK · current session",
|
|
53
|
+
`CREATED · ${created.id} · ${availability} · ${subject}`,
|
|
54
|
+
`ROUTING · ${routing}`,
|
|
55
|
+
].join("\n");
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
export function registerLeaderTools(pi: ExtensionAPI): void {
|
|
56
|
-
pi.registerTool({
|
|
57
|
-
name: "teammate_spawn",
|
|
58
|
-
promptSnippet: "Spawn a named resident teammate or sub-agent",
|
|
59
|
-
label: "Spawn Teammate",
|
|
60
|
-
description: "Spawn one named resident teammate / sub-agent. Use this tool whenever instructions, third-party skills, or workflows ask to launch or delegate work to an agent, sub-agent (subagent), worker, or teammate. Generated role definitions stay in memory by default; persist one only when the user explicitly asks to keep it for future sessions.",
|
|
61
|
-
parameters: TeammateSpawnParams,
|
|
62
|
-
renderShell: "self",
|
|
63
|
-
renderCall: emptyToolCall,
|
|
64
|
-
renderResult(result, _options, theme, context) {
|
|
65
|
-
const text = result.content.find((part) => part.type === "text")?.text ?? "";
|
|
66
|
-
if (context.isError) return new Text(theme.fg("error", text.split("\n")[0] || "Failed to spawn teammate."), 0, 0);
|
|
67
|
-
const params = context.args as { name: string; agent: string; prompt?: string };
|
|
68
|
-
const title = formatToolLifecycleTitle({
|
|
69
|
-
kind: "started",
|
|
70
|
-
tool: "agent",
|
|
71
|
-
subject: `@${params.name} started · ${spawnAssignment(params)}`,
|
|
72
|
-
});
|
|
73
|
-
return new Text(theme.fg("customMessageLabel", theme.bold(title)), 0, 0);
|
|
74
|
-
},
|
|
75
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
76
|
-
const result = spawnTeammate(params);
|
|
77
|
-
if (!result.ok) throw new Error(result.error);
|
|
78
|
-
refreshTeamUI(ctx);
|
|
79
|
-
refreshLeaderToolDisclosure();
|
|
80
|
-
const kickoffNote = params.prompt?.trim()
|
|
81
|
-
? "It received your kickoff prompt and is working on it."
|
|
82
|
-
: "It received the standard board-check kickoff and is running its first turn; it idles once that settles.";
|
|
83
|
-
return {
|
|
84
|
-
content: [{ type: "text", text: `@${params.name} is alive as ${params.agent}.\n${kickoffNote}\n\n${rosterSummary()}` }],
|
|
85
|
-
details: { started: true },
|
|
86
|
-
};
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
|
|
58
|
+
export function registerLeaderTools(pi: ExtensionAPI, runtime: AgentActionRuntime & { sendLeaderMessage: typeof sendLeaderMessage } = { spawnTeammate, shutdownTeammateExact, sendLeaderMessage }): void {
|
|
90
59
|
pi.registerTool({
|
|
91
|
-
name: "
|
|
92
|
-
promptSnippet: "
|
|
93
|
-
label: "
|
|
94
|
-
description: "
|
|
95
|
-
|
|
60
|
+
name: "agent",
|
|
61
|
+
promptSnippet: "Delegate, start, inspect, or stop an Agent session",
|
|
62
|
+
label: "Agent",
|
|
63
|
+
description: "Strict Agent lifecycle interface. Delegate creates independent Work; start creates an unassigned resident; inspect and stop use incarnation-bound session handles. Results arrive automatically; inspect is for deliberate diagnosis, not waiting.",
|
|
64
|
+
promptGuidelines: ["After agent delegation, results arrive automatically. Continue independent work or end the turn; do not wait with sleep, repeated agent inspect, or work list polling."],
|
|
65
|
+
parameters: AgentActionParams,
|
|
96
66
|
renderShell: "self",
|
|
97
67
|
renderCall: emptyToolCall,
|
|
98
68
|
renderResult(result, options, theme, context) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// report is queued to); a second event row is noise.
|
|
102
|
-
if (hasAnnouncedFinish(name) || hasTerminalReport(name)) return { render: () => [], invalidate: () => {} };
|
|
103
|
-
return renderLifecycleResult(result, options, theme, context, eventToolLifecycle(
|
|
69
|
+
return renderCoordinationRow(
|
|
70
|
+
result, options, theme, context,
|
|
104
71
|
"agent",
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
));
|
|
72
|
+
agentRow(context.args as AgentRowArgs, result.details, { isError: context.isError, isPartial: options.isPartial }),
|
|
73
|
+
);
|
|
108
74
|
},
|
|
109
75
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
110
|
-
const result = await
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return { content: [{ type: "text", text: result
|
|
76
|
+
const result = await runAgentAction(params, ctx.cwd, runtime, ctx.sessionManager);
|
|
77
|
+
if (params.action !== "inspect") {
|
|
78
|
+
refreshTeamUI(ctx);
|
|
79
|
+
}
|
|
80
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
115
81
|
},
|
|
116
82
|
});
|
|
117
83
|
|
|
118
84
|
pi.registerTool({
|
|
119
|
-
name: "
|
|
120
|
-
promptSnippet: "Send
|
|
121
|
-
label: "
|
|
122
|
-
description: "
|
|
123
|
-
parameters:
|
|
124
|
-
// Canonical lifecycle rows (same as packages/monitor): empty call slot,
|
|
125
|
-
// ONE delivery row owned by renderResult.
|
|
85
|
+
name: "agent_event",
|
|
86
|
+
promptSnippet: "Send an event or message to a participant",
|
|
87
|
+
label: "Agent Event",
|
|
88
|
+
description: "Shared communication interface across Leader, Worker, and Peers. Leader-to-Agent messages carry only new evidence, changed constraints, or decisions — never kickoff echoes, progress requests, completion requests, or confirmation probes.",
|
|
89
|
+
parameters: AgentEventParams,
|
|
126
90
|
renderShell: "self",
|
|
127
91
|
renderCall: emptyToolCall,
|
|
128
92
|
renderResult(result, options, theme, context) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const subject = outcome === "not-sent" ? "terminal report available" : outcome;
|
|
132
|
-
return renderLifecycleResult(result, options, theme, context, eventToolLifecycle(
|
|
93
|
+
return renderCoordinationRow(
|
|
94
|
+
result, options, theme, context,
|
|
133
95
|
"message",
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
));
|
|
96
|
+
messageRow(context.args as { to?: string; message?: string }, result.details, { isError: context.isError }),
|
|
97
|
+
);
|
|
137
98
|
},
|
|
138
99
|
async execute(_toolCallId, params) {
|
|
139
|
-
if (params.to
|
|
140
|
-
|
|
141
|
-
reopen: params.reopen,
|
|
142
|
-
resources: params.resources,
|
|
143
|
-
});
|
|
144
|
-
if (!result.ok) throw new Error(result.error);
|
|
145
|
-
if (result.outcome === "not-sent") {
|
|
146
|
-
return {
|
|
147
|
-
content: [{
|
|
148
|
-
type: "text",
|
|
149
|
-
text: [
|
|
150
|
-
"ROUTING · not sent",
|
|
151
|
-
"NEXT · No new message was delivered. Read the recorded report below; use reopen=true only for a distinct new assignment.",
|
|
152
|
-
`NOTE · @${params.to} already sent a terminal report. Delivery to your context is automatic; asking it to resend produces a duplicate leader turn.`,
|
|
153
|
-
`RECORDED TERMINAL REPORT · ${result.terminalReport}`,
|
|
154
|
-
].join("\n"),
|
|
155
|
-
}],
|
|
156
|
-
details: { outcome: "not-sent", terminalReportAvailable: true },
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
const action = result.outcome === "steered"
|
|
160
|
-
? "active control stream accepted the steer"
|
|
161
|
-
: "harness will deliver the queued message on the next wake-up";
|
|
162
|
-
let text = `MESSAGING\n${result.outcome.toUpperCase()} · to=@${params.to}\nNEXT · ${action}`;
|
|
163
|
-
// A stray status field (copied from worker report patterns) must not
|
|
164
|
-
// block delivery — it carries no meaning on leader-sent messages.
|
|
165
|
-
if (params.status) text += `\nNOTE · status ignored for leader-directed steering`;
|
|
166
|
-
if (result.priorTerminalReport) {
|
|
167
|
-
text += `\nNOTE · @${params.to} already sent a terminal report (below); it reaches your context automatically. A steer asking it to repeat that report produces a duplicate delivery — use reopen only for a distinct new assignment.`;
|
|
168
|
-
text += `\nPRIOR TERMINAL REPORT · ${result.priorTerminalReport}`;
|
|
100
|
+
if (!params.to) {
|
|
101
|
+
throw new Error("No bound reply route exists. Please specify 'to' explicitly.");
|
|
169
102
|
}
|
|
103
|
+
if (params.to === LEADER_RECIPIENT) throw new Error("The leader cannot send an event to itself.");
|
|
104
|
+
const to = resolveRecipient(params.to, livingTeammates()).name;
|
|
105
|
+
const result = runtime.sendLeaderMessage(to, params.message, {});
|
|
106
|
+
if (!result.ok) throw new Error(result.error);
|
|
107
|
+
const recorded = result.outcome === "not-sent" ? `\nRECORDED TERMINAL REPORT · ${result.terminalReport}` : "";
|
|
108
|
+
const next = "";
|
|
170
109
|
return {
|
|
171
|
-
content: [{ type: "text", text }],
|
|
172
|
-
details: { outcome: result.outcome },
|
|
110
|
+
content: [{ type: "text", text: `EVENT ROUTING · ${result.outcome} · to=@${to}\nINTENT · ${params.intent ?? "inform"}${recorded}${next}` }],
|
|
111
|
+
details: { to, outcome: result.outcome, intent: params.intent ?? "inform" },
|
|
173
112
|
};
|
|
174
113
|
},
|
|
175
114
|
});
|
|
176
115
|
|
|
177
116
|
pi.registerTool({
|
|
178
|
-
name: "
|
|
179
|
-
promptSnippet: "Create
|
|
180
|
-
label: "
|
|
181
|
-
description: "
|
|
182
|
-
parameters:
|
|
183
|
-
// Canonical lifecycle rows (same as packages/monitor): empty call slot,
|
|
184
|
-
// ONE created row owned by renderResult.
|
|
117
|
+
name: "work",
|
|
118
|
+
promptSnippet: "Create, list, assign, release, reopen, or supersede Work Items",
|
|
119
|
+
label: "Work",
|
|
120
|
+
description: "Manage Work Items: create (subject), list, assign (id, target.session), release (id, reason), reopen (id, reason), or supersede (subject, supersedes). Creation uses the session's single-writer Work state and never starts a resident.",
|
|
121
|
+
parameters: WorkToolParams,
|
|
185
122
|
renderShell: "self",
|
|
186
123
|
renderCall: emptyToolCall,
|
|
187
124
|
renderResult(result, options, theme, context) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
"
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
));
|
|
125
|
+
return renderCoordinationRow(
|
|
126
|
+
result, options, theme, context,
|
|
127
|
+
"work",
|
|
128
|
+
leaderWorkRow(context.args as Record<string, unknown>, result.details, { isError: context.isError }),
|
|
129
|
+
);
|
|
194
130
|
},
|
|
195
131
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
132
|
+
if (params.action === "supersede") {
|
|
133
|
+
const tasks = listTasks();
|
|
134
|
+
const referenced = [...(params.dependsOn ?? []), ...params.supersedes];
|
|
135
|
+
if (referenced.some((id) => !tasks.some((task) => task.id === id))) {
|
|
136
|
+
throw new Error(`Unknown work id in [${referenced.join(", ")}].`);
|
|
137
|
+
}
|
|
138
|
+
const created = createBoardTask(params);
|
|
139
|
+
if (!created.ok) throw new Error(created.error);
|
|
140
|
+
const work = listTasks().find((task) => task.id === created.id);
|
|
141
|
+
if (!work) throw new Error(`Replacement Work Item "${created.id}" is unavailable.`);
|
|
142
|
+
refreshTeamUI(ctx);
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text", text: `WORK · current session\nSUPERSEDED · ${work.id} · ${work.status}\nREPLACED · ${created.supersededTaskIds.join(", ")}\nROUTING · ${created.notifiedTeammates.length > 0 ? `eligible residents notified: ${created.notifiedTeammates.map((name) => `@${name}`).join(", ")}` : "no eligible resident notified"}` }],
|
|
145
|
+
details: {
|
|
146
|
+
action: "supersede", outcome: "superseded", state: work.status,
|
|
147
|
+
work: { id: work.id, subject: work.subject, ...(work.description ? { description: work.description } : {}), dependsOn: work.dependsOn, resources: work.resources, ...(work.verify ? { verify: work.verify } : {}), state: work.status },
|
|
148
|
+
supersededWorkIds: created.supersededTaskIds, notifiedTeammates: created.notifiedTeammates, claimable: created.claimable,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (params.action === "reopen") {
|
|
153
|
+
const reopened = reopenExistingWork(params.id);
|
|
154
|
+
if (!reopened.ok) throw new Error(reopened.error);
|
|
155
|
+
refreshTeamUI(ctx);
|
|
156
|
+
return {
|
|
157
|
+
content: [{ type: "text", text: `WORK · current session\nREOPENED · ${reopened.workId} · pending\nREASON · ${params.reason}` }],
|
|
158
|
+
details: { action: "reopen", outcome: "reopened", state: "pending", work: { id: reopened.workId, subject: reopened.subject, resources: reopened.resources, state: "pending" }, reason: params.reason },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (params.action === "release") {
|
|
162
|
+
const released = releaseExistingWork(params.id, params.reason);
|
|
163
|
+
if (!released.ok) throw new Error(released.error);
|
|
164
|
+
refreshTeamUI(ctx);
|
|
165
|
+
const residual = released.holderStillRunning
|
|
166
|
+
? `\nRISK · @${released.holderStillRunning} was still working; an in-flight tool batch may still write inside ${released.resources.join(", ") || "the released scope"}.`
|
|
167
|
+
: "";
|
|
168
|
+
return {
|
|
169
|
+
content: [{ type: "text", text: `WORK · current session\nRELEASED · ${released.workId} · pending\nREASON · ${params.reason}${residual}` }],
|
|
170
|
+
details: { action: "release", outcome: "released", state: "pending", work: { id: released.workId, subject: released.subject, resources: released.resources, state: "pending" }, reason: params.reason, ...(released.holderStillRunning ? { holderStillRunning: released.holderStillRunning } : {}) },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (params.action === "assign") {
|
|
174
|
+
const assigned = assignExistingWork(params.id, params.target.session);
|
|
175
|
+
if (!assigned.ok) throw new Error(assigned.error);
|
|
176
|
+
const task = listTasks().find((entry) => entry.id === assigned.workId);
|
|
177
|
+
if (!task) throw new Error(`Assigned Work Item "${assigned.workId}" is unavailable.`);
|
|
178
|
+
refreshTeamUI(ctx);
|
|
179
|
+
return {
|
|
180
|
+
content: [{ type: "text", text: `WORK · current session\nASSIGNED · ${task.id} · claimed · owner=@${assigned.owner}\nATTEMPT · ${assigned.assignmentId} · fresh-session-pending` }],
|
|
181
|
+
details: {
|
|
182
|
+
action: "assign", outcome: "assigned", state: task.status,
|
|
183
|
+
work: { id: task.id, subject: task.subject, resources: task.resources, state: task.status, claimedBy: task.claimedBy },
|
|
184
|
+
assignment: { id: assigned.assignmentId, owner: assigned.owner, kind: "direct" },
|
|
185
|
+
target: params.target, delivery: "fresh-session-pending",
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (params.action === "list") {
|
|
190
|
+
const works = listTasks().map((task) => ({
|
|
191
|
+
id: task.id,
|
|
192
|
+
subject: task.subject,
|
|
193
|
+
...(task.description ? { description: task.description } : {}),
|
|
194
|
+
dependsOn: task.dependsOn,
|
|
195
|
+
resources: task.resources,
|
|
196
|
+
...(task.verify ? { verify: task.verify } : {}),
|
|
197
|
+
state: task.status,
|
|
198
|
+
...(task.claimedBy ? { claimedBy: task.claimedBy } : {}),
|
|
199
|
+
...(task.result ? { result: task.result } : {}),
|
|
200
|
+
}));
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text", text: formatWorkList(listTasks()) }],
|
|
203
|
+
details: { action: "list", outcome: "listed", works, count: works.length },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
196
206
|
const tasks = listTasks();
|
|
197
|
-
const referenced =
|
|
207
|
+
const referenced = params.dependsOn ?? [];
|
|
198
208
|
if (referenced.some((id) => !tasks.some((task) => task.id === id))) {
|
|
199
|
-
throw new Error(`Unknown
|
|
209
|
+
throw new Error(`Unknown work id in [${referenced.join(", ")}].`);
|
|
200
210
|
}
|
|
201
211
|
const created = createBoardTask(params);
|
|
202
212
|
if (!created.ok) throw new Error(created.error);
|
|
213
|
+
const work = listTasks().find((task) => task.id === created.id);
|
|
214
|
+
if (!work) throw new Error(`Created Work Item "${created.id}" is unavailable.`);
|
|
203
215
|
refreshTeamUI(ctx);
|
|
204
|
-
refreshLeaderToolDisclosure();
|
|
205
216
|
return {
|
|
206
|
-
content: [{ type: "text", text:
|
|
217
|
+
content: [{ type: "text", text: formatWorkCreation(params.subject, created) }],
|
|
207
218
|
details: {
|
|
219
|
+
action: "create",
|
|
220
|
+
outcome: "created",
|
|
221
|
+
state: work.status,
|
|
222
|
+
work: {
|
|
223
|
+
id: work.id,
|
|
224
|
+
subject: work.subject,
|
|
225
|
+
...(work.description ? { description: work.description } : {}),
|
|
226
|
+
dependsOn: work.dependsOn,
|
|
227
|
+
resources: work.resources,
|
|
228
|
+
...(work.verify ? { verify: work.verify } : {}),
|
|
229
|
+
state: work.status,
|
|
230
|
+
},
|
|
208
231
|
notifiedTeammates: created.notifiedTeammates,
|
|
209
|
-
livingTeammates: created.livingTeammates,
|
|
210
232
|
claimable: created.claimable,
|
|
211
|
-
|
|
233
|
+
supersededWorkIds: created.supersededTaskIds,
|
|
212
234
|
},
|
|
213
235
|
};
|
|
214
236
|
},
|
|
215
237
|
});
|
|
216
238
|
|
|
217
|
-
|
|
218
|
-
leaderToolApi = pi;
|
|
239
|
+
|
|
219
240
|
}
|
|
220
241
|
|
|
221
242
|
function teamStatusSummary(): string {
|
|
@@ -223,7 +244,8 @@ function teamStatusSummary(): string {
|
|
|
223
244
|
const rolesLine = roles.length > 0
|
|
224
245
|
? `${roles.length} persistent agent role${roles.length === 1 ? "" : "s"}: ${roles.join(", ")}`
|
|
225
246
|
: "No agent roles discovered.";
|
|
226
|
-
|
|
247
|
+
const roster = livingTeammates().map((t) => `@${t.name} (${t.agent}, ${t.status})`).join("\n") || "No living sessions.";
|
|
248
|
+
return `${roster}\n${rolesLine}`;
|
|
227
249
|
}
|
|
228
250
|
|
|
229
251
|
export function registerTeamCommand(pi: ExtensionAPI): void {
|