@cryptolibertus/pi-peer 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/peers/command.mjs +2 -2
- package/src/peers/goal-board.mjs +92 -7
- package/src/peers/guidance.mjs +1 -1
- package/src/peers/idle-watcher.mjs +18 -2
- package/src/peers/runtime.mjs +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ pi install ./packages/pi-peer
|
|
|
17
17
|
- Local peer discovery and transport using project `.pi/peers.json`
|
|
18
18
|
- Repo-scoped discovery: only Pi sessions in the same git repo/project appear as local peers
|
|
19
19
|
- Idle watcher daemon: idle peers nudge stuck inbound activations and proactively inspect open goal-board work
|
|
20
|
-
- Persona-aware scout routing: goal-board suggestions include recommended lanes, preferred roles, claim mode, and rationale so proactive peers can
|
|
20
|
+
- Persona-aware scout routing: goal-board suggestions include recommended lanes, preferred roles, claim mode, work keys, and rationale so proactive peers can self-select complementary work that fits their role/persona
|
|
21
21
|
- Protocol compatibility metadata (`protocolVersion`, min/max compatible versions), peer manifests, capabilities, and trust summaries in descriptors/status/list output
|
|
22
22
|
- `PI_PEER_ID` runtime override for running multiple local Pi sessions
|
|
23
23
|
- `pi-peer-publish` skill for safe npm release checks, version bumping, tag push, publish, and verification
|
|
@@ -58,7 +58,7 @@ Useful commands (long form and short aliases):
|
|
|
58
58
|
|
|
59
59
|
Short aliases keep common board updates terse: `/peer goals`/`/peer ls`, `/peer current`, `/peer scout`, `/peer fanout`, `/peer proposal`/`/peer propose`, `/peer take`/`/peer claim`, `/peer complete`/`/peer done`, `/peer objection`/`/peer block`, `/peer unblock`, `/peer note`, `/peer finding`, `/peer ping`/`/peer heartbeat`, `/peer drop`/`/peer release`, `/peer pass`, `/peer fail`, `/peer vote`, and `/peer close` map to the corresponding `/peer goal ...` actions.
|
|
60
60
|
|
|
61
|
-
The board is stored locally at `.pi/peer-goals.json`; outbound message snapshots are stored in `.pi/peer-messages.json` so restarted planners can still inspect disconnected historical tasks. Mutating goal-board operations take a short local lock before load/modify/save so concurrent peer appends do not drop events. `/peer send --goal <goal-id> --claim <path[,path]>` and the `peer_send` tool's `goalId`/`claimedPaths` parameters link long-running peer tasks to the board: Symphony records a task, claims overlapping write paths before dispatch, injects goal/heartbeat instructions into the peer prompt, keeps the claim alive with local heartbeats, and releases the claim after the peer returns a final response. Each goal-linked dispatch also gets a semantic work key (`goalId | lane | objective | mode | paths`) so duplicate read/review/research lanes are leased just like write paths; pass `--key <work-key>` / `workKey` for an explicit fingerprint and `--duplicate-policy allow-parallel` only when independent second opinions are intentional. The default dispatch policy is `reuse`, so a matching active work key returns the existing claim/task handle instead of starting another peer. `/peer goal fanout` turns a goal into role-specific peer lanes, while `peer_progress` reports checkpoints from an inbound long-running task. Scout suggestions are persona-aware: they surface a recommended lane (`research`, `review`, `coordination`, etc.), preferred roles, a safe default claim mode, rationale, and suppress suggestions already covered by active work keys. Active write claims conflict on overlapping paths; active semantic claims conflict on matching work keys; released, stale, or expired claims are kept visible but inactive. Claims become stale after 45 minutes without a heartbeat unless the claim or heartbeat sets `--stale-after-ms`.
|
|
61
|
+
The board is stored locally at `.pi/peer-goals.json`; outbound message snapshots are stored in `.pi/peer-messages.json` so restarted planners can still inspect disconnected historical tasks. Mutating goal-board operations take a short local lock before load/modify/save so concurrent peer appends do not drop events. `/peer send --goal <goal-id> --claim <path[,path]>` and the `peer_send` tool's `goalId`/`claimedPaths` parameters link long-running peer tasks to the board: Symphony records a task, claims overlapping write paths before dispatch, injects goal/heartbeat instructions into the peer prompt, keeps the claim alive with local heartbeats, and releases the claim after the peer returns a final response. Each goal-linked dispatch also gets a semantic work key (`goalId | lane | objective | mode | paths`) so duplicate read/review/research lanes are leased just like write paths; pass `--key <work-key>` / `workKey` for an explicit fingerprint and `--duplicate-policy allow-parallel` only when independent second opinions are intentional. The default dispatch policy is `reuse`, so a matching active work key returns the existing claim/task handle instead of starting another peer. `/peer goal fanout` turns a goal into role-specific peer lanes, while `peer_progress` reports checkpoints from an inbound long-running task. Scout suggestions are persona-aware: they surface a recommended lane (`research`, `review`, `implementation`, `coordination`, etc.), preferred roles, a safe default claim mode, a stable work key, rationale, and suppress suggestions already covered by active work keys. `/peer scout` prints the exact work key and a copyable `claim:` command for each read-only suggestion, so idle peers can claim the same semantic lane the scout is using instead of inventing generic seed keys. Empty goals emit multiple lane-specific read-only suggestions so idle peers can self-select research, review/QA, or implementation-planning work instead of waiting for a planner to assign lanes. Lane-tagged proposals (for example `/peer goal propose <goal> "Check package contents" --lane review --key review:package-contents`) become matching scout suggestions that reuse the proposal work key, letting the next suitable peer claim or review that proposed lane and suppress duplicate suggestions while the lane is active. Active write claims conflict on overlapping paths; active semantic claims conflict on matching work keys; released, stale, or expired claims are kept visible but inactive. Completed goal-linked tasks are shown with their handoff status instead of remaining visually stuck as `running`. Claims become stale after 45 minutes without a heartbeat unless the claim or heartbeat sets `--stale-after-ms`.
|
|
62
62
|
|
|
63
63
|
Normal goal closure requires at least one current passing vote, no current failed votes, no unresolved blocking objections, and no active write claims. Open proposals are intentionally non-blocking: they let peers show initiative without freezing closure. Stale write claims no longer block closure or new overlapping claims; use `/peer goal heartbeat` to revive work after a reconnect and `--force` only when intentionally overriding the readiness gate. Goal-linked tasks validate final handoff headings (`Status`, `Files changed`, `Verification`, `Blockers/risks`, `Safe for review`); missing sections create a blocking objection while still releasing the write claim. For multi-part work, use the fan-out gate: list peers, create/reuse a goal, delegate research/review/worker lanes, and include `Fan-out used: yes/no` plus peer handles in the final answer.
|
|
64
64
|
|
package/package.json
CHANGED
package/src/peers/command.mjs
CHANGED
|
@@ -146,8 +146,8 @@ export function formatPeerHelp() {
|
|
|
146
146
|
"- `/peer goal create <objective> [--constraint <a,b>]` — start a flat shared goal board",
|
|
147
147
|
"- `/peer goal list|show [goal-id]` — inspect peer goals, active claims, blockers, proposals, and votes",
|
|
148
148
|
"- `/peer goal fanout <goal-id> <objective> --peer <id[,id]> [--path <a,b>] [--send] [--no-await]` — plan or dispatch role-specific peer lanes",
|
|
149
|
-
"- `/peer goal scout [goal-id] [--limit <n>] [--include-closed]` — read-only proactive suggestions for what peers could do next",
|
|
150
|
-
"- `/peer goal task|finding|proposal|handoff|note <goal-id> <summary> [--path <a,b>] [--status done]` — post goal-board events",
|
|
149
|
+
"- `/peer goal scout [goal-id] [--limit <n>] [--include-closed]` — read-only proactive suggestions with exact work keys and copyable claim commands for what peers could do next",
|
|
150
|
+
"- `/peer goal task|finding|proposal|handoff|note <goal-id> <summary> [--path <a,b>] [--lane research|review|implementation] [--status done]` — post goal-board events; lane-tagged proposals become scout suggestions peers can self-select",
|
|
151
151
|
"- `/peer goal claim <goal-id> <task> --mode read|write --path <a,b> [--key <work-key>] [--duplicate-policy error|allow-parallel] [--ttl-ms <ms>]` — lease work without hierarchy",
|
|
152
152
|
"- `/peer goal heartbeat <goal-id> <claim-event-id> [summary] [--ttl-ms <ms>] [--stale-after-ms <ms>]` — refresh a live or stale claim",
|
|
153
153
|
"- `/peer goal release <goal-id> <claim-event-id> [summary]` — release a claimed lane",
|
package/src/peers/goal-board.mjs
CHANGED
|
@@ -16,9 +16,14 @@ const SCOUT_LANES = Object.freeze({
|
|
|
16
16
|
"stale-claim": { recommendedLane: "coordination", preferredRoles: ["planner", "coordinator"], claimMode: "read", suggestedIntent: "coordinate", rationale: "Stale claims need owner follow-up or release, not duplicate writes." },
|
|
17
17
|
"open-proposal": { recommendedLane: "coordination", preferredRoles: ["planner", "coordinator", "reviewer"], claimMode: "read", suggestedIntent: "review", rationale: "Open proposals need triage into accept, defer, or resolve decisions." },
|
|
18
18
|
close: { recommendedLane: "coordination", preferredRoles: ["planner", "coordinator", "reviewer"], claimMode: "read", suggestedIntent: "coordinate", rationale: "Ready goals need final closure checks and a concise handoff." },
|
|
19
|
-
"next-step": { recommendedLane: "research", preferredRoles: ["researcher", "reviewer", "planner", "coordinator"
|
|
19
|
+
"next-step": { recommendedLane: "research", preferredRoles: ["researcher", "reviewer", "planner", "coordinator"], claimMode: "read", suggestedIntent: "review", rationale: "Empty goals benefit from peers self-selecting read-only lanes before write claims." },
|
|
20
20
|
review: { recommendedLane: "review", preferredRoles: ["reviewer", "qa", "coordinator", "planner"], claimMode: "read", suggestedIntent: "review", rationale: "Goals without current votes need read-only validation before closure." },
|
|
21
21
|
});
|
|
22
|
+
const STARTUP_SCOUT_LANES = Object.freeze([
|
|
23
|
+
{ lane: "research", preferredRoles: ["researcher", "planner", "coordinator"], summary: "No active work yet; self-select a research lane to map risks, options, and next moves." },
|
|
24
|
+
{ lane: "review", preferredRoles: ["reviewer", "qa", "planner", "coordinator"], summary: "No active work yet; self-select a read-only review/QA lane to validate the plan and risks." },
|
|
25
|
+
{ lane: "implementation", preferredRoles: ["worker"], summary: "No active work yet; self-select an implementation-planning lane, then claim write paths only after naming them." },
|
|
26
|
+
]);
|
|
22
27
|
const GOAL_BOARD_LOCK_STALE_MS = 30_000;
|
|
23
28
|
const GOAL_BOARD_LOCK_RETRY_MS = 10;
|
|
24
29
|
const GOAL_BOARD_LOCK_TIMEOUT_MS = 5_000;
|
|
@@ -243,7 +248,7 @@ export function deriveGoalState(goal, options = {}) {
|
|
|
243
248
|
const failedVotes = currentVotes.filter((vote) => vote.verdict === "fail");
|
|
244
249
|
const passingVotes = currentVotes.filter((vote) => vote.verdict === "pass" || vote.verdict === "pass-with-risks");
|
|
245
250
|
const activeWriteClaims = activeClaims.filter((claim) => claim.mode === "write");
|
|
246
|
-
const tasks = events.filter((event) => event.type === "task").map(
|
|
251
|
+
const tasks = events.filter((event) => event.type === "task").map((event) => projectTaskSummary(event, events));
|
|
247
252
|
return {
|
|
248
253
|
...goal,
|
|
249
254
|
events,
|
|
@@ -286,9 +291,12 @@ export function formatPeerGoalScout(board, options = {}) {
|
|
|
286
291
|
for (const suggestion of suggestions.slice(0, limit)) {
|
|
287
292
|
const pathText = suggestion.paths?.length ? ` · paths: ${suggestion.paths.join(", ")}` : "";
|
|
288
293
|
const laneText = suggestion.recommendedLane ? ` · lane: ${suggestion.recommendedLane}${suggestion.preferredRoles?.length ? ` for ${suggestion.preferredRoles.join("/")}` : ""}${suggestion.claimMode ? ` (${suggestion.claimMode})` : ""}` : "";
|
|
289
|
-
|
|
294
|
+
const keyText = suggestion.workKey ? ` · key: ${suggestion.workKey}` : "";
|
|
295
|
+
lines.push(`- ${suggestion.priority} · ${suggestion.goalId} · ${suggestion.kind}: ${suggestion.summary}${laneText}${pathText}${keyText}`);
|
|
296
|
+
const claim = formatScoutClaimCommand(suggestion);
|
|
297
|
+
if (claim) lines.push(` claim: ${claim}`);
|
|
290
298
|
}
|
|
291
|
-
lines.push("", "Next: post one with `/peer goal propose <goal-id> <summary>` or claim
|
|
299
|
+
lines.push("", "Next: post one with `/peer goal propose <goal-id> <summary>` or claim the exact suggested lane with the printed `claim:` command/work key. Scout does not mutate the board.");
|
|
292
300
|
return lines.join("\n");
|
|
293
301
|
}
|
|
294
302
|
|
|
@@ -318,6 +326,20 @@ export function derivePeerGoalScoutSuggestions(board, options = {}) {
|
|
|
318
326
|
push("P1", "stale-claim", `Ask owners to heartbeat or release ${state.staleClaims.length} stale claim${state.staleClaims.length === 1 ? "" : "s"}.`, { paths: uniqueEventPaths(state.staleClaims) });
|
|
319
327
|
}
|
|
320
328
|
if (state.openProposals.length) {
|
|
329
|
+
for (const proposal of state.openProposals.filter((item) => item.lane)) {
|
|
330
|
+
const lane = normalizeLaneName(proposal.lane);
|
|
331
|
+
const summary = `Self-select proposed ${lane} lane: ${proposal.summary}`;
|
|
332
|
+
push("P1", "open-proposal", summary, {
|
|
333
|
+
paths: proposal.paths,
|
|
334
|
+
recommendedLane: lane,
|
|
335
|
+
preferredRoles: preferredRolesForLane(lane),
|
|
336
|
+
claimMode: "read",
|
|
337
|
+
suggestedIntent: suggestedIntentForLane(lane),
|
|
338
|
+
rationale: "A peer proposed a lane; matching idle peers can claim or review it without planner assignment.",
|
|
339
|
+
workKey: proposal.workKey || derivePeerGoalWorkKey({ goalId: goal.id, lane, objective: proposal.summary, mode: "read", paths: proposal.paths }),
|
|
340
|
+
relatedEventId: proposal.id,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
321
343
|
push("P1", "open-proposal", `Triage ${state.openProposals.length} open proposal${state.openProposals.length === 1 ? "" : "s"}; claim one or resolve it if obsolete.`, { paths: uniqueEventPaths(state.openProposals) });
|
|
322
344
|
}
|
|
323
345
|
if (state.readyToClose) {
|
|
@@ -325,7 +347,15 @@ export function derivePeerGoalScoutSuggestions(board, options = {}) {
|
|
|
325
347
|
continue;
|
|
326
348
|
}
|
|
327
349
|
if (!state.activeClaims.length && !state.tasks.length && !state.openProposals.length) {
|
|
328
|
-
|
|
350
|
+
for (const lane of STARTUP_SCOUT_LANES) {
|
|
351
|
+
push("P2", "next-step", lane.summary, {
|
|
352
|
+
recommendedLane: lane.lane,
|
|
353
|
+
preferredRoles: lane.preferredRoles,
|
|
354
|
+
claimMode: "read",
|
|
355
|
+
suggestedIntent: suggestedIntentForLane(lane.lane),
|
|
356
|
+
rationale: "Multiple lane-specific suggestions let idle peers self-select complementary work and suppress duplicates by work key.",
|
|
357
|
+
});
|
|
358
|
+
}
|
|
329
359
|
} else if (!state.currentVotes.length && !state.activeWriteClaims.length) {
|
|
330
360
|
push("P2", "review", "No current peer vote; ask a peer for read-only review or record a pass/fail vote.");
|
|
331
361
|
}
|
|
@@ -535,6 +565,26 @@ function projectEventSummary(event) {
|
|
|
535
565
|
});
|
|
536
566
|
}
|
|
537
567
|
|
|
568
|
+
function projectTaskSummary(task, events) {
|
|
569
|
+
const handoff = latestEvent(events.filter((event) => taskMatchesHandoff(task, event, events)));
|
|
570
|
+
const projected = projectEventSummary(task);
|
|
571
|
+
if (!handoff) return projected;
|
|
572
|
+
return stripEmpty({
|
|
573
|
+
...projected,
|
|
574
|
+
status: handoff.status || "done",
|
|
575
|
+
completedAt: handoff.at,
|
|
576
|
+
handoffEventId: handoff.id,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function taskMatchesHandoff(task, event, events) {
|
|
581
|
+
if (event.type !== "handoff" || event.at < task.at) return false;
|
|
582
|
+
if (task.taskId && event.taskId === task.taskId) return true;
|
|
583
|
+
if (event.taskId === task.id) return true;
|
|
584
|
+
if (task.taskId || event.taskId || !task.workKey || event.workKey !== task.workKey) return false;
|
|
585
|
+
return events.filter((item) => item.type === "task" && item.workKey === task.workKey).length === 1;
|
|
586
|
+
}
|
|
587
|
+
|
|
538
588
|
function projectClaimSummary(claim, events, now) {
|
|
539
589
|
const heartbeat = latestEvent(events.filter((event) => event.type === "heartbeat" && event.resolves === claim.id));
|
|
540
590
|
const lastHeartbeatAt = heartbeat?.at;
|
|
@@ -591,7 +641,7 @@ function normalizeDuplicatePolicy(value) {
|
|
|
591
641
|
|
|
592
642
|
function enrichScoutSuggestion(suggestion = {}) {
|
|
593
643
|
const lane = SCOUT_LANES[suggestion.kind] || {};
|
|
594
|
-
const recommendedLane = suggestion.recommendedLane || lane.recommendedLane;
|
|
644
|
+
const recommendedLane = normalizeLaneName(suggestion.recommendedLane || lane.recommendedLane);
|
|
595
645
|
const claimMode = cleanText(suggestion.claimMode || lane.claimMode);
|
|
596
646
|
const enriched = stripEmpty({
|
|
597
647
|
...suggestion,
|
|
@@ -601,13 +651,48 @@ function enrichScoutSuggestion(suggestion = {}) {
|
|
|
601
651
|
claimMode,
|
|
602
652
|
suggestedIntent: cleanText(suggestion.suggestedIntent || lane.suggestedIntent),
|
|
603
653
|
rationale: cleanText(suggestion.rationale || lane.rationale),
|
|
654
|
+
relatedEventId: cleanText(suggestion.relatedEventId),
|
|
604
655
|
});
|
|
605
656
|
return stripEmpty({
|
|
606
657
|
...enriched,
|
|
607
|
-
workKey: suggestion.workKey || derivePeerGoalWorkKey({ goalId: enriched.goalId, lane: recommendedLane, objective: enriched.summary, mode: claimMode, paths: enriched.paths }),
|
|
658
|
+
workKey: normalizeWorkKey(suggestion.workKey) || derivePeerGoalWorkKey({ goalId: enriched.goalId, lane: recommendedLane, objective: enriched.summary, mode: claimMode, paths: enriched.paths }),
|
|
608
659
|
});
|
|
609
660
|
}
|
|
610
661
|
|
|
662
|
+
function formatScoutClaimCommand(suggestion = {}) {
|
|
663
|
+
if (suggestion.claimMode !== "read" || !suggestion.workKey || !suggestion.goalId || !suggestion.summary) return "";
|
|
664
|
+
const lane = suggestion.recommendedLane ? ` --lane ${shellQuote(suggestion.recommendedLane)}` : "";
|
|
665
|
+
const paths = suggestion.paths?.length ? suggestion.paths.map((path) => ` --path ${shellQuote(path)}`).join("") : "";
|
|
666
|
+
return `/peer goal claim ${shellQuote(suggestion.goalId)} ${shellQuote(suggestion.summary)} --mode read${lane} --key ${shellQuote(suggestion.workKey)}${paths}`;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function shellQuote(value) {
|
|
670
|
+
const text = String(value || "");
|
|
671
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(text)) return text;
|
|
672
|
+
return `'${text.replace(/'/g, `'"'"'`)}'`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function normalizeLaneName(value) {
|
|
676
|
+
const lane = cleanText(value).toLowerCase();
|
|
677
|
+
if (["qa", "quality", "test", "testing"].includes(lane)) return "review";
|
|
678
|
+
if (["implement", "implementation", "developer", "engineer", "worker", "code", "coding"].includes(lane)) return "implementation";
|
|
679
|
+
if (["coordinate", "coordinator", "planning", "planner", "orchestration"].includes(lane)) return "coordination";
|
|
680
|
+
if (["researcher", "scout", "investigation"].includes(lane)) return "research";
|
|
681
|
+
return lane || "review";
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function preferredRolesForLane(lane) {
|
|
685
|
+
const normalized = normalizeLaneName(lane);
|
|
686
|
+
if (normalized === "implementation") return ["worker"];
|
|
687
|
+
if (normalized === "research") return ["researcher", "planner", "coordinator"];
|
|
688
|
+
if (normalized === "coordination") return ["planner", "coordinator", "reviewer"];
|
|
689
|
+
return ["reviewer", "qa", "planner", "coordinator"];
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function suggestedIntentForLane(lane) {
|
|
693
|
+
return normalizeLaneName(lane) === "implementation" ? "task" : "review";
|
|
694
|
+
}
|
|
695
|
+
|
|
611
696
|
function hasActiveClaimForScoutSuggestion(state, suggestion) {
|
|
612
697
|
if (!suggestion?.workKey) return false;
|
|
613
698
|
return state.activeClaims.some((claim) => claim.workKey === suggestion.workKey);
|
package/src/peers/guidance.mjs
CHANGED
|
@@ -11,7 +11,7 @@ const PEER_SEND_GUIDANCE = "Use peer_send to send a prompt-first message to a pe
|
|
|
11
11
|
const PEER_AWAIT_GUIDANCE = "Use peer_await with messageId values from queued or timed-out peer_send calls to read final assistant replies.";
|
|
12
12
|
const PEER_GET_GUIDANCE = "Use peer_get to inspect a peer, message, conversation, runtime summary, active tasks via 'tasks', fan-out suggestions via 'fanout', or redacted audit state by id.";
|
|
13
13
|
const PEER_PROGRESS_GUIDANCE = "Use peer_progress from inside an inbound long-running peer task to send structured checkpoint updates before the final handoff.";
|
|
14
|
-
const PEER_FANOUT_GUIDANCE = "Fan-out gate: for multi-part, long-running, or implementation-plus-review work, call peer_list and
|
|
14
|
+
const PEER_FANOUT_GUIDANCE = "Fan-out gate: for multi-part, long-running, or implementation-plus-review work, call peer_list and use a goal board plus peer_send for research/review/QA lanes unless the user explicitly says to work solo. For emergent self-organization tests, create/reuse a peer goal and let peers inspect scout suggestions or claim lane-specific work keys instead of over-assigning every lane; if you skip fan-out, state the reason in the final response.";
|
|
15
15
|
|
|
16
16
|
export const PEER_INBOUND_FINAL_RESPONSE_GUIDANCE = "For inbound peer asks, answer the inbound ask in your final assistant response; that final assistant response is returned to the requesting peer. For write-capable task intents, end with a concise handoff: status, files changed, verification commands with exit status, and blockers.";
|
|
17
17
|
|
|
@@ -139,13 +139,28 @@ export function buildPeerIdleActivationPrompt(activation, options = {}) {
|
|
|
139
139
|
const peerId = options.localPeerId || "this-peer";
|
|
140
140
|
const paths = activation.paths?.length ? `\nPaths: ${activation.paths.join(", ")}` : "";
|
|
141
141
|
const lane = activation.recommendedLane ? `\nRecommended lane: ${activation.recommendedLane}${activation.claimMode ? ` (${activation.claimMode})` : ""}${activation.preferredRoles?.length ? ` · preferred roles: ${activation.preferredRoles.join(", ")}` : ""}` : "";
|
|
142
|
+
const workKey = activation.workKey ? `\nWork key: ${activation.workKey}` : "";
|
|
143
|
+
const suggestedClaim = buildSuggestedReadClaim(activation);
|
|
142
144
|
const rationale = activation.rationale ? `\nRationale: ${activation.rationale}` : "";
|
|
143
145
|
const fit = activation.personaFit?.matched?.length ? `\nPersona fit: matched ${activation.personaFit.matched.join(", ")}` : "";
|
|
144
|
-
return `[Pi peer idle watcher]\nYou are local peer '${peerId}' and Pi is idle. A proactive goal-board scout suggestion is available.\n\nGoal: ${activation.goalId}\nSuggestion: ${activation.kind} (${activation.priority}) — ${activation.summary}${lane}${rationale}${fit}${paths}\n\nInstructions:\n- First inspect current state with peer_get id '${activation.goalId}'.\n- If useful, take one small safe action that fits the recommended lane: post a proposal/finding/vote,
|
|
146
|
+
return `[Pi peer idle watcher]\nYou are local peer '${peerId}' and Pi is idle. A proactive goal-board scout suggestion is available.\n\nGoal: ${activation.goalId}\nSuggestion: ${activation.kind} (${activation.priority}) — ${activation.summary}${lane}${workKey}${rationale}${fit}${paths}${suggestedClaim}\n\nInstructions:\n- First inspect current state with peer_get id '${activation.goalId}'.\n- If useful, take one small safe action that fits the recommended lane: claim a read-only lane with the work key above, post a proposal/finding/vote, or claim write work only when you intend to edit and can name the paths.\n- If the suggested claim fails as duplicate, inspect the board and stop with a brief handoff instead of starting parallel work.\n- Do not duplicate active claims, work keys, or proposals. If the board is no longer actionable, say so briefly and stop.\n- For write work, respect goal-board claims and end with the required peer handoff sections.\n- Keep the response concise.`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildSuggestedReadClaim(activation = {}) {
|
|
150
|
+
if (activation.claimMode !== "read" || !activation.workKey) return "";
|
|
151
|
+
const lane = activation.recommendedLane ? ` --lane ${shellQuote(activation.recommendedLane)}` : "";
|
|
152
|
+
const paths = activation.paths?.length ? activation.paths.map((path) => ` --path ${shellQuote(path)}`).join("") : "";
|
|
153
|
+
return `\nSuggested first action: /peer goal claim ${shellQuote(activation.goalId)} ${shellQuote(activation.summary)} --mode read${lane} --key ${shellQuote(activation.workKey)}${paths}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function shellQuote(value) {
|
|
157
|
+
const text = String(value || "");
|
|
158
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(text)) return text;
|
|
159
|
+
return `'${text.replace(/'/g, `'"'"'`)}'`;
|
|
145
160
|
}
|
|
146
161
|
|
|
147
162
|
export function peerIdleActivationKey(activation = {}) {
|
|
148
|
-
return [activation.goalId, activation.kind, activation.recommendedLane, activation.summary, ...(activation.paths || [])].join("|");
|
|
163
|
+
return [activation.goalId, activation.kind, activation.recommendedLane, activation.workKey, activation.summary, ...(activation.paths || [])].join("|");
|
|
149
164
|
}
|
|
150
165
|
|
|
151
166
|
function isActivationCoolingDown(state, activation, config, nowMs) {
|
|
@@ -167,6 +182,7 @@ function normalizeActivation(suggestion = {}, localPeerId, options = {}) {
|
|
|
167
182
|
claimMode: cleanString(suggestion.claimMode),
|
|
168
183
|
suggestedIntent: cleanString(suggestion.suggestedIntent),
|
|
169
184
|
rationale: cleanString(suggestion.rationale),
|
|
185
|
+
workKey: cleanString(suggestion.workKey),
|
|
170
186
|
personaFit,
|
|
171
187
|
paths: Array.isArray(suggestion.paths) ? suggestion.paths.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim()) : [],
|
|
172
188
|
peerId: localPeerId,
|
package/src/peers/runtime.mjs
CHANGED
|
@@ -195,7 +195,7 @@ export async function getPeerRuntimeValue(runtime, id) {
|
|
|
195
195
|
const peers = await runtime.comms.listPeers();
|
|
196
196
|
const messages = await runtime.comms.listMessages();
|
|
197
197
|
const suggestion = deriveFanoutSuggestion(peers, messages);
|
|
198
|
-
return { type: "fanout", value: { ...suggestion, checklist: ["Run peer_list before multi-lane work", "Create or reuse /peer goal", "Use /peer goal fanout or peer_send goalId+claimedPaths", "Final response must include Fan-out used: yes/no and peer handles"] } };
|
|
198
|
+
return { type: "fanout", value: { ...suggestion, checklist: ["Run peer_list before multi-lane work", "Create or reuse /peer goal", "For emergent self-organization, ask peers to inspect /peer scout or claim lane-specific work keys before assigning every lane", "Use /peer goal fanout or peer_send goalId+claimedPaths when direct dispatch is needed", "Final response must include Fan-out used: yes/no and peer handles"] } };
|
|
199
199
|
}
|
|
200
200
|
if (id === "audit") return { type: "audit", value: await runtime.comms.getAuditEntries() };
|
|
201
201
|
|