@gavin-hu/my-pi-agent 0.18.0 → 0.20.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.
Files changed (324) hide show
  1. package/README.md +66 -52
  2. package/README.zh-CN.md +56 -46
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +507 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
  6. package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
  7. package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
  8. package/docs/extensions/file-browse.md +561 -0
  9. package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
  10. package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
  11. package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
  12. package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
  14. package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +298 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
  19. package/docs/extensions/team.md +254 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
  25. package/extensions/board/commands.ts +105 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +170 -0
  28. package/extensions/board/index.ts +40 -0
  29. package/extensions/board/input.ts +54 -0
  30. package/extensions/board/join.ts +42 -0
  31. package/extensions/board/page.ts +388 -0
  32. package/extensions/board/result.ts +108 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +90 -0
  35. package/extensions/board/seat.ts +28 -0
  36. package/extensions/board/snapshot.ts +42 -0
  37. package/extensions/board/tools.ts +444 -0
  38. package/extensions/board/transport/router.ts +91 -0
  39. package/extensions/board/transport/server.ts +138 -0
  40. package/extensions/board/types.ts +121 -0
  41. package/extensions/board/view/client.ts +51 -0
  42. package/extensions/board/view/dialog.ts +43 -0
  43. package/extensions/board/view/keys.ts +125 -0
  44. package/extensions/board/view/live.ts +231 -0
  45. package/extensions/board/view/names.ts +91 -0
  46. package/extensions/board/view/page.ts +118 -0
  47. package/extensions/board/view/render.ts +519 -0
  48. package/extensions/board/view/styles.ts +228 -0
  49. package/extensions/board/view/team.ts +327 -0
  50. package/extensions/bytefly/footer/budget.ts +6 -5
  51. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  52. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  53. package/extensions/bytefly/footer/config.ts +19 -8
  54. package/extensions/bytefly/footer/price/price.ts +30 -8
  55. package/extensions/bytefly/footer/register.ts +5 -5
  56. package/extensions/bytefly/footer/render/format.ts +18 -0
  57. package/extensions/bytefly/footer/render/lines.ts +23 -7
  58. package/extensions/bytefly/footer/render/segments.ts +117 -31
  59. package/extensions/bytefly/footer/types.ts +29 -5
  60. package/extensions/bytefly/footer/unpriced.ts +42 -17
  61. package/extensions/bytefly/panel/commands.ts +67 -7
  62. package/extensions/bytefly/panel/filter.ts +68 -0
  63. package/extensions/bytefly/panel/format.ts +16 -15
  64. package/extensions/bytefly/panel/join.ts +56 -13
  65. package/extensions/bytefly/panel/prices.ts +43 -0
  66. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  67. package/extensions/bytefly/panel/settings.ts +75 -43
  68. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  69. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  70. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  71. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  72. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  73. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  74. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  75. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  76. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  77. package/extensions/bytefly/panel/tui.ts +306 -259
  78. package/extensions/bytefly/picker/discovery.ts +0 -2
  79. package/extensions/bytefly/picker/types.ts +0 -2
  80. package/extensions/bytefly/update/check.ts +39 -0
  81. package/extensions/bytefly/update/format.ts +15 -0
  82. package/extensions/bytefly/update/register.ts +73 -34
  83. package/extensions/bytefly/update/state.ts +6 -3
  84. package/extensions/check/commands.ts +2 -1
  85. package/extensions/context/tui.ts +1 -1
  86. package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
  87. package/extensions/file-browse/files/classify.ts +136 -0
  88. package/extensions/file-browse/files/files.ts +12 -0
  89. package/extensions/file-browse/files/index.ts +90 -0
  90. package/extensions/file-browse/files/list.ts +261 -0
  91. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  92. package/extensions/file-browse/files/tree.ts +101 -0
  93. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  94. package/extensions/file-browse/index.ts +4 -4
  95. package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
  96. package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
  97. package/extensions/file-browse/transport/server.ts +110 -0
  98. package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
  99. package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
  100. package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
  101. package/extensions/file-browse/view/emit.ts +51 -0
  102. package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
  103. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  104. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
  105. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
  106. package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
  107. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  108. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  109. package/extensions/file-read/schema.ts +2 -1
  110. package/extensions/job/runtime.ts +4 -4
  111. package/extensions/memory/store.ts +12 -5
  112. package/extensions/plan/blocks.ts +23 -2
  113. package/extensions/plan/commands.ts +21 -16
  114. package/extensions/plan/feedback.ts +185 -52
  115. package/extensions/plan/format.ts +10 -3
  116. package/extensions/plan/index.ts +23 -5
  117. package/extensions/plan/plan-files.ts +88 -0
  118. package/extensions/plan/plan-index.ts +43 -9
  119. package/extensions/plan/plan-text.ts +33 -23
  120. package/extensions/plan/plans.ts +27 -34
  121. package/extensions/plan/policy.ts +30 -17
  122. package/extensions/plan/review.ts +3 -27
  123. package/extensions/plan/runtime.ts +52 -11
  124. package/extensions/plan/sections.ts +112 -0
  125. package/extensions/plan/steps.ts +3 -22
  126. package/extensions/plan/todos.ts +51 -3
  127. package/extensions/plan/tools/exit.ts +20 -95
  128. package/extensions/plan/tools/todo-seed.ts +4 -2
  129. package/extensions/plan/tools/write.ts +12 -1
  130. package/extensions/plan/tui.ts +5 -34
  131. package/extensions/plan/types.ts +15 -8
  132. package/extensions/plan/web-blocks.ts +89 -0
  133. package/extensions/plan/web-chrome.ts +3 -5
  134. package/extensions/plan/web-client.ts +179 -330
  135. package/extensions/plan/web-collab.ts +3 -3
  136. package/extensions/plan/web-comments.ts +18 -8
  137. package/extensions/plan/web-composer.ts +80 -0
  138. package/extensions/plan/web-draft.ts +40 -0
  139. package/extensions/plan/web-keys.ts +134 -0
  140. package/extensions/plan/web-outline.ts +192 -0
  141. package/extensions/plan/web-page.ts +4 -8
  142. package/extensions/plan/web-plan.ts +150 -0
  143. package/extensions/plan/web-router.ts +3 -58
  144. package/extensions/plan/web-settle.ts +194 -0
  145. package/extensions/plan/web-share-session.ts +24 -30
  146. package/extensions/plan/web-streams.ts +14 -14
  147. package/extensions/plan/web-tokens.ts +2 -2
  148. package/extensions/plan/web.ts +66 -120
  149. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  150. package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
  151. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  152. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  153. package/extensions/rewind/index.ts +3 -3
  154. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  155. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  156. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  157. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  158. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  159. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  160. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  161. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  162. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  163. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  164. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  165. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  166. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  167. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  168. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  169. package/extensions/session-bus/commands.ts +17 -69
  170. package/extensions/session-bus/format.ts +22 -45
  171. package/extensions/session-bus/index.ts +13 -7
  172. package/extensions/session-bus/join-request.ts +40 -0
  173. package/extensions/session-bus/mail-watch.ts +134 -0
  174. package/extensions/session-bus/notice.ts +32 -4
  175. package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
  176. package/extensions/session-bus/protocol.ts +19 -49
  177. package/extensions/session-bus/resolve.ts +67 -0
  178. package/extensions/session-bus/result.ts +16 -51
  179. package/extensions/session-bus/rows.ts +11 -5
  180. package/extensions/session-bus/runtime.ts +220 -267
  181. package/extensions/session-bus/store.ts +55 -7
  182. package/extensions/session-bus/tools.ts +31 -66
  183. package/extensions/session-bus/tui.ts +5 -5
  184. package/extensions/session-bus/types.ts +23 -14
  185. package/extensions/subagent/agents.ts +9 -8
  186. package/extensions/subagent/commands.ts +2 -1
  187. package/extensions/subagent/invocation.ts +4 -2
  188. package/extensions/team/commands.ts +145 -0
  189. package/extensions/team/dispatch.ts +133 -0
  190. package/extensions/team/format.ts +119 -0
  191. package/extensions/team/index.ts +34 -0
  192. package/extensions/team/schema.ts +29 -0
  193. package/extensions/team/tools.ts +267 -0
  194. package/extensions/team/types.ts +65 -0
  195. package/extensions/todo/index.ts +4 -1
  196. package/extensions/todo/nudge.ts +8 -2
  197. package/extensions/web-access/fetch/page.ts +4 -1
  198. package/extensions/web-access/fetch/structured.ts +447 -0
  199. package/extensions/web-access/fetch/tool.ts +2 -1
  200. package/extensions/web-access/search/tool.ts +2 -1
  201. package/extensions/wechat/bridge.ts +106 -251
  202. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  203. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  204. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  205. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  206. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  207. package/extensions/wechat/dialog/channels.ts +136 -0
  208. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
  209. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
  210. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  211. package/extensions/wechat/index.ts +10 -10
  212. package/extensions/wechat/intake.ts +245 -0
  213. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  214. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  215. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
  216. package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
  217. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
  218. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  219. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  220. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  221. package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
  222. package/extensions/wechat/tools.ts +29 -12
  223. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  224. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
  225. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  226. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  227. package/extensions/wechat/transport/outbound.ts +142 -0
  228. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  229. package/extensions/wechat/types.ts +10 -1
  230. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  231. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  232. package/extensions/worktree/commands.ts +24 -17
  233. package/extensions/worktree/exit.ts +12 -6
  234. package/extensions/worktree/git.ts +22 -14
  235. package/extensions/worktree/guard.ts +18 -290
  236. package/extensions/worktree/handoff.ts +10 -2
  237. package/extensions/worktree/index.ts +2 -1
  238. package/extensions/worktree/lifecycle.ts +2 -0
  239. package/extensions/worktree/registry.ts +20 -11
  240. package/extensions/worktree/root-tools.ts +13 -3
  241. package/extensions/worktree/runtime.ts +8 -2
  242. package/extensions/worktree/screen.ts +15 -8
  243. package/extensions/worktree/shell.ts +303 -0
  244. package/extensions/worktree/state.ts +1 -1
  245. package/extensions/worktree/status.ts +42 -9
  246. package/extensions/worktree/tools.ts +19 -13
  247. package/lib/README.md +11 -8
  248. package/lib/board/card.ts +479 -0
  249. package/lib/board/links.ts +141 -0
  250. package/lib/board/page-pointer.ts +310 -0
  251. package/lib/board/round-text.ts +81 -0
  252. package/lib/board/round.ts +94 -0
  253. package/lib/board/rules.ts +437 -0
  254. package/lib/board/session.ts +105 -0
  255. package/lib/board/store.ts +497 -0
  256. package/lib/board/view.ts +234 -0
  257. package/lib/errors.ts +12 -4
  258. package/lib/format.ts +35 -0
  259. package/lib/http/guard.ts +112 -0
  260. package/lib/http/index.ts +16 -5
  261. package/lib/http/serve.ts +237 -0
  262. package/lib/http/server.ts +12 -10
  263. package/lib/http/sink.ts +135 -0
  264. package/lib/http/token.ts +38 -0
  265. package/lib/plan-entry.ts +11 -0
  266. package/lib/policy.ts +25 -2
  267. package/lib/requirements.ts +6 -7
  268. package/lib/session-label.ts +64 -0
  269. package/lib/session-roster.ts +201 -0
  270. package/lib/session-tool.ts +184 -0
  271. package/lib/spawn.ts +37 -3
  272. package/lib/stray-fields.ts +45 -0
  273. package/lib/tool-names.ts +38 -2
  274. package/lib/tui/glyphs.ts +3 -2
  275. package/lib/web/README.md +57 -4
  276. package/lib/web/client-runtime.ts +123 -0
  277. package/lib/web/components.ts +5 -0
  278. package/lib/web/index.ts +4 -2
  279. package/lib/web/markup.ts +88 -1
  280. package/lib/web/names.ts +42 -0
  281. package/lib/web/runtime.ts +7 -1
  282. package/lib/web/tokens.ts +25 -2
  283. package/package.json +26 -14
  284. package/skills/next/SKILL.md +4 -3
  285. package/skills/project-docs/SKILL.md +133 -0
  286. package/skills/project-docs/references/audit.md +131 -0
  287. package/skills/project-docs/references/changelog.md +105 -0
  288. package/skills/project-docs/references/discovery.md +127 -0
  289. package/skills/project-docs/references/readme.md +80 -0
  290. package/skills/project-docs/references/release.md +84 -0
  291. package/skills/project-docs/references/roadmap.md +93 -0
  292. package/skills/project-docs/scripts/state.ts +316 -0
  293. package/skills/refactor/SKILL.md +100 -125
  294. package/skills/refactor/references/handoff.md +35 -0
  295. package/skills/refactor/references/signals.md +54 -0
  296. package/skills/refactor/scripts/inventory.ts +587 -0
  297. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  298. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  299. package/extensions/file-browse/README.md +0 -506
  300. package/extensions/file-browse/files.ts +0 -564
  301. package/extensions/file-browse/server.ts +0 -141
  302. package/extensions/job/process.ts +0 -35
  303. package/extensions/plan/split.ts +0 -87
  304. package/extensions/session-bus/README.md +0 -247
  305. package/extensions/session-bus/team.ts +0 -102
  306. package/extensions/wechat/outbound.ts +0 -100
  307. package/lib/plans/sections.ts +0 -172
  308. package/lib/plans/setting.ts +0 -52
  309. package/lib/plans/split.ts +0 -215
  310. package/lib/plans/store.ts +0 -19
  311. package/lib/plans/team.ts +0 -151
  312. package/skills/changelog/SKILL.md +0 -148
  313. package/skills/parallel-dev/SKILL.md +0 -150
  314. /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
  315. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  316. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  317. /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
  318. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  319. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  320. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  321. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  322. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  323. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  324. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -0,0 +1,267 @@
