@cryptolibertus/pi-peer 0.3.2
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/LICENSE +21 -0
- package/README.md +70 -0
- package/extensions/pi-peer/index.ts +753 -0
- package/package.json +58 -0
- package/src/peers/command.mjs +289 -0
- package/src/peers/comms.mjs +676 -0
- package/src/peers/config.mjs +356 -0
- package/src/peers/extension-lifecycle.mjs +21 -0
- package/src/peers/goal-board.mjs +528 -0
- package/src/peers/guidance.mjs +45 -0
- package/src/peers/inbound-bridge.mjs +240 -0
- package/src/peers/local-transport.mjs +814 -0
- package/src/peers/message-store.mjs +114 -0
- package/src/peers/protocol.mjs +256 -0
- package/src/peers/role-collaboration-demo.mjs +71 -0
- package/src/peers/runtime.mjs +200 -0
- package/src/peers/status.mjs +158 -0
- package/src/peers/tool-results.mjs +154 -0
- package/src/utils.mjs +83 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
|
+
|
|
5
|
+
import { installPeerRuntimeLifecycle } from "../../src/peers/extension-lifecycle.mjs";
|
|
6
|
+
import { initPeerConfig } from "../../src/peers/config.mjs";
|
|
7
|
+
import { formatPeerCommandError, formatPeerHelp, formatPeerInitResult, parsePeerCommand } from "../../src/peers/command.mjs";
|
|
8
|
+
import { createPeerRuntime, getPeerRuntimeValue } from "../../src/peers/runtime.mjs";
|
|
9
|
+
import { appendPeerGoalEvent, beginPeerGoalTask, closePeerGoal, completePeerGoalTask, createPeerGoal, formatPeerGoal, formatPeerGoalList, loadPeerGoalBoard, recordPeerGoalTaskDispatch } from "../../src/peers/goal-board.mjs";
|
|
10
|
+
import { collectPeerRuntimeStatus, derivePeerDoctorReport, formatPeerDoctorText, formatPeerStatusLines, formatPeerStatusText } from "../../src/peers/status.mjs";
|
|
11
|
+
import {
|
|
12
|
+
peerAwaitToolResult,
|
|
13
|
+
peerGetToolResult,
|
|
14
|
+
peerListToolResult,
|
|
15
|
+
peerSendQueuedToolResult,
|
|
16
|
+
peerSendResponseToolResult,
|
|
17
|
+
peerSendTimeoutToolResult,
|
|
18
|
+
} from "../../src/peers/tool-results.mjs";
|
|
19
|
+
import { PEER_TOOL_NAMES, PEER_TOOL_PROMPT_GUIDELINES } from "../../src/peers/guidance.mjs";
|
|
20
|
+
|
|
21
|
+
const MESSAGE_TYPE = "pi-peer";
|
|
22
|
+
const runtimeByCwd = new Map<string, Promise<any>>();
|
|
23
|
+
|
|
24
|
+
export default function piPeerExtension(pi: ExtensionAPI) {
|
|
25
|
+
let activeContext: any;
|
|
26
|
+
|
|
27
|
+
pi.registerMessageRenderer(MESSAGE_TYPE, (message, _options, theme) => {
|
|
28
|
+
const content = `🔗 Peer\n${String(message.content || "")}`;
|
|
29
|
+
return new Text(theme?.fg ? theme.fg("accent", content) : content, 0, 0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
installPeerRuntimeLifecycle(pi, { runtimeFor: (cwd: string) => runtimeFor(pi, cwd) });
|
|
33
|
+
|
|
34
|
+
pi.on("session_start", async (_event, ctx = {}) => {
|
|
35
|
+
activeContext = ctx;
|
|
36
|
+
const runtime = await runtimeFor(pi, ctx.cwd);
|
|
37
|
+
attachPeerUi(runtime, () => activeContext, (current: any) => refreshPeerUi(current, runtime));
|
|
38
|
+
await refreshPeerUi(ctx, runtime);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
pi.on("agent_end", async (_event, ctx = {}) => {
|
|
42
|
+
activeContext = ctx;
|
|
43
|
+
const runtime = await runtimeFor(pi, ctx.cwd);
|
|
44
|
+
await refreshPeerUi(ctx, runtime);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
pi.on("session_shutdown", async (_event, ctx = {}) => {
|
|
48
|
+
await refreshPeerUi(ctx, await runtimeFor(pi, ctx.cwd));
|
|
49
|
+
activeContext = undefined;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
pi.registerCommand("peer", {
|
|
53
|
+
description: "Pi-to-Pi peers: setup, doctor, status, list, send, get, await, progress, goal",
|
|
54
|
+
getArgumentCompletions: (prefix: string) => ["help", "status", "list", "init", "setup", "doctor", "reconnect", "resume", "cancel", "send", "get", "await", "progress", "goal", "goals", "ls", "current", "fanout", "claim", "take", "done", "complete", "block", "objection", "unblock", "pass", "fail"]
|
|
55
|
+
.filter((value) => value.startsWith(prefix))
|
|
56
|
+
.map((value) => ({ value, label: value })),
|
|
57
|
+
handler: async (rawArgs, ctx) => {
|
|
58
|
+
activeContext = ctx;
|
|
59
|
+
await handlePeerCommand(pi, rawArgs, ctx, () => refreshPeerUi(ctx, undefined));
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
pi.registerTool({
|
|
64
|
+
name: PEER_TOOL_NAMES.list,
|
|
65
|
+
label: "Peer List",
|
|
66
|
+
description: "List configured local Pi-to-Pi peers and their prototype capabilities.",
|
|
67
|
+
promptSnippet: "Discover configured Pi-to-Pi peers before sending a peer prompt.",
|
|
68
|
+
promptGuidelines: PEER_TOOL_PROMPT_GUIDELINES[PEER_TOOL_NAMES.list],
|
|
69
|
+
parameters: Type.Object({}),
|
|
70
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
71
|
+
const runtime = await runtimeFor(pi, ctx?.cwd);
|
|
72
|
+
attachPeerUi(runtime, () => activeContext, (current: any) => refreshPeerUi(current, runtime));
|
|
73
|
+
if (runtime.enabled) await runtime.refreshLocalPeers();
|
|
74
|
+
const peers = runtime.enabled ? await runtime.comms.listPeers() : [];
|
|
75
|
+
await refreshPeerUi(ctx, runtime);
|
|
76
|
+
return peerListToolResult(runtime, peers);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
pi.registerTool({
|
|
81
|
+
name: PEER_TOOL_NAMES.send,
|
|
82
|
+
label: "Peer Send",
|
|
83
|
+
description: "Send a prompt-first Pi-to-Pi message to a configured local peer.",
|
|
84
|
+
promptSnippet: "Send an inbound prompt to a Pi peer; use peer_await if returned pending.",
|
|
85
|
+
promptGuidelines: PEER_TOOL_PROMPT_GUIDELINES[PEER_TOOL_NAMES.send],
|
|
86
|
+
parameters: Type.Object({
|
|
87
|
+
peer: Type.String({ description: "Configured peer id" }),
|
|
88
|
+
prompt: Type.String({ description: "Prompt to deliver to the peer" }),
|
|
89
|
+
conversationId: Type.Optional(Type.String({ description: "Existing conversation id to continue" })),
|
|
90
|
+
intent: Type.Optional(Type.String({ description: "ask, review, notify, coordinate, task, or custom; defaults to ask" })),
|
|
91
|
+
await: Type.Optional(Type.Boolean({ description: "Wait for the final assistant message before returning; defaults true" })),
|
|
92
|
+
timeoutMs: Type.Optional(Type.Number({ description: "Optional await timeout in milliseconds" })),
|
|
93
|
+
maxHopCount: Type.Optional(Type.Number({ description: "Maximum peer route hops; defaults to the peer descriptor or 1" })),
|
|
94
|
+
allowSelf: Type.Optional(Type.Boolean({ description: "Allow sending to the current peer; defaults false because self-targeting is usually accidental" })),
|
|
95
|
+
contextRefs: Type.Optional(Type.Array(Type.Object({ type: Type.String(), value: Type.String() }))),
|
|
96
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
97
|
+
claimedPaths: Type.Optional(Type.Array(Type.String({ description: "Paths this peer task is expected to own while running" }))),
|
|
98
|
+
goalId: Type.Optional(Type.String({ description: "Peer goal id to link this long-running task to" })),
|
|
99
|
+
goalClaimMode: Type.Optional(Type.String({ description: "Goal-board claim mode for claimedPaths; defaults to write" })),
|
|
100
|
+
goalStaleAfterMs: Type.Optional(Type.Number({ description: "Milliseconds before this goal claim is considered stale without heartbeat" })),
|
|
101
|
+
}),
|
|
102
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
103
|
+
const runtime = await runtimeFor(pi, ctx?.cwd);
|
|
104
|
+
attachPeerUi(runtime, () => activeContext, (current: any) => refreshPeerUi(current, runtime));
|
|
105
|
+
ensureEnabled(runtime);
|
|
106
|
+
await runtime.refreshLocalPeers();
|
|
107
|
+
const metadata = mergePeerMetadata(params.metadata, params.claimedPaths, params.goalId);
|
|
108
|
+
const goalLink = await beginPeerSendGoalLink(ctx?.cwd, runtime, {
|
|
109
|
+
goalId: params.goalId,
|
|
110
|
+
targetPeerId: params.peer,
|
|
111
|
+
prompt: params.prompt,
|
|
112
|
+
claimedPaths: metadata.claimedPaths,
|
|
113
|
+
claimMode: params.goalClaimMode,
|
|
114
|
+
staleAfterMs: params.goalStaleAfterMs,
|
|
115
|
+
});
|
|
116
|
+
if (goalLink?.claimEvent?.id) metadata.goalClaimId = goalLink.claimEvent.id;
|
|
117
|
+
let handle: any;
|
|
118
|
+
try {
|
|
119
|
+
handle = await runtime.comms.sendMessage(params.peer, {
|
|
120
|
+
prompt: withPeerGoalInstructions(params.prompt, goalLink),
|
|
121
|
+
intent: params.intent || "ask",
|
|
122
|
+
contextRefs: params.contextRefs || [],
|
|
123
|
+
metadata,
|
|
124
|
+
}, {
|
|
125
|
+
conversationId: params.conversationId,
|
|
126
|
+
maxHopCount: Number.isInteger(params.maxHopCount) ? params.maxHopCount : undefined,
|
|
127
|
+
allowSelf: params.allowSelf === true,
|
|
128
|
+
});
|
|
129
|
+
} catch (error: any) {
|
|
130
|
+
await recordPeerSendGoalFailure(ctx?.cwd, goalLink, {
|
|
131
|
+
targetPeerId: params.peer,
|
|
132
|
+
prompt: params.prompt,
|
|
133
|
+
claimedPaths: metadata.claimedPaths,
|
|
134
|
+
error,
|
|
135
|
+
});
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
await recordPeerSendGoalDispatch(ctx?.cwd, runtime, goalLink, handle, {
|
|
139
|
+
targetPeerId: params.peer,
|
|
140
|
+
prompt: params.prompt,
|
|
141
|
+
claimedPaths: metadata.claimedPaths,
|
|
142
|
+
});
|
|
143
|
+
trackPeerSendGoalCompletion(ctx?.cwd, goalLink, handle, {
|
|
144
|
+
targetPeerId: params.peer,
|
|
145
|
+
prompt: params.prompt,
|
|
146
|
+
claimedPaths: metadata.claimedPaths,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await refreshPeerUi(ctx, runtime);
|
|
150
|
+
if (params.await === false) return peerSendQueuedToolResult(handle);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const response = await runtime.comms.awaitMessage(handle.messageId, { timeoutMs: params.timeoutMs });
|
|
154
|
+
await refreshPeerUi(ctx, runtime);
|
|
155
|
+
return peerSendResponseToolResult(handle, response);
|
|
156
|
+
} catch (error: any) {
|
|
157
|
+
if (error?.code === "PI_PEER_AWAIT_TIMEOUT") {
|
|
158
|
+
const message = await runtime.comms.getMessage(handle.messageId);
|
|
159
|
+
await refreshPeerUi(ctx, runtime);
|
|
160
|
+
return peerSendTimeoutToolResult(handle, error, message);
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
pi.registerTool({
|
|
168
|
+
name: PEER_TOOL_NAMES.progress,
|
|
169
|
+
label: "Peer Progress",
|
|
170
|
+
description: "Send a structured progress checkpoint from the current inbound peer task.",
|
|
171
|
+
promptSnippet: "Report progress during a long-running inbound peer task before final handoff.",
|
|
172
|
+
promptGuidelines: PEER_TOOL_PROMPT_GUIDELINES[PEER_TOOL_NAMES.progress],
|
|
173
|
+
parameters: Type.Object({
|
|
174
|
+
summary: Type.String({ description: "Concise progress summary" }),
|
|
175
|
+
status: Type.Optional(Type.String({ description: "running, blocked, testing, reviewing, or custom status" })),
|
|
176
|
+
phase: Type.Optional(Type.String({ description: "Optional phase/checkpoint name" })),
|
|
177
|
+
detail: Type.Optional(Type.Unknown({ description: "Optional structured detail; do not include secrets" })),
|
|
178
|
+
}),
|
|
179
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
180
|
+
const runtime = await runtimeFor(pi, ctx?.cwd);
|
|
181
|
+
attachPeerUi(runtime, () => activeContext, (current: any) => refreshPeerUi(current, runtime));
|
|
182
|
+
ensureEnabled(runtime);
|
|
183
|
+
const result = runtime.recordInboundProgress(params);
|
|
184
|
+
await refreshPeerUi(ctx, runtime);
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: "text", text: result.ok ? `Progress sent for ${result.messageId}: ${params.summary}` : `No active inbound peer task: ${result.reason || "unknown"}` }],
|
|
187
|
+
details: { ok: result.ok === true, kind: "peer_progress", ...result },
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
pi.registerTool({
|
|
193
|
+
name: PEER_TOOL_NAMES.get,
|
|
194
|
+
label: "Peer Get",
|
|
195
|
+
description: "Inspect a peer, conversation, message, runtime summary, or redacted audit entries by id.",
|
|
196
|
+
promptSnippet: "Inspect peer messaging state by message id, conversation id, peer id, 'runtime', or 'audit'.",
|
|
197
|
+
promptGuidelines: PEER_TOOL_PROMPT_GUIDELINES[PEER_TOOL_NAMES.get],
|
|
198
|
+
parameters: Type.Object({
|
|
199
|
+
id: Type.String({ description: "Peer id, conversation id, message id, 'runtime', or 'audit'" }),
|
|
200
|
+
}),
|
|
201
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
202
|
+
const runtime = await runtimeFor(pi, ctx?.cwd);
|
|
203
|
+
attachPeerUi(runtime, () => activeContext, (current: any) => refreshPeerUi(current, runtime));
|
|
204
|
+
ensureEnabled(runtime);
|
|
205
|
+
if (runtime.enabled) await runtime.refreshLocalPeers();
|
|
206
|
+
const { type, value } = await getPeerRuntimeValue(runtime, params.id);
|
|
207
|
+
await refreshPeerUi(ctx, runtime);
|
|
208
|
+
return peerGetToolResult(params.id, type, value);
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
pi.registerTool({
|
|
213
|
+
name: PEER_TOOL_NAMES.await,
|
|
214
|
+
label: "Peer Await",
|
|
215
|
+
description: "Wait for one or more pending Pi-to-Pi peer messages to return final assistant messages.",
|
|
216
|
+
promptSnippet: "Join pending peer_send handles and read final assistant messages.",
|
|
217
|
+
promptGuidelines: PEER_TOOL_PROMPT_GUIDELINES[PEER_TOOL_NAMES.await],
|
|
218
|
+
parameters: Type.Object({
|
|
219
|
+
messageId: Type.Optional(Type.String({ description: "Single message id to await" })),
|
|
220
|
+
messageIds: Type.Optional(Type.Array(Type.String({ description: "Message ids to await" }))),
|
|
221
|
+
timeoutMs: Type.Optional(Type.Number({ description: "Optional await timeout in milliseconds" })),
|
|
222
|
+
}),
|
|
223
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
224
|
+
const runtime = await runtimeFor(pi, ctx?.cwd);
|
|
225
|
+
attachPeerUi(runtime, () => activeContext, (current: any) => refreshPeerUi(current, runtime));
|
|
226
|
+
ensureEnabled(runtime);
|
|
227
|
+
const messageIds = params.messageIds || (params.messageId ? [params.messageId] : []);
|
|
228
|
+
if (!messageIds.length) throw new Error("peer_await requires messageId or messageIds");
|
|
229
|
+
const responses = [];
|
|
230
|
+
for (const messageId of messageIds) {
|
|
231
|
+
try {
|
|
232
|
+
responses.push({ messageId, response: await runtime.comms.awaitMessage(messageId, { timeoutMs: params.timeoutMs }) });
|
|
233
|
+
} catch (error: any) {
|
|
234
|
+
const message = error?.code === "PI_PEER_AWAIT_TIMEOUT" ? await runtime.comms.getMessage(messageId) : undefined;
|
|
235
|
+
responses.push({ messageId, error: { message: error?.message || String(error), code: error?.code || "PI_PEER_AWAIT_ERROR" }, message });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
await refreshPeerUi(ctx, runtime);
|
|
239
|
+
return peerAwaitToolResult(responses);
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
async function refreshPeerUi(ctx: any, runtime?: any) {
|
|
244
|
+
if (!ctx?.hasUI || !ctx.ui?.setStatus || !ctx.ui?.setWidget) return;
|
|
245
|
+
try {
|
|
246
|
+
const resolved = runtime || await runtimeFor(pi, ctx.cwd);
|
|
247
|
+
if (resolved.enabled) await resolved.refreshLocalPeers().catch(() => []);
|
|
248
|
+
const status = await collectPeerRuntimeStatus(resolved);
|
|
249
|
+
const lines = formatPeerStatusLines(status);
|
|
250
|
+
const headline = lines[0];
|
|
251
|
+
const theme = ctx.ui.theme;
|
|
252
|
+
const color = (line: any) => theme?.fg ? theme.fg(line.color, line.text) : line.text;
|
|
253
|
+
ctx.ui.setStatus("peer", theme?.fg ? theme.fg(headline.color, headline.text) : headline.text);
|
|
254
|
+
ctx.ui.setWidget("peer", lines.map(color), { placement: "belowEditor" });
|
|
255
|
+
} catch {
|
|
256
|
+
// Peer UI is best-effort and must stay safe in non-UI/RPC/print contexts.
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function handlePeerCommand(pi: ExtensionAPI, rawArgs: string, ctx: any, refresh: () => Promise<void>) {
|
|
262
|
+
const parsed = parsePeerCommand(rawArgs);
|
|
263
|
+
if (parsed.error) return sendPeerMessage(pi, formatPeerCommandError(parsed.error));
|
|
264
|
+
if (parsed.subcommand === "help") return sendPeerMessage(pi, formatPeerHelp());
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
if (parsed.subcommand === "init" || parsed.subcommand === "setup") {
|
|
268
|
+
const result = await initPeerConfig(ctx.cwd || process.cwd(), { localPeerId: parsed.localPeerId, role: parsed.role, persona: parsed.persona, trust: parsed.trust, capabilities: parsed.capabilities, seedPeers: parsed.seedPeers, enabled: parsed.enabled });
|
|
269
|
+
await resetRuntimeFor(ctx.cwd);
|
|
270
|
+
const runtime = await runtimeFor(pi, ctx.cwd);
|
|
271
|
+
if (runtime.enabled) await runtime.start(ctx);
|
|
272
|
+
await refresh();
|
|
273
|
+
const suffix = parsed.subcommand === "setup" ? "\n\nNext: start another Pi session with PI_PEER_ID=<peer-id> pi, then run /peer doctor or /peer list." : "";
|
|
274
|
+
return sendPeerMessage(pi, `${formatPeerInitResult(result)}${suffix}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const runtime = await runtimeFor(pi, ctx?.cwd);
|
|
278
|
+
if (parsed.subcommand === "status") {
|
|
279
|
+
if (runtime.enabled) await runtime.refreshLocalPeers();
|
|
280
|
+
await refresh();
|
|
281
|
+
return sendPeerMessage(pi, formatPeerStatusText(await collectPeerRuntimeStatus(runtime)));
|
|
282
|
+
}
|
|
283
|
+
if (parsed.subcommand === "doctor") {
|
|
284
|
+
if (runtime.enabled) await runtime.refreshLocalPeers();
|
|
285
|
+
const status = await collectPeerRuntimeStatus(runtime);
|
|
286
|
+
await refresh();
|
|
287
|
+
return sendPeerMessage(pi, formatPeerDoctorText(derivePeerDoctorReport(status)));
|
|
288
|
+
}
|
|
289
|
+
if (parsed.subcommand === "reconnect") {
|
|
290
|
+
const peers = runtime.enabled ? await runtime.refreshLocalPeers() : [];
|
|
291
|
+
await refresh();
|
|
292
|
+
return sendPeerMessage(pi, `Peer discovery refreshed: ${peers.length} discovered endpoint${peers.length === 1 ? "" : "s"}.\n\n${formatPeerStatusText(await collectPeerRuntimeStatus(runtime))}`);
|
|
293
|
+
}
|
|
294
|
+
if (parsed.subcommand === "list") {
|
|
295
|
+
if (runtime.enabled) await runtime.refreshLocalPeers();
|
|
296
|
+
const peers = runtime.enabled ? await runtime.comms.listPeers() : [];
|
|
297
|
+
await refresh();
|
|
298
|
+
return sendPeerMessage(pi, peerListToolResult(runtime, peers).content[0].text);
|
|
299
|
+
}
|
|
300
|
+
if (parsed.subcommand === "goal") {
|
|
301
|
+
const text = await handlePeerGoalCommand(parsed, ctx, runtime);
|
|
302
|
+
await refresh();
|
|
303
|
+
return sendPeerMessage(pi, text);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
ensureEnabled(runtime);
|
|
307
|
+
if (parsed.subcommand === "progress") {
|
|
308
|
+
const result = runtime.recordInboundProgress({ summary: parsed.summary, status: parsed.status, phase: parsed.phase, detail: parsed.detail });
|
|
309
|
+
await refresh();
|
|
310
|
+
return sendPeerMessage(pi, result.ok ? `Progress sent for ${result.messageId}: ${parsed.summary}` : `No active inbound peer task: ${result.reason || "unknown"}`);
|
|
311
|
+
}
|
|
312
|
+
if (parsed.subcommand === "send") {
|
|
313
|
+
await runtime.refreshLocalPeers();
|
|
314
|
+
const metadata = { ...(parsed.metadata || {}) };
|
|
315
|
+
const goalLink = await beginPeerSendGoalLink(ctx?.cwd, runtime, {
|
|
316
|
+
goalId: parsed.goalId,
|
|
317
|
+
targetPeerId: parsed.peerId,
|
|
318
|
+
prompt: parsed.prompt,
|
|
319
|
+
claimedPaths: parsed.claimedPaths,
|
|
320
|
+
claimMode: parsed.goalClaimMode,
|
|
321
|
+
staleAfterMs: parsed.goalStaleAfterMs,
|
|
322
|
+
});
|
|
323
|
+
if (goalLink?.claimEvent?.id) metadata.goalClaimId = goalLink.claimEvent.id;
|
|
324
|
+
let handle: any;
|
|
325
|
+
try {
|
|
326
|
+
handle = await runtime.comms.sendMessage(parsed.peerId, { prompt: withPeerGoalInstructions(parsed.prompt, goalLink), intent: parsed.intent, metadata }, { maxHopCount: parsed.maxHopCount, allowSelf: parsed.allowSelf });
|
|
327
|
+
} catch (error: any) {
|
|
328
|
+
await recordPeerSendGoalFailure(ctx?.cwd, goalLink, {
|
|
329
|
+
targetPeerId: parsed.peerId,
|
|
330
|
+
prompt: parsed.prompt,
|
|
331
|
+
claimedPaths: parsed.claimedPaths,
|
|
332
|
+
error,
|
|
333
|
+
});
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
await recordPeerSendGoalDispatch(ctx?.cwd, runtime, goalLink, handle, {
|
|
337
|
+
targetPeerId: parsed.peerId,
|
|
338
|
+
prompt: parsed.prompt,
|
|
339
|
+
claimedPaths: parsed.claimedPaths,
|
|
340
|
+
});
|
|
341
|
+
trackPeerSendGoalCompletion(ctx?.cwd, goalLink, handle, {
|
|
342
|
+
targetPeerId: parsed.peerId,
|
|
343
|
+
prompt: parsed.prompt,
|
|
344
|
+
claimedPaths: parsed.claimedPaths,
|
|
345
|
+
});
|
|
346
|
+
await refresh();
|
|
347
|
+
if (!parsed.awaitResponse) return sendPeerMessage(pi, peerSendQueuedToolResult(handle).content[0].text);
|
|
348
|
+
try {
|
|
349
|
+
const response = await runtime.comms.awaitMessage(handle.messageId, { timeoutMs: parsed.timeoutMs });
|
|
350
|
+
await refresh();
|
|
351
|
+
return sendPeerMessage(pi, peerSendResponseToolResult(handle, response).content[0].text);
|
|
352
|
+
} catch (error: any) {
|
|
353
|
+
if (error?.code === "PI_PEER_AWAIT_TIMEOUT") {
|
|
354
|
+
const message = await runtime.comms.getMessage(handle.messageId);
|
|
355
|
+
await refresh();
|
|
356
|
+
return sendPeerMessage(pi, peerSendTimeoutToolResult(handle, error, message).content[0].text);
|
|
357
|
+
}
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (parsed.subcommand === "resume") {
|
|
362
|
+
if (runtime.enabled) await runtime.refreshLocalPeers();
|
|
363
|
+
const handle = await runtime.comms.resumeMessage(parsed.messageId);
|
|
364
|
+
await refresh();
|
|
365
|
+
return sendPeerMessage(pi, `Peer message resumed: ${handle.messageId} in ${handle.conversationId}. Use /peer await ${handle.messageId} to wait for completion.`);
|
|
366
|
+
}
|
|
367
|
+
if (parsed.subcommand === "cancel") {
|
|
368
|
+
const message = await runtime.comms.cancelMessage(parsed.messageId, parsed.reason);
|
|
369
|
+
await refresh();
|
|
370
|
+
return sendPeerMessage(pi, `Peer message cancelled: ${parsed.messageId}${message?.conversationId ? ` in ${message.conversationId}` : ""}.`);
|
|
371
|
+
}
|
|
372
|
+
if (parsed.subcommand === "get") {
|
|
373
|
+
if (runtime.enabled) await runtime.refreshLocalPeers();
|
|
374
|
+
const { type, value } = await getPeerRuntimeValue(runtime, parsed.id);
|
|
375
|
+
await refresh();
|
|
376
|
+
return sendPeerMessage(pi, peerGetToolResult(parsed.id, type, value).content[0].text);
|
|
377
|
+
}
|
|
378
|
+
if (parsed.subcommand === "await") {
|
|
379
|
+
const responses = [];
|
|
380
|
+
for (const messageId of parsed.messageIds) {
|
|
381
|
+
try {
|
|
382
|
+
responses.push({ messageId, response: await runtime.comms.awaitMessage(messageId, { timeoutMs: parsed.timeoutMs }) });
|
|
383
|
+
} catch (error: any) {
|
|
384
|
+
const message = error?.code === "PI_PEER_AWAIT_TIMEOUT" ? await runtime.comms.getMessage(messageId) : undefined;
|
|
385
|
+
responses.push({ messageId, error: { message: error?.message || String(error), code: error?.code || "PI_PEER_AWAIT_ERROR" }, message });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
await refresh();
|
|
389
|
+
return sendPeerMessage(pi, peerAwaitToolResult(responses).content[0].text);
|
|
390
|
+
}
|
|
391
|
+
} catch (error: any) {
|
|
392
|
+
await refresh().catch(() => {});
|
|
393
|
+
return sendPeerMessage(pi, formatPeerCommandError(error?.message || String(error)));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function handlePeerGoalCommand(parsed: any, ctx: any, runtime: any) {
|
|
398
|
+
const root = ctx?.cwd || process.cwd();
|
|
399
|
+
const peerId = runtime?.localPeerId || runtime?.summary?.localPeerId || "unknown";
|
|
400
|
+
if (parsed.goalAction === "list") return formatPeerGoalList(await loadPeerGoalBoard(root));
|
|
401
|
+
if (parsed.goalAction === "create") {
|
|
402
|
+
const goal = await createPeerGoal(root, { objective: parsed.objective, constraints: parsed.constraints, peerId });
|
|
403
|
+
return `${formatPeerGoal(goal)}\n\nNext: peers can post findings, claim work, object, vote, and hand off with /peer goal <action> ${goal.id} ...`;
|
|
404
|
+
}
|
|
405
|
+
if (parsed.goalAction === "show") {
|
|
406
|
+
const board = await loadPeerGoalBoard(root);
|
|
407
|
+
const goalId = parsed.goalId || board.currentGoalId;
|
|
408
|
+
const goal = goalId ? board.goals[goalId] : undefined;
|
|
409
|
+
if (!goal) throw new Error(goalId ? `peer goal ${goalId} not found` : "no current peer goal");
|
|
410
|
+
return formatPeerGoal(goal);
|
|
411
|
+
}
|
|
412
|
+
if (parsed.goalAction === "fanout") return handlePeerGoalFanout(parsed, ctx, runtime, peerId);
|
|
413
|
+
if (["task", "finding", "handoff", "note"].includes(parsed.goalAction)) {
|
|
414
|
+
const result = await appendPeerGoalEvent(root, parsed.goalId, {
|
|
415
|
+
type: parsed.eventType,
|
|
416
|
+
peerId,
|
|
417
|
+
summary: parsed.summary,
|
|
418
|
+
severity: parsed.severity,
|
|
419
|
+
paths: parsed.paths,
|
|
420
|
+
taskId: parsed.taskId,
|
|
421
|
+
status: parsed.status,
|
|
422
|
+
});
|
|
423
|
+
return `Posted ${result.event.type} ${result.event.id} to ${result.goal.id}.\n\n${formatPeerGoal(result.goal)}`;
|
|
424
|
+
}
|
|
425
|
+
if (parsed.goalAction === "claim") {
|
|
426
|
+
const result = await appendPeerGoalEvent(root, parsed.goalId, {
|
|
427
|
+
type: "claim",
|
|
428
|
+
peerId,
|
|
429
|
+
summary: parsed.summary,
|
|
430
|
+
paths: parsed.paths,
|
|
431
|
+
mode: parsed.mode,
|
|
432
|
+
ttlMs: parsed.ttlMs,
|
|
433
|
+
staleAfterMs: parsed.staleAfterMs,
|
|
434
|
+
});
|
|
435
|
+
return `Claimed work ${result.event.id} on ${result.goal.id}.\n\n${formatPeerGoal(result.goal)}`;
|
|
436
|
+
}
|
|
437
|
+
if (parsed.goalAction === "heartbeat") {
|
|
438
|
+
const result = await appendPeerGoalEvent(root, parsed.goalId, {
|
|
439
|
+
type: "heartbeat",
|
|
440
|
+
peerId,
|
|
441
|
+
summary: parsed.summary,
|
|
442
|
+
resolves: parsed.resolves,
|
|
443
|
+
ttlMs: parsed.ttlMs,
|
|
444
|
+
staleAfterMs: parsed.staleAfterMs,
|
|
445
|
+
});
|
|
446
|
+
return `Refreshed claim ${parsed.resolves} with ${result.event.id}.\n\n${formatPeerGoal(result.goal)}`;
|
|
447
|
+
}
|
|
448
|
+
if (parsed.goalAction === "release") {
|
|
449
|
+
const result = await appendPeerGoalEvent(root, parsed.goalId, {
|
|
450
|
+
type: "release",
|
|
451
|
+
peerId,
|
|
452
|
+
summary: parsed.summary,
|
|
453
|
+
resolves: parsed.resolves,
|
|
454
|
+
});
|
|
455
|
+
return `Released claim ${parsed.resolves} with ${result.event.id}.\n\n${formatPeerGoal(result.goal)}`;
|
|
456
|
+
}
|
|
457
|
+
if (parsed.goalAction === "object") {
|
|
458
|
+
const result = await appendPeerGoalEvent(root, parsed.goalId, {
|
|
459
|
+
type: "objection",
|
|
460
|
+
peerId,
|
|
461
|
+
summary: parsed.summary,
|
|
462
|
+
paths: parsed.paths,
|
|
463
|
+
severity: parsed.severity || "blocking",
|
|
464
|
+
});
|
|
465
|
+
return `Posted objection ${result.event.id} to ${result.goal.id}.\n\n${formatPeerGoal(result.goal)}`;
|
|
466
|
+
}
|
|
467
|
+
if (parsed.goalAction === "resolve") {
|
|
468
|
+
const result = await appendPeerGoalEvent(root, parsed.goalId, {
|
|
469
|
+
type: "resolve",
|
|
470
|
+
peerId,
|
|
471
|
+
summary: parsed.summary,
|
|
472
|
+
resolves: parsed.resolves,
|
|
473
|
+
});
|
|
474
|
+
return `Resolved ${parsed.resolves} with ${result.event.id}.\n\n${formatPeerGoal(result.goal)}`;
|
|
475
|
+
}
|
|
476
|
+
if (parsed.goalAction === "vote") {
|
|
477
|
+
const result = await appendPeerGoalEvent(root, parsed.goalId, {
|
|
478
|
+
type: "vote",
|
|
479
|
+
peerId,
|
|
480
|
+
summary: parsed.summary,
|
|
481
|
+
verdict: parsed.verdict,
|
|
482
|
+
confidence: parsed.confidence,
|
|
483
|
+
});
|
|
484
|
+
return `Recorded vote ${result.event.id} on ${result.goal.id}.\n\n${formatPeerGoal(result.goal)}`;
|
|
485
|
+
}
|
|
486
|
+
if (parsed.goalAction === "close") {
|
|
487
|
+
const goal = await closePeerGoal(root, parsed.goalId, { peerId, summary: parsed.summary, force: parsed.force });
|
|
488
|
+
return `Closed peer goal ${goal.id}.\n\n${formatPeerGoal(goal)}`;
|
|
489
|
+
}
|
|
490
|
+
throw new Error(`Unknown peer goal action '${parsed.goalAction}'`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function handlePeerGoalFanout(parsed: any, ctx: any, runtime: any, peerId: string) {
|
|
494
|
+
const root = ctx?.cwd || process.cwd();
|
|
495
|
+
if (parsed.send) {
|
|
496
|
+
ensureEnabled(runtime);
|
|
497
|
+
await runtime.refreshLocalPeers();
|
|
498
|
+
}
|
|
499
|
+
const planned = [];
|
|
500
|
+
for (const targetPeerId of parsed.peers) {
|
|
501
|
+
const mode = inferFanoutClaimMode(targetPeerId);
|
|
502
|
+
const summary = `${parsed.objective} [fanout:${targetPeerId}]`;
|
|
503
|
+
const task = await appendPeerGoalEvent(root, parsed.goalId, {
|
|
504
|
+
type: "task",
|
|
505
|
+
peerId,
|
|
506
|
+
summary,
|
|
507
|
+
paths: parsed.paths,
|
|
508
|
+
status: parsed.send ? "dispatching" : "planned",
|
|
509
|
+
metadata: { targetPeerId, fanout: true, claimMode: mode },
|
|
510
|
+
});
|
|
511
|
+
planned.push({ peerId: targetPeerId, taskEventId: task.event.id, mode });
|
|
512
|
+
}
|
|
513
|
+
if (parsed.send) {
|
|
514
|
+
await Promise.all(planned.map(async (item: any) => {
|
|
515
|
+
let goalLink: any;
|
|
516
|
+
try {
|
|
517
|
+
goalLink = await beginPeerSendGoalLink(root, runtime, {
|
|
518
|
+
goalId: parsed.goalId,
|
|
519
|
+
targetPeerId: item.peerId,
|
|
520
|
+
prompt: parsed.objective,
|
|
521
|
+
claimedPaths: parsed.paths,
|
|
522
|
+
claimMode: item.mode,
|
|
523
|
+
staleAfterMs: parsed.staleAfterMs,
|
|
524
|
+
});
|
|
525
|
+
const metadata = mergePeerMetadata({ fanout: true }, parsed.paths, parsed.goalId);
|
|
526
|
+
if (goalLink?.claimEvent?.id) metadata.goalClaimId = goalLink.claimEvent.id;
|
|
527
|
+
const handle = await runtime.comms.sendMessage(item.peerId, {
|
|
528
|
+
prompt: withPeerGoalInstructions(buildFanoutPrompt(parsed.objective, item.peerId, item.mode), goalLink),
|
|
529
|
+
intent: item.mode === "write" ? "task" : "review",
|
|
530
|
+
metadata,
|
|
531
|
+
});
|
|
532
|
+
await recordPeerSendGoalDispatch(root, runtime, goalLink, handle, { targetPeerId: item.peerId, prompt: parsed.objective, claimedPaths: parsed.paths });
|
|
533
|
+
trackPeerSendGoalCompletion(root, goalLink, handle, { targetPeerId: item.peerId, prompt: parsed.objective, claimedPaths: parsed.paths });
|
|
534
|
+
item.messageId = handle.messageId;
|
|
535
|
+
item.conversationId = handle.conversationId;
|
|
536
|
+
item.handle = handle;
|
|
537
|
+
} catch (error: any) {
|
|
538
|
+
if (goalLink?.goalId) {
|
|
539
|
+
await recordPeerSendGoalFailure(root, goalLink, { targetPeerId: item.peerId, prompt: parsed.objective, claimedPaths: parsed.paths, error });
|
|
540
|
+
} else {
|
|
541
|
+
await appendPeerGoalEvent(root, parsed.goalId, {
|
|
542
|
+
type: "handoff",
|
|
543
|
+
peerId: item.peerId,
|
|
544
|
+
summary: `Fan-out dispatch failed before claim: ${error?.message || String(error)}`,
|
|
545
|
+
paths: parsed.paths,
|
|
546
|
+
taskId: item.taskEventId,
|
|
547
|
+
status: "blocked",
|
|
548
|
+
metadata: { fanout: true, targetPeerId: item.peerId },
|
|
549
|
+
}).catch(() => {});
|
|
550
|
+
}
|
|
551
|
+
item.error = { message: error?.message || String(error), code: error?.code || "PI_PEER_SEND_ERROR" };
|
|
552
|
+
}
|
|
553
|
+
}));
|
|
554
|
+
if (parsed.awaitResponse) {
|
|
555
|
+
await Promise.all(planned.filter((item: any) => item.handle).map(async (item: any) => {
|
|
556
|
+
try {
|
|
557
|
+
item.response = await runtime.comms.awaitMessage(item.handle.messageId, { timeoutMs: parsed.timeoutMs });
|
|
558
|
+
} catch (error: any) {
|
|
559
|
+
item.error = { message: error?.message || String(error), code: error?.code || "PI_PEER_AWAIT_ERROR" };
|
|
560
|
+
}
|
|
561
|
+
}));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const lines = [`Fan-out ${parsed.send ? "dispatched" : "planned"} for ${parsed.goalId}: ${parsed.objective}`];
|
|
565
|
+
for (const item of planned) {
|
|
566
|
+
lines.push(`- ${item.peerId} · ${item.mode}${item.messageId ? ` · ${item.messageId}` : ""}${item.error ? ` · ${item.error.code}` : ""}`);
|
|
567
|
+
}
|
|
568
|
+
lines.push("", "Final-answer checklist: include Fan-out used: yes, peer ids, message ids, blockers, and verification.");
|
|
569
|
+
return lines.join("\n");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function runtimeFor(pi: ExtensionAPI, cwd?: string) {
|
|
573
|
+
const key = cwd || process.cwd();
|
|
574
|
+
if (!runtimeByCwd.has(key)) runtimeByCwd.set(key, createPeerRuntime(key, { pi, homeDir: process.env.HOME || "" }));
|
|
575
|
+
return runtimeByCwd.get(key)!;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function resetRuntimeFor(cwd?: string) {
|
|
579
|
+
const key = cwd || process.cwd();
|
|
580
|
+
const pending = runtimeByCwd.get(key);
|
|
581
|
+
runtimeByCwd.delete(key);
|
|
582
|
+
const runtime = await pending?.catch(() => undefined);
|
|
583
|
+
await runtime?.dispose?.();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function attachPeerUi(runtime: any, activeContext: () => any, refresh: (ctx: any) => Promise<void>) {
|
|
587
|
+
if (!runtime?.comms || runtime.__peerUiAttached) return;
|
|
588
|
+
runtime.__peerUiAttached = true;
|
|
589
|
+
runtime.comms.subscribe(() => {
|
|
590
|
+
const ctx = activeContext();
|
|
591
|
+
if (ctx?.hasUI) void refresh(ctx);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function ensureEnabled(runtime: any) {
|
|
596
|
+
if (!runtime.enabled) throw new Error("Pi-to-Pi peer messaging is disabled for this project. Run /peer init or enable experimental.peerMessaging before using peer send/get/await.");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function sendPeerMessage(pi: ExtensionAPI, content: string) {
|
|
600
|
+
pi.sendMessage({ customType: MESSAGE_TYPE, content, display: true });
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function mergePeerMetadata(metadata: any, claimedPaths: unknown, goalId?: unknown) {
|
|
604
|
+
const base = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? { ...metadata } : {};
|
|
605
|
+
if (Array.isArray(claimedPaths)) {
|
|
606
|
+
const paths = [...new Set(claimedPaths.filter((item): item is string => typeof item === "string" && item.trim()).map((item) => item.trim()))];
|
|
607
|
+
if (paths.length) base.claimedPaths = paths;
|
|
608
|
+
}
|
|
609
|
+
if (typeof goalId === "string" && goalId.trim()) base.goalId = goalId.trim();
|
|
610
|
+
return base;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function beginPeerSendGoalLink(root: string | undefined, runtime: any, options: any) {
|
|
614
|
+
if (!options?.goalId) return undefined;
|
|
615
|
+
return beginPeerGoalTask(root || process.cwd(), options.goalId, {
|
|
616
|
+
requesterPeerId: runtime?.localPeerId || runtime?.summary?.localPeerId || "unknown",
|
|
617
|
+
targetPeerId: options.targetPeerId,
|
|
618
|
+
prompt: options.prompt,
|
|
619
|
+
claimedPaths: options.claimedPaths,
|
|
620
|
+
mode: options.claimMode || "write",
|
|
621
|
+
staleAfterMs: options.staleAfterMs,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function recordPeerSendGoalDispatch(root: string | undefined, runtime: any, goalLink: any, handle: any, options: any) {
|
|
626
|
+
if (!goalLink?.goalId) return;
|
|
627
|
+
await recordPeerGoalTaskDispatch(root || process.cwd(), goalLink.goalId, {
|
|
628
|
+
requesterPeerId: runtime?.localPeerId || runtime?.summary?.localPeerId || "unknown",
|
|
629
|
+
targetPeerId: options.targetPeerId,
|
|
630
|
+
prompt: options.prompt,
|
|
631
|
+
claimedPaths: options.claimedPaths,
|
|
632
|
+
messageId: handle.messageId,
|
|
633
|
+
conversationId: handle.conversationId,
|
|
634
|
+
claimEventId: goalLink.claimEvent?.id,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function recordPeerSendGoalFailure(root: string | undefined, goalLink: any, options: any) {
|
|
639
|
+
if (!goalLink?.goalId) return;
|
|
640
|
+
await completePeerGoalTask(root || process.cwd(), goalLink.goalId, {
|
|
641
|
+
targetPeerId: options.targetPeerId,
|
|
642
|
+
prompt: options.prompt,
|
|
643
|
+
claimedPaths: options.claimedPaths,
|
|
644
|
+
claimEventId: goalLink.claimEvent?.id,
|
|
645
|
+
status: "blocked",
|
|
646
|
+
responseStatus: "DISPATCH_ERROR",
|
|
647
|
+
summary: `DISPATCH_ERROR: ${options.error?.message || String(options.error || "peer send failed")}`,
|
|
648
|
+
releaseSummary: "Peer message dispatch failed before delivery",
|
|
649
|
+
}).catch(() => {});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function trackPeerSendGoalCompletion(root: string | undefined, goalLink: any, handle: any, options: any) {
|
|
653
|
+
if (!goalLink?.goalId || !handle?.response || typeof handle.response.then !== "function") return;
|
|
654
|
+
const boardRoot = root || process.cwd();
|
|
655
|
+
const heartbeatTimer = startPeerGoalClaimHeartbeat(boardRoot, goalLink, handle, options);
|
|
656
|
+
void handle.response.then(async (response: any) => {
|
|
657
|
+
await completePeerGoalTask(boardRoot, goalLink.goalId, {
|
|
658
|
+
targetPeerId: options.targetPeerId,
|
|
659
|
+
prompt: options.prompt,
|
|
660
|
+
claimedPaths: options.claimedPaths,
|
|
661
|
+
messageId: handle.messageId,
|
|
662
|
+
conversationId: handle.conversationId,
|
|
663
|
+
claimEventId: goalLink.claimEvent?.id,
|
|
664
|
+
status: response?.status === "OK" || response?.status === "OK_WITH_NOTES" ? "done" : "blocked",
|
|
665
|
+
responseStatus: response?.status,
|
|
666
|
+
summary: summarizePeerGoalResponse(response),
|
|
667
|
+
releaseSummary: `Peer message ${handle.messageId} completed with ${response?.status || "unknown"}`,
|
|
668
|
+
});
|
|
669
|
+
const missing = missingHandoffFields(response);
|
|
670
|
+
if (missing.length) {
|
|
671
|
+
await appendPeerGoalEvent(boardRoot, goalLink.goalId, {
|
|
672
|
+
type: "objection",
|
|
673
|
+
peerId: options.targetPeerId || "unknown",
|
|
674
|
+
summary: `Incomplete final handoff for ${handle.messageId}; missing ${missing.join(", ")}`,
|
|
675
|
+
severity: "blocking",
|
|
676
|
+
taskId: handle.messageId,
|
|
677
|
+
metadata: { messageId: handle.messageId, conversationId: handle.conversationId, missingHandoffFields: missing },
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}).catch(() => {}).finally(() => {
|
|
681
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function startPeerGoalClaimHeartbeat(root: string, goalLink: any, handle: any, options: any) {
|
|
686
|
+
const claimId = goalLink?.claimEvent?.id;
|
|
687
|
+
if (!goalLink?.goalId || !claimId) return undefined;
|
|
688
|
+
const staleAfterMs = Number.isFinite(Number(goalLink.claimEvent.staleAfterMs)) ? Number(goalLink.claimEvent.staleAfterMs) : undefined;
|
|
689
|
+
const intervalMs = Math.min(60_000, Math.max(1, Math.floor((staleAfterMs || 45 * 60 * 1000) / 2)));
|
|
690
|
+
const timer = setInterval(() => {
|
|
691
|
+
void appendPeerGoalEvent(root, goalLink.goalId, {
|
|
692
|
+
type: "heartbeat",
|
|
693
|
+
peerId: options.targetPeerId || "unknown",
|
|
694
|
+
resolves: claimId,
|
|
695
|
+
summary: `Peer message ${handle.messageId} still running`,
|
|
696
|
+
staleAfterMs,
|
|
697
|
+
metadata: {
|
|
698
|
+
messageId: handle.messageId,
|
|
699
|
+
conversationId: handle.conversationId,
|
|
700
|
+
},
|
|
701
|
+
}).catch(() => {});
|
|
702
|
+
}, intervalMs);
|
|
703
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
704
|
+
return timer;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function withPeerGoalInstructions(prompt: string, goalLink: any) {
|
|
708
|
+
if (!goalLink?.goalId) return prompt;
|
|
709
|
+
const lines = [
|
|
710
|
+
`Peer goal context:`,
|
|
711
|
+
`- goalId: ${goalLink.goalId}`,
|
|
712
|
+
...(goalLink.claimEvent?.id ? [`- claimEventId: ${goalLink.claimEvent.id}`, `- If this takes a while, send heartbeats with /peer goal heartbeat ${goalLink.goalId} ${goalLink.claimEvent.id} "still working".`] : []),
|
|
713
|
+
`- End with a concise handoff: status, files changed, verification, blockers.`,
|
|
714
|
+
``,
|
|
715
|
+
`Original prompt:`,
|
|
716
|
+
prompt,
|
|
717
|
+
];
|
|
718
|
+
return lines.join("\n");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function inferFanoutClaimMode(peerId: string) {
|
|
722
|
+
const id = String(peerId || "").toLowerCase();
|
|
723
|
+
return id.includes("worker") || id.includes("implement") ? "write" : "read";
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function buildFanoutPrompt(objective: string, peerId: string, mode: string) {
|
|
727
|
+
const role = mode === "write" ? "implementation lane" : "read-only research/review lane";
|
|
728
|
+
return `${objective}\n\nFan-out role for ${peerId}: ${role}. Stay within that lane. Report progress with peer_progress when work is long-running, and end with the required final handoff.`;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function missingHandoffFields(response: any) {
|
|
732
|
+
if (response?.status !== "OK" && response?.status !== "OK_WITH_NOTES") return [];
|
|
733
|
+
const text = typeof response?.finalAssistantMessage === "string" ? response.finalAssistantMessage : "";
|
|
734
|
+
if (!text.trim()) return ["Status", "Files changed", "Verification", "Blockers/risks", "Safe for review"];
|
|
735
|
+
const required = [
|
|
736
|
+
["Status", /status\s*:/i],
|
|
737
|
+
["Files changed", /files changed\s*:/i],
|
|
738
|
+
["Verification", /verification\s*:/i],
|
|
739
|
+
["Blockers/risks", /(?:blockers?|risks?|blockers?\s*\/\s*risks?)\s*:/i],
|
|
740
|
+
["Safe for review", /safe for review\s*:/i],
|
|
741
|
+
];
|
|
742
|
+
return required.filter(([, pattern]) => !pattern.test(text)).map(([name]) => name);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function summarizePeerGoalResponse(response: any) {
|
|
746
|
+
const status = response?.status || "unknown";
|
|
747
|
+
const text = typeof response?.summary === "string" && response.summary.trim()
|
|
748
|
+
? response.summary.trim()
|
|
749
|
+
: typeof response?.finalAssistantMessage === "string" && response.finalAssistantMessage.trim()
|
|
750
|
+
? response.finalAssistantMessage.trim()
|
|
751
|
+
: "Peer task completed";
|
|
752
|
+
return `${status}: ${text.replace(/\s+/g, " ").slice(0, 240)}`;
|
|
753
|
+
}
|