@aeondave/pi-persona 1.15.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.
Files changed (165) hide show
  1. package/AGENTS.md +249 -0
  2. package/CHANGELOG.md +260 -0
  3. package/LICENSE +21 -0
  4. package/README.md +172 -0
  5. package/agents/balthasar.md +32 -0
  6. package/agents/casper.md +33 -0
  7. package/agents/evidence-verifier.md +23 -0
  8. package/agents/melchior.md +32 -0
  9. package/agents/operator.md +63 -0
  10. package/agents/research.md +58 -0
  11. package/agents/reviewer.md +49 -0
  12. package/agents/scout.md +23 -0
  13. package/agents/verifier.md +21 -0
  14. package/assets/banner.png +0 -0
  15. package/assets/demo1.gif +0 -0
  16. package/assets/demo1.mp4 +0 -0
  17. package/assets/workflow.png +0 -0
  18. package/contracts/finding.contract.json +9 -0
  19. package/docs/ARCHITECTURE.md +727 -0
  20. package/docs/EXPERIENCE.md +96 -0
  21. package/docs/MONITORS.md +84 -0
  22. package/docs/README.md +53 -0
  23. package/docs/REFERENCE.md +408 -0
  24. package/docs/SPINE.md +152 -0
  25. package/docs/STRATEGIES.md +215 -0
  26. package/docs/TELEMETRY.md +9 -0
  27. package/flows/deep-review.flow.json +8 -0
  28. package/flows/gated-build.flow.json +9 -0
  29. package/package.json +106 -0
  30. package/personas/audit.md +29 -0
  31. package/personas/dev.md +52 -0
  32. package/personas/elite.md +256 -0
  33. package/personas/judge.md +32 -0
  34. package/personas/magi.md +41 -0
  35. package/personas/planner.md +37 -0
  36. package/personas/researcher.md +37 -0
  37. package/personas/swarm.md +38 -0
  38. package/personas/verify.md +31 -0
  39. package/presets/magi-rounds.preset.json +5 -0
  40. package/prompts/spine.md +11 -0
  41. package/prompts/spine.worker.md +9 -0
  42. package/src/agents/agent.ts +78 -0
  43. package/src/bridge.ts +209 -0
  44. package/src/bus/broker/client.ts +379 -0
  45. package/src/bus/broker/framing.ts +43 -0
  46. package/src/bus/broker/host.ts +407 -0
  47. package/src/bus/broker/messages.ts +87 -0
  48. package/src/bus/broker/paths.ts +31 -0
  49. package/src/bus/contact.ts +145 -0
  50. package/src/bus/inproc.ts +279 -0
  51. package/src/bus/peers.ts +114 -0
  52. package/src/core/brief.ts +356 -0
  53. package/src/core/capabilities.ts +114 -0
  54. package/src/core/config.ts +228 -0
  55. package/src/core/contract.ts +391 -0
  56. package/src/core/data-root.ts +260 -0
  57. package/src/core/discovery.ts +33 -0
  58. package/src/core/display-label.ts +47 -0
  59. package/src/core/fence.ts +151 -0
  60. package/src/core/format.ts +13 -0
  61. package/src/core/frontmatter.ts +202 -0
  62. package/src/core/models.ts +192 -0
  63. package/src/core/monitor.ts +415 -0
  64. package/src/core/naming.ts +42 -0
  65. package/src/core/nudge.ts +238 -0
  66. package/src/core/ownership.ts +102 -0
  67. package/src/core/permissions.ts +56 -0
  68. package/src/core/pi-compat.ts +50 -0
  69. package/src/core/seed.ts +770 -0
  70. package/src/core/session-identity.ts +89 -0
  71. package/src/core/time.ts +202 -0
  72. package/src/core/timer.ts +221 -0
  73. package/src/core/types.ts +28 -0
  74. package/src/engine/adapter.ts +257 -0
  75. package/src/engine/async.ts +898 -0
  76. package/src/engine/change-report.ts +143 -0
  77. package/src/engine/child.ts +532 -0
  78. package/src/engine/errors.ts +30 -0
  79. package/src/engine/fallback.ts +57 -0
  80. package/src/engine/handles.ts +21 -0
  81. package/src/engine/inproc.ts +857 -0
  82. package/src/engine/signals.ts +11 -0
  83. package/src/engine/spec-preflight.ts +33 -0
  84. package/src/engine/stream.ts +369 -0
  85. package/src/engine/worktree.ts +144 -0
  86. package/src/exocom/activation.ts +31 -0
  87. package/src/exocom/codes.ts +261 -0
  88. package/src/exocom/envelope.ts +180 -0
  89. package/src/exocom/gate.ts +6 -0
  90. package/src/exocom/guards.ts +45 -0
  91. package/src/exocom/inbound.ts +141 -0
  92. package/src/exocom/install.ts +970 -0
  93. package/src/exocom/ledger.ts +603 -0
  94. package/src/exocom/limits.ts +20 -0
  95. package/src/exocom/paths.ts +35 -0
  96. package/src/exocom/plane.ts +1116 -0
  97. package/src/exocom/registry.ts +307 -0
  98. package/src/exocom/scope.ts +42 -0
  99. package/src/exocom/status.ts +86 -0
  100. package/src/exocom/untrusted.ts +40 -0
  101. package/src/exocom/wait.ts +23 -0
  102. package/src/exocom/write-guard.ts +89 -0
  103. package/src/extension/broker-host.ts +181 -0
  104. package/src/extension/clock.ts +14 -0
  105. package/src/extension/engine.ts +321 -0
  106. package/src/extension/hooks.ts +796 -0
  107. package/src/extension/identity.ts +244 -0
  108. package/src/extension/shared.ts +175 -0
  109. package/src/extension.ts +2221 -0
  110. package/src/loader.ts +218 -0
  111. package/src/monitor/process.ts +116 -0
  112. package/src/monitor/session.ts +61 -0
  113. package/src/orchestration/flow-journal.ts +86 -0
  114. package/src/orchestration/flow-run.ts +301 -0
  115. package/src/orchestration/flow.ts +125 -0
  116. package/src/orchestration/judge.ts +78 -0
  117. package/src/orchestration/model-retry.ts +85 -0
  118. package/src/orchestration/parallel.ts +107 -0
  119. package/src/orchestration/params.ts +23 -0
  120. package/src/orchestration/reducers.ts +123 -0
  121. package/src/orchestration/render.ts +127 -0
  122. package/src/orchestration/roster.ts +133 -0
  123. package/src/orchestration/sdk.ts +326 -0
  124. package/src/orchestration/strategies/compete.ts +182 -0
  125. package/src/orchestration/strategies/council-rounds.ts +124 -0
  126. package/src/orchestration/strategies/critic-loop.ts +168 -0
  127. package/src/orchestration/strategies/debate.ts +126 -0
  128. package/src/orchestration/strategies/fanout.ts +20 -0
  129. package/src/orchestration/strategies/judge.ts +117 -0
  130. package/src/orchestration/strategies/magi.ts +239 -0
  131. package/src/orchestration/strategies/map.ts +327 -0
  132. package/src/orchestration/strategies/pair.ts +72 -0
  133. package/src/orchestration/strategies/pipeline.ts +91 -0
  134. package/src/orchestration/strategies/synthesize.ts +99 -0
  135. package/src/orchestration/strategy.ts +49 -0
  136. package/src/orchestration/types.ts +31 -0
  137. package/src/orchestration/voting.ts +126 -0
  138. package/src/persona/config-store.ts +215 -0
  139. package/src/persona/controller.ts +173 -0
  140. package/src/persona/gating.ts +82 -0
  141. package/src/persona/orchestrate.ts +83 -0
  142. package/src/persona/persona.ts +294 -0
  143. package/src/persona/spine.ts +221 -0
  144. package/src/persona/state.ts +28 -0
  145. package/src/telemetry/contract.ts +244 -0
  146. package/src/telemetry/paths.ts +14 -0
  147. package/src/telemetry/producer.ts +390 -0
  148. package/src/tools/council.ts +200 -0
  149. package/src/tools/delegate-tool.ts +637 -0
  150. package/src/tools/delegate.ts +661 -0
  151. package/src/tools/exocom-work.ts +328 -0
  152. package/src/tools/exocom.ts +476 -0
  153. package/src/tools/flow.ts +117 -0
  154. package/src/tools/intercom-tool.ts +292 -0
  155. package/src/tools/intercom.ts +181 -0
  156. package/src/tools/models.ts +39 -0
  157. package/src/tools/monitor.ts +66 -0
  158. package/src/tools/timer.ts +105 -0
  159. package/src/ui/agent-overlay.ts +526 -0
  160. package/src/ui/agent-tree.ts +307 -0
  161. package/src/ui/live-clock.ts +42 -0
  162. package/src/ui/model-picker.ts +160 -0
  163. package/src/ui/presentation.ts +170 -0
  164. package/src/ui/usage.ts +133 -0
  165. package/teams.yaml +27 -0
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Collaboration tools over the work ledger: claim / ask / answer / decline / wait / progress / release.
3
+ * Wait is non-blocking (MCP cap) — it arms a wake like `timer`.
4
+ */
5
+ import { randomUUID } from "node:crypto";
6
+ import { type Static, Type } from "typebox";
7
+
8
+ import { type ExtensionAPI, keyHint } from "@earendil-works/pi-coding-agent";
9
+ import { Text } from "@earendil-works/pi-tui";
10
+
11
+ import { attributePeer } from "../core/fence.ts";
12
+ import type { LedgerAnswer, LedgerState } from "../exocom/ledger.ts";
13
+ import { formatLedgerStatus } from "../exocom/status.ts";
14
+ import type { ExocomPlane } from "../exocom/plane.ts";
15
+ import type { ExocomSemanticFrame } from "../exocom/envelope.ts";
16
+ import { EXOCOM_WAIT_MAX_MS, waitTimeoutMs } from "../exocom/wait.ts";
17
+ import { normalizeMetadataText } from "../exocom/registry.ts";
18
+
19
+ const TOKEN = { minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9._:-]+$" } as const;
20
+
21
+ const ClaimParams = Type.Object({
22
+ work_key: Type.String({ ...TOKEN, description: "Stable work key for this slice of collaboration." }),
23
+ write_set: Type.Array(Type.String({ minLength: 1, maxLength: 256 }), { minItems: 1, maxItems: 64, description: "Repository-relative paths this session claims." }),
24
+ slice: Type.String({ minLength: 1, maxLength: 256, description: "Short description of the claimed slice." }),
25
+ });
26
+
27
+ const AskParams = Type.Object({
28
+ work_key: Type.String({ ...TOKEN, description: "Stable work key for this question, 1–128 token characters matching [A-Za-z0-9._:-], e.g. `api-review`; a local claim is not required." }),
29
+ target: Type.String({ minLength: 1, maxLength: 80, description: "One peer's target exactly as exocom_list shows it (never *)." }),
30
+ question: Type.String({ minLength: 1, maxLength: 4_096, description: "The bounded question." }),
31
+ ask_id: Type.Optional(Type.String({ ...TOKEN, description: "Correlation id; generated if omitted." })),
32
+ });
33
+
34
+ const AnswerParams = Type.Object({
35
+ ask_id: Type.String({ ...TOKEN, description: "The ask_id being answered." }),
36
+ work_key: Type.String({ ...TOKEN, description: "Work key of the ask." }),
37
+ ok: Type.Boolean({ description: "Whether the ask is satisfied." }),
38
+ evidence: Type.String({ minLength: 1, maxLength: 8_192, description: "Evidence or decline reason." }),
39
+ });
40
+
41
+ const DeclineParams = Type.Object({
42
+ ask_id: Type.String({ ...TOKEN, description: "The ask_id being declined." }),
43
+ work_key: Type.String({ ...TOKEN, description: "Work key of the ask." }),
44
+ reason: Type.Optional(Type.String({ minLength: 1, maxLength: 8_192, description: "Why this session declines." })),
45
+ });
46
+
47
+ const WaitParams = Type.Object({
48
+ work_key: Type.String({ ...TOKEN, description: "Work key paired with the outbound ask." }),
49
+ ask_id: Type.String({ ...TOKEN, description: "The ask_id returned by your outbound exocom_ask. A retained answer is returned immediately." }),
50
+ timeoutMs: Type.Optional(Type.Number({ description: `Requested wake budget in milliseconds (default ${EXOCOM_WAIT_MAX_MS}); runtime floors fractions and clamps to 1–${EXOCOM_WAIT_MAX_MS}. Does not block this call.` })),
51
+ });
52
+
53
+ const ReleaseParams = Type.Object({
54
+ work_key: Type.String({ ...TOKEN, description: "Work key whose claims and outbound asks this session releases." }),
55
+ });
56
+
57
+ const ProgressParams = Type.Object({
58
+ work_key: Type.String({ ...TOKEN, description: "Work key to journal progress on." }),
59
+ note: Type.String({ minLength: 1, maxLength: 4_096, description: "Progress note." }),
60
+ });
61
+
62
+ export interface ExocomWorkDeps {
63
+ getPlane: () => ExocomPlane | undefined;
64
+ sessionId: () => string;
65
+ name: () => string;
66
+ now: () => number;
67
+ /** False for a Pi that joined from another workspace: ledger paths belong to the scope workspace. */
68
+ canClaim?: () => boolean;
69
+ /** Canonicalize the public exocom_list target to the authenticated raw session id. */
70
+ resolveTarget: (target: string) => string;
71
+ /** Local ledger apply, then wire send routed by kind. */
72
+ dispatch: (frame: ExocomSemanticFrame) => Promise<{ msg_id: string; peerWakeDeferred?: true }>;
73
+ /** Read-before-arm closes the answer-before-wait race. */
74
+ armWait: (work_key: string, ask_id: string, timeoutMs: number) => ExocomWaitArmResult;
75
+ /** Current ledger snapshot for the read-only ownership view. */
76
+ ledger: () => LedgerState;
77
+ /** Peer display name for a session id, or undefined when it cannot be resolved. */
78
+ labelFor: (sessionId: string) => string | undefined;
79
+ }
80
+
81
+ export type ExocomWaitArmResult =
82
+ | { status: "waiting"; id: string }
83
+ | { status: "answered"; answer: LedgerAnswer };
84
+
85
+ function tokenOrUuid(value: string | undefined): string {
86
+ return value && /^[A-Za-z0-9._:-]+$/.test(value) ? value : randomUUID();
87
+ }
88
+
89
+ function result(text: string, details: Record<string, unknown>) {
90
+ return { content: [{ type: "text" as const, text }], details };
91
+ }
92
+
93
+ function workToolFailed(renderedResult: unknown): boolean {
94
+ return !!renderedResult && typeof renderedResult === "object"
95
+ && (renderedResult as { isError?: unknown }).isError === true;
96
+ }
97
+
98
+ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps): void {
99
+ const identity = () => {
100
+ const plane = deps.getPlane();
101
+ if (!plane) throw new Error("exocom is not active for this persona");
102
+ return { plane, from_session: deps.sessionId(), from_name: deps.name(), ts: new Date(deps.now()).toISOString() };
103
+ };
104
+
105
+ pi.registerTool({
106
+ name: "exocom_claim",
107
+ label: "Exocom Claim",
108
+ description: "Claim a repository-relative write-set slice on the workspace work ledger. Overlap with an open claim is refused; a Pi joined from an external workspace cannot claim its home paths.",
109
+ parameters: ClaimParams,
110
+ async execute(_id, params: Static<typeof ClaimParams>) {
111
+ if (deps.canClaim?.() === false) {
112
+ throw new Error("exocom_claim: this Pi is in an external workspace and cannot claim repository-relative paths in the joined workspace ledger");
113
+ }
114
+ const { from_session, from_name, ts } = identity();
115
+ const frame: ExocomSemanticFrame = {
116
+ kind: "claim", work_key: params.work_key, from_session, from_name,
117
+ write_set: params.write_set, slice: params.slice, msg_id: randomUUID(), ts,
118
+ };
119
+ const { msg_id } = await deps.dispatch(frame);
120
+ return result(`exocom: claimed ${params.work_key} · msg_id=${msg_id}`, { work_key: params.work_key, msg_id });
121
+ },
122
+ renderCall(args, theme) {
123
+ return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Claim "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
124
+ },
125
+ renderResult(res, { expanded }, theme) {
126
+ const first = res.content.find((item) => item.type === "text");
127
+ const text = first?.type === "text" ? first.text : "Exocom claim failed";
128
+ if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
129
+ return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
130
+ },
131
+ });
132
+
133
+ pi.registerTool({
134
+ name: "exocom_ask",
135
+ label: "Exocom Ask",
136
+ description: "Ask one peer session a bounded question on a work key. Use one exact `target` from `exocom_list`; never broadcast (`*`). One pending ask per (work_key, target session).",
137
+ parameters: AskParams,
138
+ async execute(_id, params: Static<typeof AskParams>) {
139
+ const { from_session, from_name, ts } = identity();
140
+ if (params.target === "*") {
141
+ throw new Error("exocom_ask: target must name one other session, never *");
142
+ }
143
+ const safeTarget = normalizeMetadataText(params.target, 80, "peer");
144
+ if (!safeTarget || safeTarget !== params.target.trim()) {
145
+ throw new Error(`exocom_ask: target "${safeTarget}" contains invalid characters or whitespace; use the exact target from exocom_list.`);
146
+ }
147
+ const to_session = deps.resolveTarget(safeTarget);
148
+ if (to_session === from_session) throw new Error("exocom_ask: target cannot be this session");
149
+ const frame: ExocomSemanticFrame = {
150
+ kind: "ask", ask_id: tokenOrUuid(params.ask_id), work_key: params.work_key,
151
+ from_session, from_name, to_session, question: params.question,
152
+ msg_id: randomUUID(), ts,
153
+ };
154
+ const dispatched = await deps.dispatch(frame);
155
+ const deferred = dispatched.peerWakeDeferred === true;
156
+ return result(
157
+ `exocom: asked ${safeTarget} · ask_id=${frame.ask_id} · msg_id=${dispatched.msg_id}${deferred ? " · ledger committed; peer wake deferred" : ""}`,
158
+ { ask_id: frame.ask_id, msg_id: dispatched.msg_id, work_key: params.work_key, target: safeTarget, ...(deferred ? { peerWakeDeferred: true } : {}) },
159
+ );
160
+ },
161
+ renderCall(args, theme) {
162
+ return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Ask "))}${theme.fg("accent", normalizeMetadataText(args.target, 80, "peer"))}`, 0, 0);
163
+ },
164
+ renderResult(res, { expanded }, theme) {
165
+ const first = res.content.find((item) => item.type === "text");
166
+ const text = first?.type === "text" ? first.text : "Exocom ask failed";
167
+ if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
168
+ return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
169
+ },
170
+ });
171
+
172
+ pi.registerTool({
173
+ name: "exocom_answer",
174
+ label: "Exocom Answer",
175
+ description: "Answer a pending ask addressed to this session. Drops the ask and wakes the asker's wait.",
176
+ parameters: AnswerParams,
177
+ async execute(_id, params: Static<typeof AnswerParams>) {
178
+ const { from_session, from_name, ts } = identity();
179
+ const frame: ExocomSemanticFrame = {
180
+ kind: "answer", ask_id: params.ask_id, work_key: params.work_key,
181
+ from_session, from_name, ok: params.ok, evidence: params.evidence,
182
+ msg_id: randomUUID(), ts,
183
+ };
184
+ const dispatched = await deps.dispatch(frame);
185
+ const deferred = dispatched.peerWakeDeferred === true;
186
+ return result(
187
+ `exocom: answered ${params.ask_id} · ok=${params.ok} · msg_id=${dispatched.msg_id}${deferred ? " · ledger committed; peer wake deferred" : ""}`,
188
+ { ask_id: params.ask_id, msg_id: dispatched.msg_id, ok: params.ok, ...(deferred ? { peerWakeDeferred: true } : {}) },
189
+ );
190
+ },
191
+ renderCall(args, theme) {
192
+ return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Answer "))}${theme.fg("accent", normalizeMetadataText(args.ask_id, 80, "ask"))}`, 0, 0);
193
+ },
194
+ renderResult(res, { expanded }, theme) {
195
+ const first = res.content.find((item) => item.type === "text");
196
+ const text = first?.type === "text" ? first.text : "Exocom answer failed";
197
+ if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
198
+ return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
199
+ },
200
+ });
201
+
202
+ pi.registerTool({
203
+ name: "exocom_decline",
204
+ label: "Exocom Decline",
205
+ description: "Decline a pending ask addressed to this session (answer with ok=false).",
206
+ parameters: DeclineParams,
207
+ async execute(_id, params: Static<typeof DeclineParams>) {
208
+ const { from_session, from_name, ts } = identity();
209
+ const frame: ExocomSemanticFrame = {
210
+ kind: "answer", ask_id: params.ask_id, work_key: params.work_key,
211
+ from_session, from_name, ok: false, evidence: params.reason ?? "declined",
212
+ msg_id: randomUUID(), ts,
213
+ };
214
+ const dispatched = await deps.dispatch(frame);
215
+ const deferred = dispatched.peerWakeDeferred === true;
216
+ return result(
217
+ `exocom: declined ${params.ask_id} · msg_id=${dispatched.msg_id}${deferred ? " · ledger committed; peer wake deferred" : ""}`,
218
+ { ask_id: params.ask_id, msg_id: dispatched.msg_id, ok: false, ...(deferred ? { peerWakeDeferred: true } : {}) },
219
+ );
220
+ },
221
+ renderCall(args, theme) {
222
+ return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Decline "))}${theme.fg("accent", normalizeMetadataText(args.ask_id, 80, "ask"))}`, 0, 0);
223
+ },
224
+ renderResult(res, { expanded }, theme) {
225
+ const first = res.content.find((item) => item.type === "text");
226
+ const text = first?.type === "text" ? first.text : "Exocom decline failed";
227
+ if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
228
+ return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
229
+ },
230
+ });
231
+
232
+ pi.registerTool({
233
+ name: "exocom_wait",
234
+ label: "Exocom Wait",
235
+ description: `Join one ask WITHOUT blocking this tool call. Returns a retained answer immediately, otherwise a follow-up wakes you on answer or timeout (cap ${EXOCOM_WAIT_MAX_MS}ms).`,
236
+ parameters: WaitParams,
237
+ async execute(_id, params: Static<typeof WaitParams>) {
238
+ identity();
239
+ const timeoutMs = waitTimeoutMs(params.timeoutMs);
240
+ const armed = deps.armWait(params.work_key, params.ask_id, timeoutMs);
241
+ if (armed.status === "answered") {
242
+ const answer = armed.answer;
243
+ return result(
244
+ `exocom: ${params.ask_id} already answered · ok=${answer.ok}\n${attributePeer(answer.from_session, answer.evidence)}`,
245
+ { work_key: params.work_key, ask_id: params.ask_id, status: "answered", ok: answer.ok, msg_id: answer.msg_id },
246
+ );
247
+ }
248
+ return result(`exocom: waiting on ${params.work_key} ask_id=${params.ask_id} · timeout ${timeoutMs}ms · ${armed.id}. End this turn; you will be woken.`, { work_key: params.work_key, ask_id: params.ask_id, timeoutMs, id: armed.id, status: "waiting" });
249
+ },
250
+ renderCall(args, theme) {
251
+ return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Wait "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
252
+ },
253
+ renderResult(res, { expanded }, theme) {
254
+ const first = res.content.find((item) => item.type === "text");
255
+ const text = first?.type === "text" ? first.text : "Exocom wait failed";
256
+ if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
257
+ return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
258
+ },
259
+ });
260
+
261
+ pi.registerTool({
262
+ name: "exocom_release",
263
+ label: "Exocom Release",
264
+ description: "Release this session's open claims on a work key and drop its outbound pending asks.",
265
+ parameters: ReleaseParams,
266
+ async execute(_id, params: Static<typeof ReleaseParams>) {
267
+ const { from_session, from_name, ts } = identity();
268
+ const frame: ExocomSemanticFrame = {
269
+ kind: "release", work_key: params.work_key, from_session, from_name, msg_id: randomUUID(), ts,
270
+ };
271
+ const { msg_id } = await deps.dispatch(frame);
272
+ return result(`exocom: released ${params.work_key} · msg_id=${msg_id}`, { work_key: params.work_key, msg_id });
273
+ },
274
+ renderCall(args, theme) {
275
+ return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Release "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
276
+ },
277
+ renderResult(res, { expanded }, theme) {
278
+ const first = res.content.find((item) => item.type === "text");
279
+ const text = first?.type === "text" ? first.text : "Exocom release failed";
280
+ if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
281
+ return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
282
+ },
283
+ });
284
+
285
+ pi.registerTool({
286
+ name: "exocom_progress",
287
+ label: "Exocom Progress",
288
+ description: "Journal a progress note on a work key. Does not change claims or asks.",
289
+ parameters: ProgressParams,
290
+ async execute(_id, params: Static<typeof ProgressParams>) {
291
+ const { from_session, from_name, ts } = identity();
292
+ const frame: ExocomSemanticFrame = {
293
+ kind: "progress", work_key: params.work_key, from_session, from_name, note: params.note, msg_id: randomUUID(), ts,
294
+ };
295
+ const { msg_id } = await deps.dispatch(frame);
296
+ return result(`exocom: progress ${params.work_key} · msg_id=${msg_id}`, { work_key: params.work_key, msg_id });
297
+ },
298
+ renderCall(args, theme) {
299
+ return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Progress "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
300
+ },
301
+ renderResult(res, { expanded }, theme) {
302
+ const first = res.content.find((item) => item.type === "text");
303
+ const text = first?.type === "text" ? first.text : "Exocom progress failed";
304
+ if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
305
+ return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
306
+ },
307
+ });
308
+
309
+ pi.registerTool({
310
+ name: "exocom_status",
311
+ label: "Exocom Status",
312
+ description: "Who owns what in this Exocom scope: your claims, peers' claims, asks waiting for you, your open asks.",
313
+ parameters: Type.Object({}),
314
+ async execute() {
315
+ const state = deps.ledger();
316
+ return result(formatLedgerStatus(state, deps.sessionId(), deps.labelFor, deps.now()), { claims: state.claims.length, asks: state.asks.length });
317
+ },
318
+ renderCall(_args, theme) {
319
+ return new Text(theme.fg("toolTitle", theme.bold("Exocom Status")), 0, 0);
320
+ },
321
+ renderResult(res, { expanded }, theme) {
322
+ const first = res.content.find((item) => item.type === "text");
323
+ const text = first?.type === "text" ? first.text : "Exocom status failed";
324
+ if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
325
+ return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
326
+ },
327
+ });
328
+ }