1
+ /**
2
+ * The `team` tool: the lead's round over the same cards `board` owns.
3
+ *
4
+ * Two actions, because those are the two that need the bus. `assign` sends the
5
+ * handoff **first** and sets the owner **second**: a card assigned without a message
6
+ * is a card nobody knows about, while a message for a card that then changed is one
7
+ * the peer can still claim. Either failure is reported with the card as it actually
8
+ * is, so the lead's next call starts from the truth.
9
+ *
10
+ * `reclaim` needs no message — it is the compensation for a dispatch that did not
11
+ * land — and it belongs here rather than in `board` because it only makes sense in
12
+ * the lead's context.
13
+ */
14
+
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+ import type { Static } from "typebox";
17
+ import { type Card, cardRef, normalizeCardId } from "../../lib/board/card.ts";
18
+ import { indexOf } from "../../lib/board/links.ts";
19
+ import { type Actor, applyOp } from "../../lib/board/rules.ts";
20
+ import type { BoardSession } from "../../lib/board/session.ts";
21
+ import type { BoardStore } from "../../lib/board/store.ts";
22
+ import { messageOf } from "../../lib/errors.ts";
23
+ import { failedResult, type ToolFailure, textResult } from "../../lib/tool-result.ts";
24
+ import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
25
+ import { callLine, detailsOrLine } from "../../lib/tui/tool.ts";
26
+ import { plural } from "../../lib/board/round-text.ts";
27
+ import { strayField } from "../../lib/stray-fields.ts";
28
+ import { type Dispatch, handoffText, notListening, offerText, ownerFor, peerFor, sessionDispatch } from "./dispatch.ts";
29
+ import {
30
+ formatHandoff,
31
+ formatHandoffFailure,
32
+ formatOffer,
33
+ formatOfferFailure,
34
+ formatRacedOffer,
35
+ formatRacedAssignment,
36
+ formatReclaimed,
37
+ } from "./format.ts";
38
+ import { TeamParams } from "./schema.ts";
39
+ import {
40
+ ACTION_FIELDS,
41
+ normalizeTeamAction,
42
+ TEAM_ACTIONS,
43
+ TEAM_TOOL,
44
+ type TeamAction,
45
+ type TeamDetails,
46
+ } from "./types.ts";
47
+
48
+ /** The parameters as the schema validates them. */
49
+ type TeamArgs = Static<typeof TeamParams>;
50
+
51
+ /** One refusal: the reason for the model, the state for the renderer. */
52
+ function refusal(action: TeamAction, message: string, card?: Card): ToolFailure<TeamDetails> {
53
+ return failedResult(`Error: ${message}`, { action, error: message, ...(card === undefined ? {} : { card }) });
54
+ }
55
+
56
+ /** The call line's subject: the verb, the card, and the peer it names. */
57
+ function callSubject(action: TeamAction, args: Record<string, unknown>): string {
58
+ const card = typeof args.card === "string" && args.card.trim() !== "" ? cardRef(args.card.trim()) : "";
59
+ const to = typeof args.to === "string" && args.to.trim() !== "" ? ` → ${args.to.trim()}` : "";
60
+ return card === "" ? action : `${action} ${card}${action === "assign" ? to : ""}`;
61
+ }
62
+
63
+ /** What the tool needs from the outside, so a suite drives it without a bus. */
64
+ export interface TeamToolsDeps {
65
+ /** Test seam: the handoff transport. */
66
+ dispatch?: (ctx: Parameters<typeof sessionDispatch>[0]) => Dispatch;
67
+ }
68
+
69
+ export function registerTools(pi: ExtensionAPI, session: BoardSession, deps: TeamToolsDeps = {}): void {
70
+ const dispatchFor = deps.dispatch ?? sessionDispatch;
71
+
72
+ pi.registerTool({
73
+ name: TEAM_TOOL,
74
+ label: "Team",
75
+ description:
76
+ "Run a round of work over the shared board: `assign` hands a card to a running session and sends it a handoff " +
77
+ "(`to` is a teammate's label or id, as `/team` lists them); `offer` broadcasts an unclaimed card to every other " +
78
+ "live session and writes nothing, so the first to claim it takes it; `reclaim` takes a card back to To do and " +
79
+ "sends nothing, so it needs no team. Use `board` for everything else. Nothing here starts a peer's turn: a " +
80
+ "message waits in its inbox, and a peer that is listening reports it at its next turn.",
81
+ promptSnippet: "Run a round over the board: assign a card to a running session, offer one, or reclaim one.",
82
+ promptGuidelines: [
83
+ "Open the board with `/board` or work a card first, so this session and its peers are on the team.",
84
+ "Offer a card nobody has taken; assign one you mean a particular peer to hold.",
85
+ "When a result says `Not listening`, say so: the message waits for that peer's next turn.",
86
+ "Reclaim a card whose owner is gone or has gone quiet rather than editing someone else's work. A " +
87
+ "`gone:` owner's process is not running, so what it held is anyone's to reclaim.",
88
+ ],
89
+ parameters: TeamParams,
90
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
91
+ executionMode: "sequential",
92
+
93
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
94
+ const args = params as TeamArgs;
95
+ const action = normalizeTeamAction(args.action);
96
+ if (!action) {
97
+ return refusal("assign", `unknown action "${String(args.action)}"; use ${TEAM_ACTIONS.join(" or ")}.`);
98
+ }
99
+ // Before the store and before the card: a refused call has done nothing, so it
100
+ // cannot have read a card or sent a message on its way to the refusal.
101
+ const stray = strayField(action, args, ACTION_FIELDS);
102
+ if (stray !== undefined) return refusal(action, stray);
103
+ const id = normalizeCardId(args.card);
104
+ if (id === undefined) return refusal(action, `"${String(args.card)}" is not a card id.`);
105
+ const me = session.who(ctx, ctx.cwd);
106
+ try {
107
+ const store = await session.store(ctx.cwd);
108
+ const now = session.now();
109
+ if (action === "reclaim") return await reclaimCard(store, me, id, now);
110
+ if (action === "offer") return await offerCard(store, me, id, args.note, now, dispatchFor(ctx));
111
+ const to = typeof args.to === "string" ? args.to.trim() : "";
112
+ if (to === "") return refusal("assign", 'assign needs a peer: pass `to`, e.g. "billing-core".');
113
+ return await assignCard(store, me, id, to, args.note, now, dispatchFor(ctx));
114
+ } catch (error) {
115
+ return refusal(action, messageOf(error));
116
+ }
117
+ },
118
+
119
+ renderCall(args, theme, context) {
120
+ const action = normalizeTeamAction(args.action) ?? "assign";
121
+ return callLine(theme, context.lastComponent, TEAM_TOOL, callSubject(action, args as Record<string, unknown>));
122
+ },
123
+
124
+ renderResult(result, _options, theme, context) {
125
+ const text = reuseText(context?.lastComponent);
126
+ const details = toolDetails<TeamDetails>(result.details);
127
+ const outcome = detailsOrLine(theme, result, false, details);
128
+ text.setText("line" in outcome ? outcome.line : summaryOf(outcome.details));
129
+ return text;
130
+ },
131
+ });
132
+ }
133
+
134
+ /**
135
+ * `assign`: check the preconditions, send the handoff, then set the owner.
136
+ *
137
+ * The preconditions are checked with the same `applyOp` the store will use, so an
138
+ * assignment the rules would refuse never sends a message at all.
139
+ */
140
+ async function assignCard(
141
+ store: BoardStore,
142
+ me: Actor,
143
+ id: string,
144
+ to: string,
145
+ note: string | undefined,
146
+ now: number,
147
+ dispatch: Dispatch,
148
+ ): Promise<ReturnType<typeof textResult<TeamDetails>> | ToolFailure<TeamDetails>> {
149
+ const card = await store.read(id);
150
+ if (!card) return refusal("assign", `${cardRef(id)} does not exist.`);
151
+ const owner: Actor = { label: to };
152
+ // The board, not just the card: a card that waits on unfinished work is refused here,
153
+ // so a handoff for it is never sent.
154
+ const dry = applyOp(card, { kind: "assign", to: owner }, me, now, indexOf(await store.list()));
155
+ if (!dry.ok) return refusal("assign", dry.error, card);
156
+
157
+ let delivered: string[];
158
+ try {
159
+ delivered = (await dispatch.send({ kind: "handoff", to, text: handoffText(card, to, note) })).delivered;
160
+ } catch (error) {
161
+ return refusal("assign", formatHandoffFailure(card, to, messageOf(error)), card);
162
+ }
163
+
164
+ // Ask the bus which session answers to the label: an owner recorded with its id
165
+ // survives the peer renaming itself, which a label alone does not — and the same read
166
+ // says whether that peer will be woken by the message it just got.
167
+ const peers = await dispatch.peers().catch(() => undefined);
168
+ const peer = peerFor(peers, to);
169
+ const applied = await store.mutate(
170
+ id,
171
+ { kind: "assign", to: peer === undefined ? ownerFor(to, delivered) : { id: peer.id, label: peer.label } },
172
+ me,
173
+ now,
174
+ );
175
+ if (!applied.ok)
176
+ return refusal("assign", formatRacedAssignment(applied.card ?? card, to, applied.error), applied.card ?? card);
177
+ const silent = notListening(peers, delivered);
178
+ return textResult(formatHandoff(applied.card, to, delivered, silent), {
179
+ action: "assign",
180
+ card: applied.card,
181
+ assigned: applied.card.owner,
182
+ delivered,
183
+ ...(silent.length === 0 ? {} : { notListening: silent }),
184
+ });
185
+ }
186
+
187
+ /**
188
+ * The actor an offer's preconditions are judged as: nobody in particular.
189
+ *
190
+ * `claim` is what the offer asks a peer to do, so the dry run is a claim by someone who
191
+ * holds nothing — the rules' own refusals decide whether a card may be offered at all,
192
+ * without a second copy of the sentences.
193
+ */
194
+ const OFFER_CLAIMANT: Actor = { label: "*" };
195
+
196
+ /**
197
+ * `offer`: check the claim's preconditions, broadcast the card, then record the offer.
198
+ *
199
+ * The broadcast comes first and the record second, because a record of an offer nobody
200
+ * received would be worse than an offer with no record: the card is unchanged by a failed
201
+ * send, and the first peer to claim is what makes an offer into work. What the record
202
+ * adds is the one thing an event cannot carry — when it was made — so a lead can tell an
203
+ * offer nobody took from a card nobody has looked at.
204
+ */
205
+ async function offerCard(
206
+ store: BoardStore,
207
+ me: Actor,
208
+ id: string,
209
+ note: string | undefined,
210
+ now: number,
211
+ dispatch: Dispatch,
212
+ ): Promise<ReturnType<typeof textResult<TeamDetails>> | ToolFailure<TeamDetails>> {
213
+ const card = await store.read(id);
214
+ if (!card) return refusal("offer", `${cardRef(id)} does not exist.`);
215
+ // The board, not just the card: a card that waits on unfinished work, or one somebody
216
+ // holds, is refused here, so a broadcast for it is never sent.
217
+ const dry = applyOp(card, { kind: "claim" }, OFFER_CLAIMANT, now, indexOf(await store.list()));
218
+ if (!dry.ok) return refusal("offer", dry.error, card);
219
+
220
+ let delivered: string[];
221
+ try {
222
+ delivered = (await dispatch.send({ kind: "event", to: "*", text: offerText(card, note) })).delivered;
223
+ } catch (error) {
224
+ return refusal("offer", formatOfferFailure(card, messageOf(error)), card);
225
+ }
226
+ // The peers have the event, so a card that changed underneath is reported as raced
227
+ // rather than refused: the message is already in their inboxes.
228
+ const recorded = await store.mutate(id, { kind: "offer" }, me, now);
229
+ if (!recorded.ok) return refusal("offer", formatRacedOffer(recorded.card ?? card, recorded.error), recorded.card);
230
+ const peers = await dispatch.peers().catch(() => undefined);
231
+ const silent = notListening(peers, delivered);
232
+ return textResult(formatOffer(recorded.card, delivered, silent), {
233
+ action: "offer",
234
+ card: recorded.card,
235
+ delivered,
236
+ ...(silent.length === 0 ? {} : { notListening: silent }),
237
+ });
238
+ }
239
+
240
+ /** `reclaim`: the owner clears and the card returns to To do, notes and all. */
241
+ async function reclaimCard(
242
+ store: BoardStore,
243
+ me: Actor,
244
+ id: string,
245
+ now: number,
246
+ ): Promise<ReturnType<typeof textResult<TeamDetails>> | ToolFailure<TeamDetails>> {
247
+ const before = await store.read(id);
248
+ if (!before) return refusal("reclaim", `${cardRef(id)} does not exist.`);
249
+ const previous = before.owner?.label;
250
+ if (previous === undefined) return refusal("reclaim", `${cardRef(id)} has no owner to reclaim.`, before);
251
+ const result = await store.mutate(id, { kind: "reclaim" }, me, now);
252
+ if (!result.ok) return refusal("reclaim", result.error, result.card ?? before);
253
+ return textResult(formatReclaimed(result.card, previous), { action: "reclaim", card: result.card });
254
+ }
255
+
256
+ /** The collapsed line for a successful call, from its details. */
257
+ function summaryOf(details: TeamDetails): string {
258
+ const card = details.card;
259
+ if (!card) return details.action === "reclaim" ? "reclaimed" : "handed off";
260
+ if (details.action === "offer") {
261
+ const count = details.delivered?.length ?? 0;
262
+ return `${cardRef(card.id)} offered to ${plural(count, "session")}`;
263
+ }
264
+ const who = details.assigned?.label ?? card.owner?.label;
265
+ const target = details.action === "assign" && who !== undefined ? ` → ${who}` : "";
266
+ return `${cardRef(card.id)} ${card.status}${target}`;
267
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The shapes `team` registers and reports.
3
+ *
4
+ * A `team` result is one card plus what happened to the handoff, so the details say
5
+ * both: `assigned` is who the card went to, `delivered` is the labels the bus wrote
6
+ * a message file for, and `messageError` is the send that never landed — a card is
7
+ * never left in a state the report does not describe.
8
+ */
9
+
10
+ import type { Card } from "../../lib/board/card.ts";
11
+ import type { CardOwner } from "../../lib/board/card.ts";
12
+
13
+ /** The tool's name. `board` owns the cards; `team` owns the round. */
14
+ export const TEAM_TOOL = "team";
15
+
16
+ /** The three actions: the lead's whole surface — hand out, offer, take back. */
17
+ export const TEAM_ACTIONS = ["assign", "offer", "reclaim"] as const;
18
+
19
+ /** One action. */
20
+ export type TeamAction = (typeof TEAM_ACTIONS)[number];
21
+
22
+ /** What one call did. */
23
+ export interface TeamDetails {
24
+ action: TeamAction;
25
+ /** The card after the action, or the one a refusal left alone. */
26
+ card?: Card;
27
+ /** Who the card was assigned to, when the assignment happened. */
28
+ assigned?: CardOwner;
29
+ /**
30
+ * The labels a message file was written for, when the send landed: the one peer for
31
+ * `assign`, every other live session for `offer`.
32
+ */
33
+ delivered?: string[];
34
+ /**
35
+ * The peers the message reached whose record says they are not listening, so it waits
36
+ * for their next turn instead of being raised as one. Absent when every peer listens.
37
+ */
38
+ notListening?: string[];
39
+ /** Why the handoff was not sent; the card is untouched when this is set. */
40
+ messageError?: string;
41
+ /** A refusal's reason, which the renderer draws instead of the details. */
42
+ error?: string;
43
+ }
44
+
45
+ /**
46
+ * The fields each action acts on: what a call may carry, per action.
47
+ *
48
+ * The parameters are one flat object, so a field is published to every action at once,
49
+ * and only this table stops one reaching an action that would drop it. `to` is `assign`'s
50
+ * alone: an `offer` that named a peer was broadcast to every live session instead, and a
51
+ * `reclaim` that named one took the card back from whoever held it.
52
+ *
53
+ * A test holds the other direction — every published parameter but `action` is claimed
54
+ * here — so a field cannot be published without an action that acts on it.
55
+ */
56
+ export const ACTION_FIELDS: Record<TeamAction, readonly string[]> = {
57
+ assign: ["card", "to", "note"],
58
+ offer: ["card", "note"],
59
+ reclaim: ["card"],
60
+ };
61
+
62
+ /** Whether a value is one of the actions, so an unknown one is a refusal and not a crash. */
63
+ export function normalizeTeamAction(raw: unknown): TeamAction | undefined {
64
+ return typeof raw === "string" && TEAM_ACTIONS.includes(raw as TeamAction) ? (raw as TeamAction) : undefined;
65
+ }
@@ -60,7 +60,10 @@ export default function todo(pi: ExtensionAPI) {
60
60
  pi.on("tool_execution_end", (event) => {
61
61
  if (event.isError) return;
62
62
  const annotations = pi.getAllTools().find((tool) => tool.name === event.toolName)?.annotations;
63
- if (!isMutatingTool(event.toolName, annotations)) return;
63
+ // The result is the only place a call's own record of what it did survives to this
64
+ // event, which is why the board's read action is read from here.
65
+ const details = (event.result as { details?: unknown } | undefined)?.details;
66
+ if (!isMutatingTool(event.toolName, annotations, details)) return;
64
67
  runtime.noteWork(isFileWrite(event.toolName, annotations));
65
68
  });
66
69
 
@@ -14,7 +14,8 @@
14
14
  */
15
15
 
16
16
  import type { ToolAnnotations } from "@earendil-works/pi-coding-agent";
17
- import { TODO_TOOL } from "../../lib/tool-names.ts";
17
+ import { isBoardRead } from "../../lib/policy.ts";
18
+ import { BOARD_TOOL, TODO_TOOL } from "../../lib/tool-names.ts";
18
19
  import { currentTodo, hasOpenTodos } from "./state.ts";
19
20
  import type { Todo } from "./types.ts";
20
21
 
@@ -32,9 +33,14 @@ export const MUTATING_TOOL_NAMES: ReadonlySet<string> = new Set(["write", "edit"
32
33
  * Whether a finished tool call counts as work for lag detection. The `todo`
33
34
  * tool never does (it is the update itself), a known mutator always does, and
34
35
  * any other tool counts only when it explicitly declares `readOnlyHint: false`.
36
+ *
37
+ * `board` is the one mutator whose read face is provable from what the call
38
+ * returned: a result whose `details` say `action: "list"` read the cards and changed
39
+ * nothing, so it counts like every other reader.
35
40
  */
36
- export function isMutatingTool(toolName: string, annotations?: ToolAnnotations): boolean {
41
+ export function isMutatingTool(toolName: string, annotations?: ToolAnnotations, details?: unknown): boolean {
37
42
  if (toolName === TODO_TOOL) return false;
43
+ if (toolName === BOARD_TOOL && isBoardRead(details)) return false;
38
44
  if (MUTATING_TOOL_NAMES.has(toolName)) return true;
39
45
  return annotations?.readOnlyHint === false;
40
46
  }
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type { WebFetchConfig } from "./config.ts";
13
13
  import { extractReadable } from "./extract.ts";
14
+ import { renderStructured } from "./structured.ts";
14
15
  import { extractPdfText, type PdfExtractor } from "../../../lib/pdf.ts";
15
16
  import { createFetchRunner, type HttpRunner, type HttpResponse } from "../http.ts";
16
17
  import { decodeHtml } from "../charset.ts";
@@ -157,7 +158,9 @@ export async function runFetch(
157
158
  title = page.title;
158
159
  text = page.text;
159
160
  } else if (isTextualType(response.contentType)) {
160
- text = response.body;
161
+ // Rendered here, once per fetch, because the result is what the cache stores:
162
+ // paging and `find` then read one stable string instead of a fresh rendering.
163
+ text = renderStructured(response.body, response.contentType) ?? response.body;
161
164
  } else {
162
165
  text = `(binary content: ${response.contentType || "unknown"}, ${response.sizeBytes} bytes; web_fetch returns text only)`;
163
166
  }