@fradser/pi-agent-teams 0.8.3 → 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 +118 -160
- 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 -161
- 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,222 +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
|
-
}).replace(/^\[agent\]\s*/, "");
|
|
73
|
-
const prefix = theme.fg("customMessageLabel", theme.bold("[agent]"));
|
|
74
|
-
return new Text(`${prefix} ${title}`, 0, 0);
|
|
75
|
-
},
|
|
76
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
77
|
-
const result = spawnTeammate(params);
|
|
78
|
-
if (!result.ok) throw new Error(result.error);
|
|
79
|
-
refreshTeamUI(ctx);
|
|
80
|
-
refreshLeaderToolDisclosure();
|
|
81
|
-
const kickoffNote = params.prompt?.trim()
|
|
82
|
-
? "It received your kickoff prompt and is working on it."
|
|
83
|
-
: "It received the standard board-check kickoff and is running its first turn; it idles once that settles.";
|
|
84
|
-
return {
|
|
85
|
-
content: [{ type: "text", text: `@${params.name} is alive as ${params.agent}.\n${kickoffNote}\n\n${rosterSummary()}` }],
|
|
86
|
-
details: { started: true },
|
|
87
|
-
};
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
|
|
58
|
+
export function registerLeaderTools(pi: ExtensionAPI, runtime: AgentActionRuntime & { sendLeaderMessage: typeof sendLeaderMessage } = { spawnTeammate, shutdownTeammateExact, sendLeaderMessage }): void {
|
|
91
59
|
pi.registerTool({
|
|
92
|
-
name: "
|
|
93
|
-
promptSnippet: "
|
|
94
|
-
label: "
|
|
95
|
-
description: "
|
|
96
|
-
|
|
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,
|
|
97
66
|
renderShell: "self",
|
|
98
67
|
renderCall: emptyToolCall,
|
|
99
68
|
renderResult(result, options, theme, context) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// report is queued to); a second event row is noise.
|
|
103
|
-
if (hasAnnouncedFinish(name) || hasTerminalReport(name)) return { render: () => [], invalidate: () => {} };
|
|
104
|
-
return renderLifecycleResult(result, options, theme, context, eventToolLifecycle(
|
|
69
|
+
return renderCoordinationRow(
|
|
70
|
+
result, options, theme, context,
|
|
105
71
|
"agent",
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
));
|
|
72
|
+
agentRow(context.args as AgentRowArgs, result.details, { isError: context.isError, isPartial: options.isPartial }),
|
|
73
|
+
);
|
|
109
74
|
},
|
|
110
75
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
111
|
-
const result = await
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
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 };
|
|
116
81
|
},
|
|
117
82
|
});
|
|
118
83
|
|
|
119
84
|
pi.registerTool({
|
|
120
|
-
name: "
|
|
121
|
-
promptSnippet: "Send
|
|
122
|
-
label: "
|
|
123
|
-
description: "
|
|
124
|
-
parameters:
|
|
125
|
-
// Canonical lifecycle rows (same as packages/monitor): empty call slot,
|
|
126
|
-
// 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,
|
|
127
90
|
renderShell: "self",
|
|
128
91
|
renderCall: emptyToolCall,
|
|
129
92
|
renderResult(result, options, theme, context) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const subject = outcome === "not-sent" ? "terminal report available" : outcome;
|
|
133
|
-
return renderLifecycleResult(result, options, theme, context, eventToolLifecycle(
|
|
93
|
+
return renderCoordinationRow(
|
|
94
|
+
result, options, theme, context,
|
|
134
95
|
"message",
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
));
|
|
96
|
+
messageRow(context.args as { to?: string; message?: string }, result.details, { isError: context.isError }),
|
|
97
|
+
);
|
|
138
98
|
},
|
|
139
99
|
async execute(_toolCallId, params) {
|
|
140
|
-
if (params.to
|
|
141
|
-
|
|
142
|
-
reopen: params.reopen,
|
|
143
|
-
resources: params.resources,
|
|
144
|
-
});
|
|
145
|
-
if (!result.ok) throw new Error(result.error);
|
|
146
|
-
if (result.outcome === "not-sent") {
|
|
147
|
-
return {
|
|
148
|
-
content: [{
|
|
149
|
-
type: "text",
|
|
150
|
-
text: [
|
|
151
|
-
"ROUTING · not sent",
|
|
152
|
-
"NEXT · No new message was delivered. Read the recorded report below; use reopen=true only for a distinct new assignment.",
|
|
153
|
-
`NOTE · @${params.to} already sent a terminal report. Delivery to your context is automatic; asking it to resend produces a duplicate leader turn.`,
|
|
154
|
-
`RECORDED TERMINAL REPORT · ${result.terminalReport}`,
|
|
155
|
-
].join("\n"),
|
|
156
|
-
}],
|
|
157
|
-
details: { outcome: "not-sent", terminalReportAvailable: true },
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
const action = result.outcome === "steered"
|
|
161
|
-
? "active control stream accepted the steer"
|
|
162
|
-
: "harness will deliver the queued message on the next wake-up";
|
|
163
|
-
let text = `MESSAGING\n${result.outcome.toUpperCase()} · to=@${params.to}\nNEXT · ${action}`;
|
|
164
|
-
// A stray status field (copied from worker report patterns) must not
|
|
165
|
-
// block delivery — it carries no meaning on leader-sent messages.
|
|
166
|
-
if (params.status) text += `\nNOTE · status ignored for leader-directed steering`;
|
|
167
|
-
if (result.priorTerminalReport) {
|
|
168
|
-
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.`;
|
|
169
|
-
text += `\nPRIOR TERMINAL REPORT · ${result.priorTerminalReport}`;
|
|
100
|
+
if (!params.to) {
|
|
101
|
+
throw new Error("No bound reply route exists. Please specify 'to' explicitly.");
|
|
170
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 = "";
|
|
171
109
|
return {
|
|
172
|
-
content: [{ type: "text", text }],
|
|
173
|
-
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" },
|
|
174
112
|
};
|
|
175
113
|
},
|
|
176
114
|
});
|
|
177
115
|
|
|
178
116
|
pi.registerTool({
|
|
179
|
-
name: "
|
|
180
|
-
promptSnippet: "Create
|
|
181
|
-
label: "
|
|
182
|
-
description: "
|
|
183
|
-
parameters:
|
|
184
|
-
// Canonical lifecycle rows (same as packages/monitor): empty call slot,
|
|
185
|
-
// 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,
|
|
186
122
|
renderShell: "self",
|
|
187
123
|
renderCall: emptyToolCall,
|
|
188
124
|
renderResult(result, options, theme, context) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
"
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
));
|
|
125
|
+
return renderCoordinationRow(
|
|
126
|
+
result, options, theme, context,
|
|
127
|
+
"work",
|
|
128
|
+
leaderWorkRow(context.args as Record<string, unknown>, result.details, { isError: context.isError }),
|
|
129
|
+
);
|
|
195
130
|
},
|
|
196
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
|
+
}
|
|
197
206
|
const tasks = listTasks();
|
|
198
|
-
const referenced =
|
|
207
|
+
const referenced = params.dependsOn ?? [];
|
|
199
208
|
if (referenced.some((id) => !tasks.some((task) => task.id === id))) {
|
|
200
|
-
throw new Error(`Unknown
|
|
209
|
+
throw new Error(`Unknown work id in [${referenced.join(", ")}].`);
|
|
201
210
|
}
|
|
202
211
|
const created = createBoardTask(params);
|
|
203
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.`);
|
|
204
215
|
refreshTeamUI(ctx);
|
|
205
|
-
refreshLeaderToolDisclosure();
|
|
206
216
|
return {
|
|
207
|
-
content: [{ type: "text", text:
|
|
217
|
+
content: [{ type: "text", text: formatWorkCreation(params.subject, created) }],
|
|
208
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
|
+
},
|
|
209
231
|
notifiedTeammates: created.notifiedTeammates,
|
|
210
|
-
livingTeammates: created.livingTeammates,
|
|
211
232
|
claimable: created.claimable,
|
|
212
|
-
|
|
233
|
+
supersededWorkIds: created.supersededTaskIds,
|
|
213
234
|
},
|
|
214
235
|
};
|
|
215
236
|
},
|
|
216
237
|
});
|
|
217
238
|
|
|
218
|
-
|
|
219
|
-
leaderToolApi = pi;
|
|
239
|
+
|
|
220
240
|
}
|
|
221
241
|
|
|
222
242
|
function teamStatusSummary(): string {
|
|
@@ -224,7 +244,8 @@ function teamStatusSummary(): string {
|
|
|
224
244
|
const rolesLine = roles.length > 0
|
|
225
245
|
? `${roles.length} persistent agent role${roles.length === 1 ? "" : "s"}: ${roles.join(", ")}`
|
|
226
246
|
: "No agent roles discovered.";
|
|
227
|
-
|
|
247
|
+
const roster = livingTeammates().map((t) => `@${t.name} (${t.agent}, ${t.status})`).join("\n") || "No living sessions.";
|
|
248
|
+
return `${roster}\n${rolesLine}`;
|
|
228
249
|
}
|
|
229
250
|
|
|
230
251
|
export function registerTeamCommand(pi: ExtensionAPI): void {
|