pi-messenger 0.7.3
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/ARCHITECTURE.md +244 -0
- package/CHANGELOG.md +418 -0
- package/README.md +394 -0
- package/banner.png +0 -0
- package/config-overlay.ts +172 -0
- package/config.ts +178 -0
- package/crew/agents/crew-docs-scout.md +55 -0
- package/crew/agents/crew-gap-analyst.md +105 -0
- package/crew/agents/crew-github-scout.md +111 -0
- package/crew/agents/crew-interview-generator.md +79 -0
- package/crew/agents/crew-plan-sync.md +64 -0
- package/crew/agents/crew-practice-scout.md +62 -0
- package/crew/agents/crew-repo-scout.md +65 -0
- package/crew/agents/crew-reviewer.md +58 -0
- package/crew/agents/crew-web-scout.md +85 -0
- package/crew/agents/crew-worker.md +95 -0
- package/crew/agents.ts +200 -0
- package/crew/handlers/interview.ts +211 -0
- package/crew/handlers/plan.ts +358 -0
- package/crew/handlers/review.ts +341 -0
- package/crew/handlers/status.ts +257 -0
- package/crew/handlers/sync.ts +232 -0
- package/crew/handlers/task.ts +511 -0
- package/crew/handlers/work.ts +289 -0
- package/crew/id-allocator.ts +44 -0
- package/crew/index.ts +229 -0
- package/crew/state.ts +116 -0
- package/crew/store.ts +480 -0
- package/crew/types.ts +164 -0
- package/crew/utils/artifacts.ts +65 -0
- package/crew/utils/config.ts +104 -0
- package/crew/utils/discover.ts +170 -0
- package/crew/utils/install.ts +373 -0
- package/crew/utils/progress.ts +107 -0
- package/crew/utils/result.ts +16 -0
- package/crew/utils/truncate.ts +79 -0
- package/crew-overlay.ts +259 -0
- package/handlers.ts +799 -0
- package/index.ts +591 -0
- package/lib.ts +232 -0
- package/overlay.ts +687 -0
- package/package.json +20 -0
- package/skills/pi-messenger-crew/SKILL.md +140 -0
- package/store.ts +1068 -0
- package/tsconfig.json +19 -0
package/handlers.ts
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Messenger - Tool and Command Handlers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
type MessengerState,
|
|
9
|
+
type Dirs,
|
|
10
|
+
type AgentMailMessage,
|
|
11
|
+
type AgentRegistration,
|
|
12
|
+
type SpecClaims,
|
|
13
|
+
type SpecCompletions,
|
|
14
|
+
formatRelativeTime,
|
|
15
|
+
extractFolder,
|
|
16
|
+
truncatePathLeft,
|
|
17
|
+
getDisplayMode,
|
|
18
|
+
displaySpecPath,
|
|
19
|
+
resolveSpecPath
|
|
20
|
+
} from "./lib.js";
|
|
21
|
+
import * as store from "./store.js";
|
|
22
|
+
import { getAutoRegisterPaths, saveAutoRegisterPaths, matchesAutoRegisterPath } from "./config.js";
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Tool Result Helper
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
function result(text: string, details: Record<string, unknown>) {
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text" as const, text }],
|
|
31
|
+
details
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Not Registered Error
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
export function notRegisteredError() {
|
|
40
|
+
return result(
|
|
41
|
+
"Not registered. Use pi_messenger({ join: true }) to join the agent mesh first.",
|
|
42
|
+
{ mode: "error", error: "not_registered" }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// Tool Execute Functions
|
|
48
|
+
// =============================================================================
|
|
49
|
+
|
|
50
|
+
export function executeJoin(
|
|
51
|
+
state: MessengerState,
|
|
52
|
+
dirs: Dirs,
|
|
53
|
+
ctx: ExtensionContext,
|
|
54
|
+
deliverFn: (msg: AgentMailMessage) => void,
|
|
55
|
+
updateStatusFn: (ctx: ExtensionContext) => void,
|
|
56
|
+
specPath?: string
|
|
57
|
+
) {
|
|
58
|
+
if (state.registered) {
|
|
59
|
+
const agents = store.getActiveAgents(state, dirs);
|
|
60
|
+
return result(
|
|
61
|
+
`Already joined as ${state.agentName}. ${agents.length} peer${agents.length === 1 ? "" : "s"} active.`,
|
|
62
|
+
{ mode: "join", alreadyJoined: true, name: state.agentName, peerCount: agents.length }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!store.register(state, dirs, ctx)) {
|
|
67
|
+
return result(
|
|
68
|
+
"Failed to join the agent mesh. Check logs for details.",
|
|
69
|
+
{ mode: "join", error: "registration_failed" }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
store.startWatcher(state, dirs, deliverFn);
|
|
74
|
+
updateStatusFn(ctx);
|
|
75
|
+
|
|
76
|
+
let specWarning = "";
|
|
77
|
+
if (specPath) {
|
|
78
|
+
state.spec = resolveSpecPath(specPath, process.cwd());
|
|
79
|
+
store.updateRegistration(state, dirs, ctx);
|
|
80
|
+
if (!existsSync(state.spec)) {
|
|
81
|
+
specWarning = `\n\nWarning: Spec file not found at ${displaySpecPath(state.spec, process.cwd())}.`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const agents = store.getActiveAgents(state, dirs);
|
|
86
|
+
const folder = extractFolder(process.cwd());
|
|
87
|
+
const locationPart = state.gitBranch ? `${folder} on ${state.gitBranch}` : folder;
|
|
88
|
+
|
|
89
|
+
let text = `Joined as ${state.agentName} in ${locationPart}. ${agents.length} peer${agents.length === 1 ? "" : "s"} active.`;
|
|
90
|
+
|
|
91
|
+
if (state.spec) {
|
|
92
|
+
text += `\nSpec: ${displaySpecPath(state.spec, process.cwd())}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (agents.length > 0) {
|
|
96
|
+
text += `\n\nActive peers: ${agents.map(a => a.name).join(", ")}`;
|
|
97
|
+
text += `\n\nUse pi_messenger({ list: true }) for details, or pi_messenger({ to: "Name", message: "..." }) to send.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (specWarning) {
|
|
101
|
+
text += specWarning;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result(text, {
|
|
105
|
+
mode: "join",
|
|
106
|
+
name: state.agentName,
|
|
107
|
+
location: locationPart,
|
|
108
|
+
peerCount: agents.length,
|
|
109
|
+
peers: agents.map(a => a.name),
|
|
110
|
+
spec: state.spec ? displaySpecPath(state.spec, process.cwd()) : undefined
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function executeStatus(state: MessengerState, dirs: Dirs) {
|
|
115
|
+
if (!state.registered) {
|
|
116
|
+
return notRegisteredError();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const agents = store.getActiveAgents(state, dirs);
|
|
120
|
+
const folder = extractFolder(process.cwd());
|
|
121
|
+
const location = state.gitBranch ? `${folder} (${state.gitBranch})` : folder;
|
|
122
|
+
const myClaim = store.getAgentCurrentClaim(dirs, state.agentName);
|
|
123
|
+
|
|
124
|
+
let text = `You: ${state.agentName}\n`;
|
|
125
|
+
text += `Location: ${location}\n`;
|
|
126
|
+
|
|
127
|
+
if (state.spec) {
|
|
128
|
+
const specDisplay = displaySpecPath(state.spec, process.cwd());
|
|
129
|
+
text += `Spec: ${specDisplay}\n`;
|
|
130
|
+
if (myClaim) {
|
|
131
|
+
text += `Claim: ${myClaim.taskId}${myClaim.reason ? ` - ${myClaim.reason}` : ""}\n`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
text += `Peers: ${agents.length}\n`;
|
|
136
|
+
if (state.reservations.length > 0) {
|
|
137
|
+
const myRes = state.reservations.map(r => `🔒 ${truncatePathLeft(r.pattern, 40)}`);
|
|
138
|
+
text += `Reservations: ${myRes.join(", ")}\n`;
|
|
139
|
+
}
|
|
140
|
+
text += `\nUse { list: true } for details, { swarm: true } for task status.`;
|
|
141
|
+
|
|
142
|
+
return result(text, {
|
|
143
|
+
mode: "status",
|
|
144
|
+
registered: true,
|
|
145
|
+
self: state.agentName,
|
|
146
|
+
folder,
|
|
147
|
+
gitBranch: state.gitBranch,
|
|
148
|
+
peerCount: agents.length,
|
|
149
|
+
spec: state.spec ? displaySpecPath(state.spec, process.cwd()) : undefined,
|
|
150
|
+
claim: myClaim
|
|
151
|
+
? {
|
|
152
|
+
...myClaim,
|
|
153
|
+
spec: displaySpecPath(myClaim.spec, process.cwd())
|
|
154
|
+
}
|
|
155
|
+
: undefined,
|
|
156
|
+
reservations: state.reservations
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function executeList(state: MessengerState, dirs: Dirs) {
|
|
161
|
+
if (!state.registered) {
|
|
162
|
+
return notRegisteredError();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const agents = store.getActiveAgents(state, dirs);
|
|
166
|
+
|
|
167
|
+
if (agents.length === 0) {
|
|
168
|
+
return result(
|
|
169
|
+
"No other agents currently active.",
|
|
170
|
+
{ mode: "list", registered: true, agents: [], self: state.agentName, agentClaims: {} }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const lines: string[] = [];
|
|
175
|
+
const hasAnySpec = !!state.spec || agents.some(a => a.spec);
|
|
176
|
+
const allClaims = store.getClaims(dirs);
|
|
177
|
+
const agentClaims: Record<string, { spec: string; taskId: string; reason?: string }> = {};
|
|
178
|
+
|
|
179
|
+
for (const [specPath, tasks] of Object.entries(allClaims)) {
|
|
180
|
+
for (const [taskId, claim] of Object.entries(tasks)) {
|
|
181
|
+
if (claim.agent !== state.agentName) {
|
|
182
|
+
agentClaims[claim.agent] = {
|
|
183
|
+
spec: displaySpecPath(specPath, process.cwd()),
|
|
184
|
+
taskId,
|
|
185
|
+
reason: claim.reason
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function formatReservations(a: AgentRegistration): string[] {
|
|
192
|
+
if (!a.reservations || a.reservations.length === 0) return [];
|
|
193
|
+
return a.reservations.map(r => `🔒 ${truncatePathLeft(r.pattern, 40)}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (hasAnySpec) {
|
|
197
|
+
const bySpec: Record<string, AgentRegistration[]> = { "No spec": [] };
|
|
198
|
+
for (const a of agents) {
|
|
199
|
+
const key = a.spec ? displaySpecPath(a.spec, process.cwd()) : "No spec";
|
|
200
|
+
if (!bySpec[key]) bySpec[key] = [];
|
|
201
|
+
bySpec[key].push(a);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const specKeys = Object.keys(bySpec).filter(key => bySpec[key].length > 0);
|
|
205
|
+
const sortedKeys = specKeys
|
|
206
|
+
.filter(key => key !== "No spec")
|
|
207
|
+
.sort((a, b) => a.localeCompare(b));
|
|
208
|
+
if (bySpec["No spec"].length > 0) sortedKeys.push("No spec");
|
|
209
|
+
|
|
210
|
+
for (const spec of sortedKeys) {
|
|
211
|
+
lines.push(`${spec}:`);
|
|
212
|
+
for (const a of bySpec[spec]) {
|
|
213
|
+
const claim = agentClaims[a.name];
|
|
214
|
+
const claimStr = claim ? claim.taskId : "(idle)";
|
|
215
|
+
const time = formatRelativeTime(a.startedAt);
|
|
216
|
+
lines.push(` ${a.name.padEnd(14)} ${claimStr.padEnd(10)} ${a.model.padEnd(18)} ${time}`);
|
|
217
|
+
}
|
|
218
|
+
lines.push("");
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
const mode = getDisplayMode(agents);
|
|
222
|
+
if (mode === "same-folder-branch") {
|
|
223
|
+
const folder = extractFolder(agents[0].cwd);
|
|
224
|
+
const branch = agents.find(a => a.gitBranch)?.gitBranch;
|
|
225
|
+
const header = branch ? `Peers in ${folder} (${branch}):` : `Peers in ${folder}:`;
|
|
226
|
+
lines.push(header, "");
|
|
227
|
+
|
|
228
|
+
for (const a of agents) {
|
|
229
|
+
const time = formatRelativeTime(a.startedAt);
|
|
230
|
+
lines.push(` ${a.name.padEnd(14)} ${a.model.padEnd(20)} ${time}`);
|
|
231
|
+
for (const res of formatReservations(a)) {
|
|
232
|
+
lines.push(` ${res}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} else if (mode === "same-folder") {
|
|
236
|
+
const folder = extractFolder(agents[0].cwd);
|
|
237
|
+
lines.push(`Peers in ${folder}:`, "");
|
|
238
|
+
|
|
239
|
+
for (const a of agents) {
|
|
240
|
+
const branch = a.gitBranch ?? "";
|
|
241
|
+
const time = formatRelativeTime(a.startedAt);
|
|
242
|
+
lines.push(` ${a.name.padEnd(14)} ${branch.padEnd(12)} ${a.model.padEnd(20)} ${time}`);
|
|
243
|
+
for (const res of formatReservations(a)) {
|
|
244
|
+
lines.push(` ${res}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
lines.push("Peers:", "");
|
|
249
|
+
|
|
250
|
+
for (const a of agents) {
|
|
251
|
+
const folder = extractFolder(a.cwd);
|
|
252
|
+
const branch = a.gitBranch ?? "";
|
|
253
|
+
const time = formatRelativeTime(a.startedAt);
|
|
254
|
+
lines.push(` ${a.name.padEnd(14)} ${folder.padEnd(20)} ${branch.padEnd(12)} ${a.model.padEnd(20)} ${time}`);
|
|
255
|
+
for (const res of formatReservations(a)) {
|
|
256
|
+
lines.push(` ${res}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return result(
|
|
263
|
+
lines.join("\n").trim(),
|
|
264
|
+
{ mode: "list", registered: true, agents, self: state.agentName, agentClaims }
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function executeSend(
|
|
269
|
+
state: MessengerState,
|
|
270
|
+
dirs: Dirs,
|
|
271
|
+
to: string | string[] | undefined,
|
|
272
|
+
broadcast: boolean | undefined,
|
|
273
|
+
message?: string,
|
|
274
|
+
replyTo?: string
|
|
275
|
+
) {
|
|
276
|
+
if (!state.registered) {
|
|
277
|
+
return notRegisteredError();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!message) {
|
|
281
|
+
return result(
|
|
282
|
+
"Error: message is required when sending.",
|
|
283
|
+
{ mode: "send", error: "missing_message" }
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let recipients: string[];
|
|
288
|
+
if (broadcast) {
|
|
289
|
+
const agents = store.getActiveAgents(state, dirs);
|
|
290
|
+
recipients = agents.map(a => a.name);
|
|
291
|
+
if (recipients.length === 0) {
|
|
292
|
+
return result(
|
|
293
|
+
"No active agents to broadcast to.",
|
|
294
|
+
{ mode: "send", error: "no_recipients" }
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
} else if (to) {
|
|
298
|
+
recipients = [...new Set(Array.isArray(to) ? to : [to])];
|
|
299
|
+
if (recipients.length === 0) {
|
|
300
|
+
return result(
|
|
301
|
+
"Error: recipient list cannot be empty.",
|
|
302
|
+
{ mode: "send", error: "empty_recipients" }
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
return result(
|
|
307
|
+
"Error: specify 'to' or 'broadcast: true'.",
|
|
308
|
+
{ mode: "send", error: "missing_recipient" }
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const sent: string[] = [];
|
|
313
|
+
const failed: Array<{ name: string; error: string }> = [];
|
|
314
|
+
|
|
315
|
+
for (const recipient of recipients) {
|
|
316
|
+
if (recipient === state.agentName) {
|
|
317
|
+
failed.push({ name: recipient, error: "cannot send to self" });
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const validation = store.validateTargetAgent(recipient, dirs);
|
|
322
|
+
if (!validation.valid) {
|
|
323
|
+
const errorMap: Record<string, string> = {
|
|
324
|
+
invalid_name: "invalid name",
|
|
325
|
+
not_found: "not found",
|
|
326
|
+
not_active: "no longer active",
|
|
327
|
+
invalid_registration: "invalid registration",
|
|
328
|
+
};
|
|
329
|
+
const errKey = (validation as { valid: false; error: string }).error;
|
|
330
|
+
failed.push({ name: recipient, error: errorMap[errKey] });
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
store.sendMessageToAgent(state, dirs, recipient, message, replyTo);
|
|
336
|
+
sent.push(recipient);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
const msg = err instanceof Error ? err.message : "write failed";
|
|
339
|
+
failed.push({ name: recipient, error: msg });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (sent.length === 0) {
|
|
344
|
+
const failedStr = failed.map(f => `${f.name} (${f.error})`).join(", ");
|
|
345
|
+
return result(
|
|
346
|
+
`Failed to send: ${failedStr}`,
|
|
347
|
+
{ mode: "send", error: "all_failed", sent: [], failed }
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let text = `Message sent to ${sent.join(", ")}.`;
|
|
352
|
+
if (failed.length > 0) {
|
|
353
|
+
const failedStr = failed.map(f => `${f.name} (${f.error})`).join(", ");
|
|
354
|
+
text += ` Failed: ${failedStr}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return result(text, { mode: "send", sent, failed });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function executeReserve(
|
|
361
|
+
state: MessengerState,
|
|
362
|
+
dirs: Dirs,
|
|
363
|
+
ctx: ExtensionContext,
|
|
364
|
+
patterns: string[],
|
|
365
|
+
reason?: string
|
|
366
|
+
) {
|
|
367
|
+
if (!state.registered) {
|
|
368
|
+
return notRegisteredError();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (patterns.length === 0) {
|
|
372
|
+
return result(
|
|
373
|
+
"Error: at least one pattern required.",
|
|
374
|
+
{ mode: "reserve", error: "empty_patterns" }
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const now = new Date().toISOString();
|
|
379
|
+
|
|
380
|
+
for (const pattern of patterns) {
|
|
381
|
+
state.reservations = state.reservations.filter(r => r.pattern !== pattern);
|
|
382
|
+
state.reservations.push({ pattern, reason, since: now });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
store.updateRegistration(state, dirs, ctx);
|
|
386
|
+
|
|
387
|
+
return result(`Reserved: ${patterns.join(", ")}`, { mode: "reserve", patterns, reason });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function executeRelease(
|
|
391
|
+
state: MessengerState,
|
|
392
|
+
dirs: Dirs,
|
|
393
|
+
ctx: ExtensionContext,
|
|
394
|
+
release: string[] | true
|
|
395
|
+
) {
|
|
396
|
+
if (!state.registered) {
|
|
397
|
+
return notRegisteredError();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (release === true) {
|
|
401
|
+
const released = state.reservations.map(r => r.pattern);
|
|
402
|
+
state.reservations = [];
|
|
403
|
+
store.updateRegistration(state, dirs, ctx);
|
|
404
|
+
return result(
|
|
405
|
+
released.length > 0 ? `Released all: ${released.join(", ")}` : "No reservations to release.",
|
|
406
|
+
{ mode: "release", released }
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const patterns = release;
|
|
411
|
+
const before = state.reservations.length;
|
|
412
|
+
state.reservations = state.reservations.filter(r => !patterns.includes(r.pattern));
|
|
413
|
+
const releasedCount = before - state.reservations.length;
|
|
414
|
+
|
|
415
|
+
store.updateRegistration(state, dirs, ctx);
|
|
416
|
+
|
|
417
|
+
return result(`Released ${releasedCount} reservation(s).`, { mode: "release", released: patterns });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function executeRename(
|
|
421
|
+
state: MessengerState,
|
|
422
|
+
dirs: Dirs,
|
|
423
|
+
ctx: ExtensionContext,
|
|
424
|
+
newName: string,
|
|
425
|
+
deliverFn: (msg: AgentMailMessage) => void,
|
|
426
|
+
updateStatusFn: (ctx: ExtensionContext) => void
|
|
427
|
+
) {
|
|
428
|
+
store.stopWatcher(state);
|
|
429
|
+
|
|
430
|
+
const renameResult = store.renameAgent(state, dirs, ctx, newName, deliverFn);
|
|
431
|
+
|
|
432
|
+
if (!renameResult.success) {
|
|
433
|
+
store.startWatcher(state, dirs, deliverFn);
|
|
434
|
+
|
|
435
|
+
const errCode = (renameResult as { success: false; error: string }).error;
|
|
436
|
+
const errorMessages: Record<string, string> = {
|
|
437
|
+
not_registered: "Cannot rename - not registered.",
|
|
438
|
+
invalid_name: `Invalid name "${newName}" - use only letters, numbers, underscore, hyphen.`,
|
|
439
|
+
name_taken: `Name "${newName}" is already in use by another agent.`,
|
|
440
|
+
same_name: `Already named "${newName}".`,
|
|
441
|
+
race_lost: `Name "${newName}" was claimed by another agent.`,
|
|
442
|
+
};
|
|
443
|
+
return result(
|
|
444
|
+
`Error: ${errorMessages[errCode]}`,
|
|
445
|
+
{ mode: "rename", error: errCode }
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
state.watcherRetries = 0;
|
|
450
|
+
store.startWatcher(state, dirs, deliverFn);
|
|
451
|
+
updateStatusFn(ctx);
|
|
452
|
+
|
|
453
|
+
return result(
|
|
454
|
+
`Renamed from "${renameResult.oldName}" to "${renameResult.newName}".`,
|
|
455
|
+
{ mode: "rename", oldName: renameResult.oldName, newName: renameResult.newName }
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function executeSetSpec(
|
|
460
|
+
state: MessengerState,
|
|
461
|
+
dirs: Dirs,
|
|
462
|
+
ctx: ExtensionContext,
|
|
463
|
+
specPath: string
|
|
464
|
+
) {
|
|
465
|
+
const absPath = resolveSpecPath(specPath, process.cwd());
|
|
466
|
+
state.spec = absPath;
|
|
467
|
+
store.updateRegistration(state, dirs, ctx);
|
|
468
|
+
const display = displaySpecPath(absPath, process.cwd());
|
|
469
|
+
const warning = existsSync(absPath) ? "" : `\n\nWarning: Spec file not found at ${display}.`;
|
|
470
|
+
return result(`Spec set to ${display}${warning}`, { mode: "spec", spec: display });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export async function executeClaim(
|
|
474
|
+
state: MessengerState,
|
|
475
|
+
dirs: Dirs,
|
|
476
|
+
ctx: ExtensionContext,
|
|
477
|
+
taskId: string,
|
|
478
|
+
specPath?: string,
|
|
479
|
+
reason?: string
|
|
480
|
+
) {
|
|
481
|
+
const spec = specPath ? resolveSpecPath(specPath, process.cwd()) : state.spec;
|
|
482
|
+
if (!spec) {
|
|
483
|
+
return result(
|
|
484
|
+
"Error: No spec registered. Use `spec` parameter or join with a spec first.",
|
|
485
|
+
{ mode: "claim", error: "no_spec" }
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const warning = specPath && !existsSync(spec)
|
|
490
|
+
? `\n\nWarning: Spec file not found at ${displaySpecPath(spec, process.cwd())}.`
|
|
491
|
+
: "";
|
|
492
|
+
|
|
493
|
+
const claimResult = await store.claimTask(
|
|
494
|
+
dirs,
|
|
495
|
+
spec,
|
|
496
|
+
taskId,
|
|
497
|
+
state.agentName,
|
|
498
|
+
ctx.sessionManager.getSessionId(),
|
|
499
|
+
process.pid,
|
|
500
|
+
reason
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const display = displaySpecPath(spec, process.cwd());
|
|
504
|
+
if (store.isClaimSuccess(claimResult)) {
|
|
505
|
+
return result(`Claimed ${taskId} in ${display}${warning}`, {
|
|
506
|
+
mode: "claim",
|
|
507
|
+
spec: display,
|
|
508
|
+
taskId,
|
|
509
|
+
claimedAt: claimResult.claimedAt,
|
|
510
|
+
reason
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (store.isClaimAlreadyHaveClaim(claimResult)) {
|
|
515
|
+
const existingDisplay = displaySpecPath(claimResult.existing.spec, process.cwd());
|
|
516
|
+
return result(
|
|
517
|
+
`Error: You already have a claim on ${claimResult.existing.taskId} in ${existingDisplay}. Complete or unclaim it first.${warning}`,
|
|
518
|
+
{
|
|
519
|
+
mode: "claim",
|
|
520
|
+
error: "already_have_claim",
|
|
521
|
+
existing: { spec: existingDisplay, taskId: claimResult.existing.taskId }
|
|
522
|
+
}
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// isClaimAlreadyClaimed
|
|
527
|
+
return result(
|
|
528
|
+
`Error: ${taskId} is already claimed by ${claimResult.conflict.agent}.${warning}`,
|
|
529
|
+
{ mode: "claim", error: "already_claimed", taskId, conflict: claimResult.conflict }
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export async function executeUnclaim(
|
|
534
|
+
state: MessengerState,
|
|
535
|
+
dirs: Dirs,
|
|
536
|
+
taskId: string,
|
|
537
|
+
specPath?: string
|
|
538
|
+
) {
|
|
539
|
+
const spec = specPath ? resolveSpecPath(specPath, process.cwd()) : state.spec;
|
|
540
|
+
if (!spec) {
|
|
541
|
+
return result("Error: No spec registered.", { mode: "unclaim", error: "no_spec" });
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const warning = specPath && !existsSync(spec)
|
|
545
|
+
? `\n\nWarning: Spec file not found at ${displaySpecPath(spec, process.cwd())}.`
|
|
546
|
+
: "";
|
|
547
|
+
|
|
548
|
+
const unclaimResult = await store.unclaimTask(dirs, spec, taskId, state.agentName);
|
|
549
|
+
const display = displaySpecPath(spec, process.cwd());
|
|
550
|
+
|
|
551
|
+
if (store.isUnclaimSuccess(unclaimResult)) {
|
|
552
|
+
return result(`Released claim on ${taskId}${warning}`, { mode: "unclaim", spec: display, taskId });
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (store.isUnclaimNotYours(unclaimResult)) {
|
|
556
|
+
return result(
|
|
557
|
+
`Error: ${taskId} is claimed by ${unclaimResult.claimedBy}, not you.${warning}`,
|
|
558
|
+
{ mode: "unclaim", error: "not_your_claim", taskId, claimedBy: unclaimResult.claimedBy }
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// error === "not_claimed"
|
|
563
|
+
return result(`Error: ${taskId} is not claimed.${warning}`, { mode: "unclaim", error: "not_claimed", taskId });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export async function executeComplete(
|
|
567
|
+
state: MessengerState,
|
|
568
|
+
dirs: Dirs,
|
|
569
|
+
taskId: string,
|
|
570
|
+
notes?: string,
|
|
571
|
+
specPath?: string
|
|
572
|
+
) {
|
|
573
|
+
const spec = specPath ? resolveSpecPath(specPath, process.cwd()) : state.spec;
|
|
574
|
+
if (!spec) {
|
|
575
|
+
return result("Error: No spec registered.", { mode: "complete", error: "no_spec" });
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const warning = specPath && !existsSync(spec)
|
|
579
|
+
? `\n\nWarning: Spec file not found at ${displaySpecPath(spec, process.cwd())}.`
|
|
580
|
+
: "";
|
|
581
|
+
|
|
582
|
+
const completeResult = await store.completeTask(dirs, spec, taskId, state.agentName, notes);
|
|
583
|
+
const display = displaySpecPath(spec, process.cwd());
|
|
584
|
+
|
|
585
|
+
if (store.isCompleteSuccess(completeResult)) {
|
|
586
|
+
return result(`Completed ${taskId} in ${display}${warning}`, {
|
|
587
|
+
mode: "complete",
|
|
588
|
+
spec: display,
|
|
589
|
+
taskId,
|
|
590
|
+
completedAt: completeResult.completedAt
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (store.isCompleteAlreadyCompleted(completeResult)) {
|
|
595
|
+
return result(
|
|
596
|
+
`Error: ${taskId} was already completed by ${completeResult.completion.completedBy}.${warning}`,
|
|
597
|
+
{ mode: "complete", error: "already_completed", taskId, completion: completeResult.completion }
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (store.isCompleteNotYours(completeResult)) {
|
|
602
|
+
return result(
|
|
603
|
+
`Error: ${taskId} is claimed by ${completeResult.claimedBy}, not you.${warning}`,
|
|
604
|
+
{ mode: "complete", error: "not_your_claim", taskId, claimedBy: completeResult.claimedBy }
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// error === "not_claimed"
|
|
609
|
+
return result(`Error: ${taskId} is not claimed.${warning}`, { mode: "complete", error: "not_claimed", taskId });
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export function executeSwarm(
|
|
613
|
+
state: MessengerState,
|
|
614
|
+
dirs: Dirs,
|
|
615
|
+
specPath?: string
|
|
616
|
+
) {
|
|
617
|
+
const claims = store.getClaims(dirs);
|
|
618
|
+
const completions = store.getCompletions(dirs);
|
|
619
|
+
const agents = store.getActiveAgents(state, dirs);
|
|
620
|
+
const cwd = process.cwd();
|
|
621
|
+
|
|
622
|
+
const absByDisplay = new Map<string, string>();
|
|
623
|
+
const addAbs = (abs: string) => {
|
|
624
|
+
const display = displaySpecPath(abs, cwd);
|
|
625
|
+
if (!absByDisplay.has(display)) absByDisplay.set(display, abs);
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
for (const abs of Object.keys(claims)) addAbs(abs);
|
|
629
|
+
for (const abs of Object.keys(completions)) addAbs(abs);
|
|
630
|
+
if (state.spec) addAbs(state.spec);
|
|
631
|
+
for (const agent of agents) {
|
|
632
|
+
if (agent.spec) addAbs(agent.spec);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const specAgents: Record<string, string[]> = {};
|
|
636
|
+
if (state.spec) {
|
|
637
|
+
const display = displaySpecPath(state.spec, cwd);
|
|
638
|
+
specAgents[display] = [state.agentName];
|
|
639
|
+
}
|
|
640
|
+
for (const agent of agents) {
|
|
641
|
+
if (!agent.spec) continue;
|
|
642
|
+
const display = displaySpecPath(agent.spec, cwd);
|
|
643
|
+
if (!specAgents[display]) specAgents[display] = [];
|
|
644
|
+
specAgents[display].push(agent.name);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const myClaim = store.getAgentCurrentClaim(dirs, state.agentName);
|
|
648
|
+
const mySpec = state.spec ? displaySpecPath(state.spec, cwd) : undefined;
|
|
649
|
+
|
|
650
|
+
if (specPath) {
|
|
651
|
+
const absSpec = resolveSpecPath(specPath, cwd);
|
|
652
|
+
const display = displaySpecPath(absSpec, cwd);
|
|
653
|
+
const warning = !existsSync(absSpec)
|
|
654
|
+
? `\n\nWarning: Spec file not found at ${display}.`
|
|
655
|
+
: "";
|
|
656
|
+
const specClaims: SpecClaims = claims[absSpec] || {};
|
|
657
|
+
const specCompletions: SpecCompletions = completions[absSpec] || {};
|
|
658
|
+
const specAgentList = specAgents[display] || [];
|
|
659
|
+
|
|
660
|
+
const lines = [`Swarm: ${display}`, ""];
|
|
661
|
+
const completedIds = Object.keys(specCompletions);
|
|
662
|
+
lines.push(`Completed: ${completedIds.length > 0 ? completedIds.join(", ") : "(none)"}`);
|
|
663
|
+
|
|
664
|
+
const inProgress = Object.entries(specClaims).map(([tid, c]) =>
|
|
665
|
+
`${tid} (${c.agent === state.agentName ? "you" : c.agent})`
|
|
666
|
+
);
|
|
667
|
+
lines.push(`In progress: ${inProgress.length > 0 ? inProgress.join(", ") : "(none)"}`);
|
|
668
|
+
|
|
669
|
+
const teammates = specAgentList.filter(name => name !== state.agentName);
|
|
670
|
+
if (teammates.length > 0) lines.push(`Teammates: ${teammates.join(", ")}`);
|
|
671
|
+
|
|
672
|
+
return result(lines.join("\n") + warning, {
|
|
673
|
+
mode: "swarm",
|
|
674
|
+
spec: display,
|
|
675
|
+
agents: specAgentList,
|
|
676
|
+
claims: specClaims,
|
|
677
|
+
completions: specCompletions
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const allSpecs = new Set<string>([
|
|
682
|
+
...absByDisplay.keys(),
|
|
683
|
+
...Object.keys(specAgents)
|
|
684
|
+
]);
|
|
685
|
+
|
|
686
|
+
const lines = ["Swarm Status:", ""];
|
|
687
|
+
const specsData: Record<string, { agents: string[]; claims: SpecClaims; completions: SpecCompletions }> = {};
|
|
688
|
+
|
|
689
|
+
for (const display of Array.from(allSpecs).sort((a, b) => a.localeCompare(b))) {
|
|
690
|
+
const absSpec = absByDisplay.get(display) ?? resolveSpecPath(display, cwd);
|
|
691
|
+
const specClaims: SpecClaims = claims[absSpec] || {};
|
|
692
|
+
const specCompletions: SpecCompletions = completions[absSpec] || {};
|
|
693
|
+
const specAgentList = specAgents[display] || [];
|
|
694
|
+
|
|
695
|
+
specsData[display] = { agents: specAgentList, claims: specClaims, completions: specCompletions };
|
|
696
|
+
|
|
697
|
+
const isMySpec = display === mySpec;
|
|
698
|
+
lines.push(`${display}${isMySpec ? " (your spec)" : ""}:`);
|
|
699
|
+
|
|
700
|
+
const completedIds = Object.keys(specCompletions);
|
|
701
|
+
lines.push(` Completed: ${completedIds.length > 0 ? completedIds.join(", ") : "(none)"}`);
|
|
702
|
+
|
|
703
|
+
const inProgress = Object.entries(specClaims).map(([tid, c]) =>
|
|
704
|
+
`${tid} (${c.agent === state.agentName ? "you" : c.agent})`
|
|
705
|
+
);
|
|
706
|
+
lines.push(` In progress: ${inProgress.length > 0 ? inProgress.join(", ") : "(none)"}`);
|
|
707
|
+
|
|
708
|
+
const idle = specAgentList.filter(name =>
|
|
709
|
+
!Object.values(specClaims).some(c => c.agent === name)
|
|
710
|
+
);
|
|
711
|
+
if (idle.length > 0) lines.push(` Idle: ${idle.join(", ")}`);
|
|
712
|
+
lines.push("");
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return result(lines.join("\n").trim(), {
|
|
716
|
+
mode: "swarm",
|
|
717
|
+
yourSpec: mySpec,
|
|
718
|
+
yourClaim: myClaim?.taskId,
|
|
719
|
+
specs: specsData
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function executeAutoRegisterPath(
|
|
724
|
+
action: "add" | "remove" | "list"
|
|
725
|
+
) {
|
|
726
|
+
const cwd = process.cwd();
|
|
727
|
+
const paths = getAutoRegisterPaths();
|
|
728
|
+
|
|
729
|
+
if (action === "list") {
|
|
730
|
+
if (paths.length === 0) {
|
|
731
|
+
return result(
|
|
732
|
+
"No auto-register paths configured.\n\nUse pi_messenger({ autoRegisterPath: \"add\" }) to add the current folder.",
|
|
733
|
+
{ mode: "autoRegisterPath", action: "list", paths: [], currentFolder: cwd, isCurrentInList: false }
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const isCurrentInList = matchesAutoRegisterPath(cwd, paths);
|
|
738
|
+
const lines = ["Auto-register paths:", ""];
|
|
739
|
+
for (const p of paths) {
|
|
740
|
+
const marker = p === cwd ? " (current)" : "";
|
|
741
|
+
lines.push(` ${p}${marker}`);
|
|
742
|
+
}
|
|
743
|
+
lines.push("");
|
|
744
|
+
lines.push(`Current folder: ${cwd}`);
|
|
745
|
+
lines.push(`Status: ${isCurrentInList ? "Will auto-register here" : "Will NOT auto-register here"}`);
|
|
746
|
+
|
|
747
|
+
return result(lines.join("\n"), {
|
|
748
|
+
mode: "autoRegisterPath",
|
|
749
|
+
action: "list",
|
|
750
|
+
paths,
|
|
751
|
+
currentFolder: cwd,
|
|
752
|
+
isCurrentInList
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (action === "add") {
|
|
757
|
+
if (paths.includes(cwd)) {
|
|
758
|
+
return result(
|
|
759
|
+
`Current folder already in auto-register paths:\n ${cwd}`,
|
|
760
|
+
{ mode: "autoRegisterPath", action: "add", alreadyExists: true, path: cwd }
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const newPaths = [...paths, cwd];
|
|
765
|
+
saveAutoRegisterPaths(newPaths);
|
|
766
|
+
|
|
767
|
+
return result(
|
|
768
|
+
`Added to auto-register paths:\n ${cwd}\n\nAgents starting in this folder will now auto-join the mesh.`,
|
|
769
|
+
{ mode: "autoRegisterPath", action: "add", path: cwd, paths: newPaths }
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (action === "remove") {
|
|
774
|
+
if (!paths.includes(cwd)) {
|
|
775
|
+
// Check if it matches via glob but isn't exact
|
|
776
|
+
const isMatched = matchesAutoRegisterPath(cwd, paths);
|
|
777
|
+
if (isMatched) {
|
|
778
|
+
return result(
|
|
779
|
+
`Current folder matches a glob pattern but isn't an exact entry.\nManually edit ~/.pi/agent/pi-messenger.json to modify glob patterns.`,
|
|
780
|
+
{ mode: "autoRegisterPath", action: "remove", notExact: true, path: cwd }
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
return result(
|
|
784
|
+
`Current folder not in auto-register paths:\n ${cwd}`,
|
|
785
|
+
{ mode: "autoRegisterPath", action: "remove", notFound: true, path: cwd }
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const newPaths = paths.filter(p => p !== cwd);
|
|
790
|
+
saveAutoRegisterPaths(newPaths);
|
|
791
|
+
|
|
792
|
+
return result(
|
|
793
|
+
`Removed from auto-register paths:\n ${cwd}\n\nAgents starting in this folder will no longer auto-join.`,
|
|
794
|
+
{ mode: "autoRegisterPath", action: "remove", path: cwd, paths: newPaths }
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return result("Invalid action. Use: add, remove, or list", { mode: "autoRegisterPath", error: "invalid_action" });
|
|
799
|
+
}
|