@gavin-hu/my-pi-agent 0.18.0 → 0.19.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 (249) hide show
  1. package/README.md +52 -44
  2. package/README.zh-CN.md +46 -41
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +344 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +446 -404
  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 +554 -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} +61 -50
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
  14. package/{extensions/plan/README.md → docs/extensions/plan.md} +390 -317
  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 +285 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
  19. package/docs/extensions/team.md +226 -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} +258 -162
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
  25. package/extensions/board/commands.ts +87 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +72 -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 +312 -0
  32. package/extensions/board/result.ts +99 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +75 -0
  35. package/extensions/board/seat.ts +29 -0
  36. package/extensions/board/snapshot.ts +41 -0
  37. package/extensions/board/tools.ts +279 -0
  38. package/extensions/board/transport/router.ts +172 -0
  39. package/extensions/board/transport/server.ts +149 -0
  40. package/extensions/board/types.ts +54 -0
  41. package/extensions/board/view/actions.ts +306 -0
  42. package/extensions/board/view/client.ts +41 -0
  43. package/extensions/board/view/keys.ts +121 -0
  44. package/extensions/board/view/live.ts +397 -0
  45. package/extensions/board/view/page.ts +158 -0
  46. package/extensions/board/view/render.ts +496 -0
  47. package/extensions/board/view/styles.ts +211 -0
  48. package/extensions/board/view/team.ts +305 -0
  49. package/extensions/board/writes.ts +99 -0
  50. package/extensions/bytefly/panel/commands.ts +40 -7
  51. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  52. package/extensions/bytefly/panel/settings.ts +3 -20
  53. package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
  54. package/extensions/bytefly/picker/discovery.ts +0 -2
  55. package/extensions/bytefly/picker/types.ts +0 -2
  56. package/extensions/bytefly/update/check.ts +39 -0
  57. package/extensions/bytefly/update/format.ts +15 -0
  58. package/extensions/bytefly/update/register.ts +73 -34
  59. package/extensions/bytefly/update/state.ts +6 -3
  60. package/extensions/context/tui.ts +1 -1
  61. package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
  62. package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
  63. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  64. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  65. package/extensions/file-browse/index.ts +4 -4
  66. package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
  67. package/extensions/file-browse/{router.ts → transport/router.ts} +21 -15
  68. package/extensions/file-browse/transport/server.ts +110 -0
  69. package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
  70. package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
  71. package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
  72. package/extensions/file-browse/{html.ts → view/html.ts} +14 -12
  73. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  74. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
  75. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
  76. package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
  77. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  78. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  79. package/extensions/file-read/schema.ts +2 -1
  80. package/extensions/memory/store.ts +12 -5
  81. package/extensions/plan/blocks.ts +23 -2
  82. package/extensions/plan/commands.ts +21 -16
  83. package/extensions/plan/feedback.ts +147 -38
  84. package/extensions/plan/index.ts +6 -4
  85. package/extensions/plan/plan-index.ts +43 -9
  86. package/extensions/plan/plan-text.ts +33 -23
  87. package/extensions/plan/plans.ts +8 -11
  88. package/extensions/plan/policy.ts +20 -12
  89. package/extensions/plan/review.ts +3 -27
  90. package/extensions/plan/runtime.ts +21 -9
  91. package/extensions/plan/sections.ts +112 -0
  92. package/extensions/plan/steps.ts +3 -22
  93. package/extensions/plan/tools/exit.ts +11 -95
  94. package/extensions/plan/tui.ts +5 -34
  95. package/extensions/plan/types.ts +0 -8
  96. package/extensions/plan/web-client.ts +1 -1
  97. package/extensions/plan/web-page.ts +0 -7
  98. package/extensions/plan/web-router.ts +3 -57
  99. package/extensions/plan/web-share-session.ts +24 -30
  100. package/extensions/plan/web-streams.ts +14 -14
  101. package/extensions/plan/web-tokens.ts +2 -2
  102. package/extensions/plan/web.ts +66 -120
  103. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  104. package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
  105. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  106. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  107. package/extensions/rewind/index.ts +3 -3
  108. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  109. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  110. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  111. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  112. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  113. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  114. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  115. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  116. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  117. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  118. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  119. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  120. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  121. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  122. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  123. package/extensions/session-bus/commands.ts +17 -69
  124. package/extensions/session-bus/format.ts +22 -45
  125. package/extensions/session-bus/index.ts +13 -7
  126. package/extensions/session-bus/join-request.ts +40 -0
  127. package/extensions/session-bus/mail-watch.ts +134 -0
  128. package/extensions/session-bus/notice.ts +32 -4
  129. package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
  130. package/extensions/session-bus/protocol.ts +19 -49
  131. package/extensions/session-bus/resolve.ts +67 -0
  132. package/extensions/session-bus/result.ts +16 -51
  133. package/extensions/session-bus/rows.ts +4 -2
  134. package/extensions/session-bus/runtime.ts +129 -238
  135. package/extensions/session-bus/store.ts +42 -6
  136. package/extensions/session-bus/tools.ts +31 -66
  137. package/extensions/session-bus/tui.ts +4 -4
  138. package/extensions/session-bus/types.ts +13 -13
  139. package/extensions/subagent/agents.ts +9 -8
  140. package/extensions/subagent/invocation.ts +4 -2
  141. package/extensions/team/commands.ts +112 -0
  142. package/extensions/team/dispatch.ts +133 -0
  143. package/extensions/team/format.ts +105 -0
  144. package/extensions/team/index.ts +33 -0
  145. package/extensions/team/schema.ts +29 -0
  146. package/extensions/team/tools.ts +246 -0
  147. package/extensions/team/types.ts +48 -0
  148. package/extensions/web-access/fetch/page.ts +4 -1
  149. package/extensions/web-access/fetch/structured.ts +447 -0
  150. package/extensions/web-access/fetch/tool.ts +2 -1
  151. package/extensions/web-access/search/tool.ts +2 -1
  152. package/extensions/wechat/bridge.ts +76 -92
  153. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  154. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  155. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  156. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  157. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  158. package/extensions/wechat/dialog/channels.ts +136 -0
  159. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
  160. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
  161. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  162. package/extensions/wechat/index.ts +10 -10
  163. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  164. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  165. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
  166. package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
  167. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
  168. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  169. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  170. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  171. package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
  172. package/extensions/wechat/tools.ts +29 -12
  173. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  174. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
  175. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  176. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  177. package/extensions/wechat/transport/outbound.ts +142 -0
  178. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  179. package/extensions/wechat/types.ts +10 -1
  180. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  181. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  182. package/extensions/worktree/guard.ts +3 -5
  183. package/lib/README.md +8 -6
  184. package/lib/board/card.ts +290 -0
  185. package/lib/board/links.ts +117 -0
  186. package/lib/board/page-pointer.ts +256 -0
  187. package/lib/board/round-text.ts +72 -0
  188. package/lib/board/round.ts +89 -0
  189. package/lib/board/rules.ts +304 -0
  190. package/lib/board/session.ts +105 -0
  191. package/lib/board/store.ts +310 -0
  192. package/lib/board/view.ts +150 -0
  193. package/lib/errors.ts +12 -4
  194. package/lib/format.ts +13 -0
  195. package/lib/http/guard.ts +112 -0
  196. package/lib/http/index.ts +16 -5
  197. package/lib/http/serve.ts +234 -0
  198. package/lib/http/server.ts +12 -10
  199. package/lib/http/sink.ts +110 -0
  200. package/lib/http/token.ts +38 -0
  201. package/lib/policy.ts +12 -2
  202. package/lib/requirements.ts +6 -7
  203. package/lib/session-label.ts +64 -0
  204. package/lib/session-roster.ts +130 -0
  205. package/lib/session-tool.ts +184 -0
  206. package/lib/tool-names.ts +25 -2
  207. package/lib/tui/glyphs.ts +2 -0
  208. package/lib/web/README.md +5 -2
  209. package/lib/web/components.ts +5 -0
  210. package/lib/web/runtime.ts +7 -1
  211. package/lib/web/tokens.ts +25 -2
  212. package/package.json +26 -14
  213. package/skills/next/SKILL.md +4 -3
  214. package/skills/project-docs/SKILL.md +133 -0
  215. package/skills/project-docs/references/audit.md +131 -0
  216. package/skills/project-docs/references/changelog.md +105 -0
  217. package/skills/project-docs/references/discovery.md +127 -0
  218. package/skills/project-docs/references/readme.md +80 -0
  219. package/skills/project-docs/references/release.md +84 -0
  220. package/skills/project-docs/references/roadmap.md +93 -0
  221. package/skills/project-docs/scripts/state.ts +316 -0
  222. package/skills/refactor/SKILL.md +100 -125
  223. package/skills/refactor/references/handoff.md +35 -0
  224. package/skills/refactor/references/signals.md +54 -0
  225. package/skills/refactor/scripts/inventory.ts +587 -0
  226. package/extensions/file-browse/README.md +0 -506
  227. package/extensions/file-browse/server.ts +0 -141
  228. package/extensions/plan/split.ts +0 -87
  229. package/extensions/session-bus/README.md +0 -247
  230. package/extensions/session-bus/team.ts +0 -102
  231. package/extensions/wechat/outbound.ts +0 -100
  232. package/lib/plans/sections.ts +0 -172
  233. package/lib/plans/setting.ts +0 -52
  234. package/lib/plans/split.ts +0 -215
  235. package/lib/plans/store.ts +0 -19
  236. package/lib/plans/team.ts +0 -151
  237. package/skills/changelog/SKILL.md +0 -148
  238. package/skills/parallel-dev/SKILL.md +0 -150
  239. /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
  240. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  241. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  242. /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
  243. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  244. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  245. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  246. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  247. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  248. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  249. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -2,10 +2,10 @@
2
2
  * The `session` tool.
3
3
  *
4
4
  * One action-shaped tool keeps the prompt surface small: status, send, inbox,
5
- * claim, release, join, leave, watch, name. The runtime owns file I/O and
6
- * lifecycle; this module only validates the action, calls the runtime, and hands
7
- * the result to the shared presentation module — one collapsed line, or the
8
- * board's rows on expand.
5
+ * claim, release, join. The runtime owns file I/O and lifecycle; this
6
+ * module only validates the action, calls the runtime, and hands the result to
7
+ * the shared presentation module — one collapsed line, or the board's rows on
8
+ * expand.
9
9
  */
10
10
 
11
11
  import { StringEnum } from "@earendil-works/pi-ai";
@@ -14,16 +14,7 @@ import { Type, type Static } from "typebox";
14
14
  import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
15
15
  import { errorResult, textResult } from "../../lib/tool-result.ts";
16
16
  import { callLine, detailsOrLine } from "../../lib/tui/tool.ts";
17
- import {
18
- formatClaim,
19
- formatInbox,
20
- formatJoin,
21
- formatName,
22
- formatRelease,
23
- formatSend,
24
- formatStatus,
25
- formatWatch,
26
- } from "./format.ts";
17
+ import { formatClaim, formatInbox, formatJoin, formatRelease, formatSend, formatStatus } from "./format.ts";
27
18
  import { normalizeAction, TOOL_NAME } from "./protocol.ts";
28
19
  import type { SessionBus } from "./runtime.ts";
29
20
  import { collapsedLine, resultSections, SessionResult } from "./result.ts";
@@ -32,8 +23,8 @@ import type { SessionDetails } from "./types.ts";
32
23
  export { TOOL_NAME };
33
24
 
34
25
  const SessionParams = Type.Object({
35
- action: StringEnum(["status", "send", "inbox", "claim", "release", "join", "leave", "watch", "name"] as const, {
36
- description: "status, send, inbox, claim, release, join, leave, watch, or name.",
26
+ action: StringEnum(["status", "send", "inbox", "claim", "release", "join"] as const, {
27
+ description: "status, send, inbox, claim, release, or join.",
37
28
  }),
38
29
  to: Type.Optional(Type.String({ description: "Target session label/id, or * for an event broadcast." })),
39
30
  text: Type.Optional(Type.String({ description: "Message text, at most 4000 characters." })),
@@ -50,19 +41,16 @@ const SessionParams = Type.Object({
50
41
  ),
51
42
  wake: Type.Optional(
52
43
  Type.Boolean({
53
- description:
54
- "With watch: set the mode — true raises one turn per burst, false waits for the next turn, omitted keeps it.",
44
+ description: "With join: mail raises one turn per burst once the agent is idle; omitted waits for the next turn.",
55
45
  }),
56
46
  ),
57
- stop: Type.Optional(Type.Boolean({ description: "With watch: turn this session's inbox watch off." })),
58
- team: Type.Optional(
59
- Type.String({
60
- description: "With join/leave: the split's name — the plan file name without `.md`.",
47
+ listen: Type.Optional(
48
+ Type.Boolean({
49
+ description: "With join: false joins without listening, so mail waits in the inbox until `inbox` reads it.",
61
50
  }),
62
51
  ),
63
- unit: Type.Optional(Type.String({ description: "With join/leave: the unit to claim or release, e.g. `core`." })),
64
52
  label: Type.Optional(
65
- Type.String({ description: "With name: what other sessions call this one, e.g. `billing-lead`." }),
53
+ Type.String({ description: "With join: what other sessions call this one, e.g. `billing-lead`." }),
66
54
  ),
67
55
  });
68
56
 
@@ -74,19 +62,14 @@ type SessionArgs = Static<typeof SessionParams>;
74
62
  * identity of the call.
75
63
  */
76
64
  function callSubject(action: string, args: Record<string, unknown>): string {
77
- if (action === "name") {
65
+ if (action === "join") {
78
66
  const label = typeof args.label === "string" ? args.label.trim() : "";
79
- return label ? `name ${label}` : "name";
67
+ return label ? `join ${label}` : "join";
80
68
  }
81
69
  if (action === "send") {
82
70
  const to = typeof args.to === "string" ? args.to : "";
83
71
  return to ? `send ${to}` : "send";
84
72
  }
85
- if (action === "join" || action === "leave") {
86
- const unit = typeof args.unit === "string" ? args.unit : "";
87
- const team = typeof args.team === "string" ? args.team : "";
88
- return [action, unit, team && !unit ? team : ""].filter(Boolean).join(" ");
89
- }
90
73
  if (action !== "claim" && action !== "release") return action;
91
74
  const paths = Array.isArray(args.paths) ? args.paths.filter((path): path is string => typeof path === "string") : [];
92
75
  const [first, ...rest] = paths;
@@ -99,16 +82,14 @@ export function registerTools(pi: ExtensionAPI, bus: SessionBus): void {
99
82
  name: TOOL_NAME,
100
83
  label: "Session bus",
101
84
  description:
102
- "Coordinate parallel Pi sessions on this repository. Actions: `status` (sessions, unread, claims, joined units), " +
85
+ "Coordinate parallel Pi sessions on this repository. Actions: `status` (sessions, unread, claims), " +
103
86
  "`send` (to a label/id, or `*` for an event), `inbox` (consume messages), `claim`/`release` (paths), " +
104
- "`join`/`leave` (a unit of an approved split, by `team` and `unit`), " +
105
- "`watch` (report new mail at your next turn, or raise one with `wake`; `stop` turns it off). " +
106
- "Delivery is pull-based unless this session arms `watch`.",
107
- promptSnippet: "Coordinate parallel sessions: status, send, inbox, claim, release, join, leave, watch, name.",
87
+ "`join` (get on the bus under a label, and listen for mail unless `listen: false`; `wake` raises a turn). " +
88
+ "Delivery is pull-based unless this session joined and is listening.",
89
+ promptSnippet: "Coordinate parallel sessions: status, send, inbox, claim, release, join.",
108
90
  promptGuidelines: [
109
- "Check `session inbox` when starting or joining a coordinated turn.",
91
+ "Check `session inbox` when starting a coordinated turn.",
110
92
  "Claim the paths you will edit before changing them; release them when done.",
111
- "When a plan was approved with a split, `join` its unit once at the start of the work and `leave` it when merged.",
112
93
  'Send `kind: "event"` for contract, build, or test changes another session needs.',
113
94
  "Never put credentials or secrets in a session-bus message.",
114
95
  ],
@@ -161,38 +142,22 @@ export function registerTools(pi: ExtensionAPI, bus: SessionBus): void {
161
142
  });
162
143
  }
163
144
  case "join": {
164
- const result = await bus.join({ team: args.team, unit: args.unit });
165
- return textResult(formatJoin(result.team, result.unit, result.claimed, result.conflicts), {
166
- action,
167
- team: result.team,
168
- unit: result.unit,
169
- claimed: result.claimed,
170
- conflicts: result.conflicts,
145
+ const label = typeof args.label === "string" ? args.label.trim() : "";
146
+ const joined = await bus.join({
147
+ ...(label === "" ? {} : { label }),
148
+ ...(args.wake === undefined ? {} : { wake: args.wake }),
149
+ ...(args.listen === undefined ? {} : { listen: args.listen }),
171
150
  });
172
- }
173
- case "leave": {
174
- const result = await bus.leave({ team: args.team, unit: args.unit });
175
- return textResult(formatRelease(result.released, result.notOwned), {
151
+ // A label is the bus's name and Pi's session name in one act, so the footer,
152
+ // `/resume`, and the record a peer addresses agree. A bare join derives its
153
+ // label from the name Pi already shows and must not rename it back.
154
+ if (label !== "") pi.setSessionName(joined.label);
155
+ return textResult(formatJoin(joined.label, joined.watching), {
176
156
  action,
177
- released: result.released,
178
- notOwned: result.notOwned,
179
- ...(args.team === undefined ? {} : { team: args.team }),
180
- ...(args.unit === undefined ? {} : { unit: args.unit }),
157
+ label: joined.label,
158
+ watching: joined.watching,
181
159
  });
182
160
  }
183
- case "watch": {
184
- const watching = await bus.watch({ wake: args.wake, stop: args.stop });
185
- return textResult(formatWatch(watching), { action, watching });
186
- }
187
- case "name": {
188
- const label = typeof args.label === "string" ? args.label.trim() : "";
189
- if (!label) return errorResult(action, 'name requires a label, e.g. "billing-lead".');
190
- const named = await bus.rename(label);
191
- // The bus's label and Pi's session name are one identity: the footer,
192
- // `/resume`, and the record a peer addresses have to agree.
193
- pi.setSessionName(named);
194
- return textResult(formatName(named), { action, label: named });
195
- }
196
161
  }
197
162
  } catch (error) {
198
163
  return errorResult(action, error instanceof Error ? error.message : String(error));
@@ -17,7 +17,7 @@ import { type DetailContents, DockListScreen } from "../../lib/tui/dock-screen.t
17
17
  import { SEPARATORS } from "../../lib/tui/glyphs.ts";
18
18
  import { keyValueRow } from "../../lib/tui/rows.ts";
19
19
  import { paint } from "../../lib/tui/tokens.ts";
20
- import { shortId } from "./format.ts";
20
+ import { idSuffix } from "./format.ts";
21
21
  import {
22
22
  type SessionRow,
23
23
  claimRowLine,
@@ -48,7 +48,7 @@ const SCOPE_TITLE: Readonly<Record<BoardScope, string>> = {
48
48
  };
49
49
 
50
50
  const SCOPE_EMPTY: Readonly<Record<BoardScope, string>> = {
51
- sessions: "No sessions on this bus.",
51
+ sessions: "No sessions on this bus yet. Press w to join and listen.",
52
52
  inbox: "No unread mail.",
53
53
  claims: "No claims.",
54
54
  };
@@ -76,7 +76,7 @@ function paneFacts(theme: Theme, row: SessionRow, now: number, width: number): s
76
76
  const labelWidth = 10;
77
77
  return [
78
78
  keyValueRow(theme, width, { label: "mode", labelWidth, value: sanitizeLine(row.session.mode) }),
79
- keyValueRow(theme, width, { label: "id", labelWidth, value: shortId(row.session.id) }),
79
+ keyValueRow(theme, width, { label: "id", labelWidth, value: idSuffix(row.session.id) }),
80
80
  keyValueRow(theme, width, {
81
81
  label: "last seen",
82
82
  labelWidth,
@@ -229,7 +229,7 @@ export class SessionListComponent extends DockListScreen<BoardRow> {
229
229
  switch (this.scope) {
230
230
  case "sessions": {
231
231
  const sessions = rows.flatMap((row) => (row.kind === "session" ? [row.row] : []));
232
- return summaryCells(this.theme, sessions, watching);
232
+ return summaryCells(this.theme, sessions, watching, this.board?.status.onBus !== false);
233
233
  }
234
234
  case "inbox": {
235
235
  const cells = [paint(this.theme, "meta", count(rows.length, "unread message"))];
@@ -20,6 +20,14 @@ export interface SessionRecord {
20
20
  mode: SessionMode;
21
21
  startedAt: number;
22
22
  lastSeen: number;
23
+ /**
24
+ * Whether this session is listening for mail, written when it joins.
25
+ *
26
+ * Absent means "not listening": a record written before this existed and a session
27
+ * that stopped listening read the same way, which is the honest answer for a peer
28
+ * deciding whether mail will reach it.
29
+ */
30
+ watching?: { armed: boolean; wake: boolean };
23
31
  }
24
32
 
25
33
  /** One message waiting in a recipient's inbox. */
@@ -47,15 +55,10 @@ export interface ClaimRecord {
47
55
  /** What `session status` returns. */
48
56
  export interface SessionStatus {
49
57
  me: { id: string; label: string };
58
+ /** False while this session has only read the bus: it joins by naming itself. */
59
+ onBus: boolean;
50
60
  sessions: Array<SessionRecord & { unread: number }>;
51
61
  claims: ClaimRecord[];
52
- /** The newest split in this repository, and who holds each unit, on an explicit status call. */
53
- units?: {
54
- team: string;
55
- joined: number;
56
- total: number;
57
- rows: Array<{ unit: string; lead?: boolean; holder?: string }>;
58
- };
59
62
  /** Present only when this session armed its inbox watch. */
60
63
  watching?: { armed: boolean; wake: boolean };
61
64
  }
@@ -78,10 +81,7 @@ export interface BoardSnapshot {
78
81
 
79
82
  /** Structured result carried in the `session` tool's details. */
80
83
  export interface SessionDetails {
81
- action: "status" | "send" | "inbox" | "claim" | "release" | "join" | "leave" | "watch" | "name";
82
- /** With join/leave: the split and the unit, so a resumed transcript still says which. */
83
- team?: string;
84
- unit?: string;
84
+ action: "status" | "send" | "inbox" | "claim" | "release" | "join";
85
85
  status?: SessionStatus;
86
86
  delivered?: string[];
87
87
  messageId?: string;
@@ -90,9 +90,9 @@ export interface SessionDetails {
90
90
  conflicts?: ClaimConflict[];
91
91
  released?: string[];
92
92
  notOwned?: string[];
93
- /** Whether the inbox watch is on, and whether it raises a turn. */
93
+ /** Whether the inbox watch is on, and whether it raises a turn; `join` reports it. */
94
94
  watching?: { armed: boolean; wake: boolean };
95
- /** With name: the label the bus settled on, which is what this session is called. */
95
+ /** With join: the label the bus settled on, which is what this session is called. */
96
96
  label?: string;
97
97
  error?: string;
98
98
  }
@@ -16,6 +16,7 @@
16
16
  import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
18
  import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
19
+ import { READ_DOC_TOOL, WEB_FETCH_TOOL, WEB_SEARCH_TOOL } from "../../lib/tool-names.ts";
19
20
 
20
21
  /** Where an agent definition came from. */
21
22
  export type AgentSource = "builtin" | "user" | "project";
@@ -44,7 +45,7 @@ export const BUILTIN_AGENTS: readonly AgentConfig[] = [
44
45
  {
45
46
  name: "explorer",
46
47
  description: "Fast codebase recon that returns compressed context for handoff to other agents",
47
- tools: ["read", "grep", "find", "ls"],
48
+ tools: ["read", "grep", "find", "ls", READ_DOC_TOOL],
48
49
  source: "builtin",
49
50
  systemPrompt: `You are an explorer. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
50
51
 
@@ -94,7 +95,7 @@ Which file to look at first and why.`,
94
95
  name: "planner",
95
96
  description:
96
97
  "Delegated implementation planning in an isolated context (headless/chain; use plan mode for interactive planning)",
97
- tools: ["read", "grep", "find", "ls"],
98
+ tools: ["read", "grep", "find", "ls", READ_DOC_TOOL],
98
99
  source: "builtin",
99
100
  systemPrompt: `You are a delegated planning specialist working in an isolated context. The caller hands you planning when it wants a plan produced without adding exploration to its own transcript — for example an explorer → planner → worker chain, or a headless run.
100
101
 
@@ -134,7 +135,7 @@ Keep the plan concrete. The worker agent will execute it verbatim.`,
134
135
  {
135
136
  name: "reviewer",
136
137
  description: "Code review specialist for quality and security analysis",
137
- tools: ["read", "grep", "find", "ls", "bash"],
138
+ tools: ["read", "grep", "find", "ls", READ_DOC_TOOL, "bash"],
138
139
  source: "builtin",
139
140
  systemPrompt: `You are a senior code reviewer. Analyze code for quality, security, and maintainability.
140
141
 
@@ -168,7 +169,7 @@ Be specific with file paths and line numbers.`,
168
169
  {
169
170
  name: "worker",
170
171
  description: "General-purpose subagent with full capabilities and an isolated context window",
171
- tools: ["read", "write", "edit", "bash", "grep", "find", "ls"],
172
+ tools: ["read", "write", "edit", "bash", "grep", "find", "ls", READ_DOC_TOOL],
172
173
  source: "builtin",
173
174
  systemPrompt: `You are a worker agent with full capabilities. You operate in an isolated context window to handle delegated tasks without polluting the main conversation.
174
175
 
@@ -192,7 +193,7 @@ If handing off to another agent (e.g. reviewer), include:
192
193
  {
193
194
  name: "researcher",
194
195
  description: "Web research specialist that returns sourced findings on external questions",
195
- tools: ["read", "grep", "find", "ls", "web_search", "web_fetch"],
196
+ tools: ["read", "grep", "find", "ls", READ_DOC_TOOL, WEB_SEARCH_TOOL, WEB_FETCH_TOOL],
196
197
  source: "builtin",
197
198
  systemPrompt: `You are a research specialist. You investigate questions using web_search and web_fetch, then return findings another agent can act on.
198
199
 
@@ -228,7 +229,7 @@ What the caller should do with this, in 2-3 sentences.`,
228
229
  {
229
230
  name: "tester",
230
231
  description: "Writes or updates tests for a change and runs them",
231
- tools: ["read", "write", "edit", "grep", "find", "ls", "bash"],
232
+ tools: ["read", "write", "edit", "grep", "find", "ls", READ_DOC_TOOL, "bash"],
232
233
  source: "builtin",
233
234
  systemPrompt: `You are a test engineer. You add or update tests for a change and run them.
234
235
 
@@ -260,7 +261,7 @@ What is still untested and why.`,
260
261
  {
261
262
  name: "debugger",
262
263
  description: "Root-cause analysis for a failing test or bug, read-only plus running tests",
263
- tools: ["read", "grep", "find", "ls", "bash"],
264
+ tools: ["read", "grep", "find", "ls", READ_DOC_TOOL, "bash"],
264
265
  source: "builtin",
265
266
  systemPrompt: `You are a debugging specialist. You find the root cause of a failure from evidence, not guesses.
266
267
 
@@ -295,7 +296,7 @@ What else the bug or the fix could affect.`,
295
296
  {
296
297
  name: "documenter",
297
298
  description: "Updates docs, READMEs, and changelog entries to match the code",
298
- tools: ["read", "write", "edit", "grep", "find", "ls", "bash"],
299
+ tools: ["read", "write", "edit", "grep", "find", "ls", READ_DOC_TOOL, "bash"],
299
300
  source: "builtin",
300
301
  systemPrompt: `You are a documentation specialist. You keep docs accurate and consistent with the code.
301
302
 
@@ -6,13 +6,15 @@
6
6
  * import for invocation concerns.
7
7
  */
8
8
 
9
+ import { WORKSPACE_READERS } from "../../lib/policy.ts";
10
+ import { WEB_FETCH_TOOL, WEB_SEARCH_TOOL } from "../../lib/tool-names.ts";
9
11
  import type { AgentConfig } from "./agents.ts";
10
12
  import type { DispatchDefaults } from "./types.ts";
11
13
 
12
14
  export { getPiInvocation, type PiInvocation } from "../../lib/pi-invocation.ts";
13
15
 
14
- /** Tools a read-only delegation may use: structured readers and web readers only. */
15
- export const READ_ONLY_TOOLS = ["read", "grep", "find", "ls", "web_search", "web_fetch"] as const;
16
+ /** Tools a read-only delegation may use: structured readers, documents, and web readers only. */
17
+ export const READ_ONLY_TOOLS = [...WORKSPACE_READERS, WEB_SEARCH_TOOL, WEB_FETCH_TOOL] as const;
16
18
 
17
19
  /** Appended to a read-only agent's system prompt; the `--tools` list is the real gate. */
18
20
  export const READ_ONLY_NOTE =
@@ -0,0 +1,112 @@
1
+ /**
2
+ * `/team` — the lead's round, printed.
3
+ *
4
+ * The Team tab is the same round with a mouse; a lead reading a TUI should not have to
5
+ * open a browser to see what needs a decision, so the command prints the rollup the
6
+ * tool returns and one line saying who is live. It reads the cards and the bus's records
7
+ * and writes nothing: acting on the round is `team`'s tool (`assign`, `reclaim`) or
8
+ * `board`'s.
9
+ */
10
+
11
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
12
+ import type { Card } from "../../lib/board/card.ts";
13
+ import { buildRound } from "../../lib/board/round.ts";
14
+ import { formatRound, plural } from "../../lib/board/round-text.ts";
15
+ import type { BoardSession } from "../../lib/board/session.ts";
16
+ import { createTeamJoin, type JoinResult, type TeamJoin } from "../../lib/session-tool.ts";
17
+ import { readRoster, type RosterSession, rosterStateOf } from "../../lib/session-roster.ts";
18
+ import { formatJoined, formatNotJoined } from "./format.ts";
19
+
20
+ /** The command's name, which is also the word a user types. */
21
+ export const TEAM_COMMAND = "team";
22
+
23
+ /** The words the command accepts besides the round itself. */
24
+ export const TEAM_WORDS = ["join"] as const;
25
+
26
+ /** How many lines a round may print before it says how many it left out. */
27
+ export const MAX_ROUND_LINES = 10;
28
+
29
+ export function registerCommands(pi: ExtensionAPI, session: BoardSession, deps: { team?: TeamJoin } = {}): void {
30
+ const team = deps.team ?? createTeamJoin(pi);
31
+ pi.registerCommand(TEAM_COMMAND, {
32
+ description: "The lead's round: who holds what, and what needs a decision; `/team join [<label>]` joins the team",
33
+ getArgumentCompletions: (prefix: string) => {
34
+ const text = prefix.trimStart();
35
+ if (text.includes(" ")) return null;
36
+ const items = TEAM_WORDS.filter((word) => word.startsWith(text));
37
+ return items.length > 0 ? items.map((value) => ({ value, label: value })) : null;
38
+ },
39
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
40
+ const [word = "", ...rest] = args.trim().split(/\s+/).filter(Boolean);
41
+ if (word === "join") {
42
+ const label = rest.join(" ").trim();
43
+ const joined = await team.join(ctx, label === "" ? undefined : label);
44
+ ctx.ui.notify(joinedLine(joined), joined.ok ? "info" : "warning");
45
+ return;
46
+ }
47
+ if (word !== "") {
48
+ ctx.ui.notify(
49
+ `team: "${word}" is not a word this command knows — try /team, or /team join [<label>]`,
50
+ "warning",
51
+ );
52
+ return;
53
+ }
54
+ const store = await session.store(ctx.cwd);
55
+ const cards = await store.list();
56
+ // The roster is read as data, so a repository with no bus prints the round and
57
+ // says nothing about who is here rather than failing.
58
+ const roster = await readRoster(await session.busDir(ctx.cwd));
59
+ ctx.ui.notify(roundText(cards, session.now(), roster), "info");
60
+ },
61
+ });
62
+ }
63
+
64
+ /** `/team join`'s line: who this session is on the team, or why it is on none. */
65
+ export function joinedLine(result: JoinResult): string {
66
+ return result.ok ? formatJoined(result.label, result.watching) : formatNotJoined(result.error);
67
+ }
68
+
69
+ /**
70
+ * The round as the lines a lead reads: the rollup, then who is live.
71
+ *
72
+ * Capped, because a notify is a message and not a screen: past its limit the round says
73
+ * how many lines it left out instead of pushing the rest of the session up the screen.
74
+ */
75
+ export function roundText(cards: readonly Card[], now: number, roster?: readonly RosterSession[]): string {
76
+ if (cards.length === 0) return "team: nothing to report — the board is empty";
77
+ const round = formatRound(cards, buildRound(cards, now)).split("\n");
78
+ const shown = round.slice(0, MAX_ROUND_LINES);
79
+ if (round.length > MAX_ROUND_LINES) shown.push(`… ${plural(round.length - MAX_ROUND_LINES, "line")} more`);
80
+ const who = presenceLine(roster, now);
81
+ if (who !== undefined) shown.push(who);
82
+ return shown.join("\n");
83
+ }
84
+
85
+ /**
86
+ * `live: a, b · not listening: b · quiet: c`, or nothing when nobody is here.
87
+ *
88
+ * Three facts, and the middle one is the one a lead can act on: a live session that is
89
+ * not listening reads mail at its next turn rather than being woken for it. It is only
90
+ * named when its record says so — a session that never wrote the field is not accused of
91
+ * ignoring anything.
92
+ */
93
+ function presenceLine(roster: readonly RosterSession[] | undefined, now: number): string | undefined {
94
+ if (roster === undefined || roster.length === 0) return undefined;
95
+ const live: string[] = [];
96
+ const quiet: string[] = [];
97
+ const silent: string[] = [];
98
+ for (const session of roster) {
99
+ if (rosterStateOf(session, now) !== "live") {
100
+ quiet.push(session.label);
101
+ continue;
102
+ }
103
+ live.push(session.label);
104
+ if (session.watching?.armed === false) silent.push(session.label);
105
+ }
106
+ const parts = [
107
+ live.length > 0 ? `live: ${live.join(", ")}` : "",
108
+ silent.length > 0 ? `not listening: ${silent.join(", ")}` : "",
109
+ quiet.length > 0 ? `quiet: ${quiet.join(", ")}` : "",
110
+ ];
111
+ return parts.filter((part) => part !== "").join(" · ");
112
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The handoff: what `team assign` tells the peer, and how it tells it.
3
+ *
4
+ * The peer is a session that is already running, so this is a message and not a
5
+ * process: `session send` writes it into the recipient's inbox, and its armed watch is
6
+ * what raises the turn. Nothing here starts anything, and a send that fails leaves the
7
+ * card exactly as it was — the order is deliberate, because a card assigned without a
8
+ * message is a card nobody knows about.
9
+ *
10
+ * The transport is a seam (`Dispatch`), so the tool's ordering is testable without a
11
+ * bus, and the one production implementation is `lib/session-tool.ts`'s.
12
+ */
13
+
14
+ import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
15
+ import type { Card } from "../../lib/board/card.ts";
16
+ import { cardRef, type CardOwner } from "../../lib/board/card.ts";
17
+ import { listPeers, MAX_MESSAGE_TEXT, type Peer, send as sendTo } from "../../lib/session-tool.ts";
18
+
19
+ /** How a handoff reaches a peer, and what the bus can say about the peers there are. */
20
+ export interface Dispatch {
21
+ /** Send the text as `kind`, and report the labels a message file was written for. Throws on failure. */
22
+ send(input: { kind: "handoff" | "event"; to: string; text: string }): Promise<{ delivered: string[] }>;
23
+ /**
24
+ * Every session the bus lists, when it can say.
25
+ *
26
+ * Its record is what makes an assignment survive a rename — `holds` compares ids
27
+ * before labels — and what makes the report able to name a peer that cannot be
28
+ * woken. It never throws: a resolve that fails must not undo a handoff that landed.
29
+ */
30
+ peers(): Promise<Peer[] | undefined>;
31
+ }
32
+
33
+ /**
34
+ * What the peer reads.
35
+ *
36
+ * It names the card, its outcome, and the paths, and says what to do next: record the
37
+ * plan, work, note, finish. It does **not** ask the peer to claim the card — `assign`
38
+ * has already recorded the owner, and a claim by the holder is a no-op. The claim is
39
+ * only for the failure path where the send landed and the assignment did not.
40
+ */
41
+ export function handoffText(card: Card, to: string, note?: string): string {
42
+ const lines = [
43
+ `Handoff: ${cardRef(card.id)} "${card.title}" is yours, ${to}.`,
44
+ card.paths.length > 0 ? `Paths: ${card.paths.join(", ")}` : undefined,
45
+ card.plan === undefined ? undefined : `Plan: ${card.plan}`,
46
+ note === undefined ? undefined : `Note: ${note}`,
47
+ `It is already assigned to you — record the plan with board action "plan" if it has none, then note each phase and move it to done or blocked.`,
48
+ ].filter((line): line is string => line !== undefined);
49
+ return withinLimit(lines.join("\n"));
50
+ }
51
+
52
+ /**
53
+ * What every live session reads when the lead offers a card instead of assigning it.
54
+ *
55
+ * An offer names no owner: the first peer to claim the card takes it, so the text says
56
+ * what the card is and that claiming is the next step, and it never implies the work is
57
+ * already somebody's.
58
+ */
59
+ export function offerText(card: Card, note?: string): string {
60
+ const lines = [
61
+ `Offer: ${cardRef(card.id)} "${card.title}" is unclaimed.`,
62
+ card.paths.length > 0 ? `Paths: ${card.paths.join(", ")}` : undefined,
63
+ card.plan === undefined ? undefined : `Plan: ${card.plan}`,
64
+ note === undefined ? undefined : `Note: ${note}`,
65
+ `First to claim it takes it: run board action "claim" with card "${card.id}", then note each phase and move it to done or blocked.`,
66
+ ].filter((line): line is string => line !== undefined);
67
+ return withinLimit(lines.join("\n"));
68
+ }
69
+
70
+ /**
71
+ * Trim to the bus's text limit, which is also this message's limit.
72
+ *
73
+ * Not `lib/tui/columns.ts`'s `clip`: that one measures terminal columns, and this one
74
+ * counts characters, because the bus's limit is characters. The number arrives from
75
+ * `lib/session-tool.ts` — one copy for the two extensions that send through it.
76
+ */
77
+ function withinLimit(text: string): string {
78
+ return text.length > MAX_MESSAGE_TEXT ? `${text.slice(0, MAX_MESSAGE_TEXT - 1)}…` : text;
79
+ }
80
+
81
+ /** The production dispatch: `lib/session-tool.ts`, with a failure turned into a throw. */
82
+ export function sessionDispatch(ctx: ExtensionToolContext): Dispatch {
83
+ return {
84
+ async send(input) {
85
+ const result = await sendTo(ctx, input);
86
+ // `assign` and `offer` branch on a failed send, so this seam reports it the way
87
+ // they read it: the bus's sentence, as the reason the card was not handed over.
88
+ if (!result.ok) throw new Error(result.error);
89
+ return { delivered: result.delivered };
90
+ },
91
+ peers: () => listPeers(ctx),
92
+ };
93
+ }
94
+
95
+ /**
96
+ * The peer `to` names: an id exactly, else a label ignoring case.
97
+ *
98
+ * The send has already proved exactly one session answers to it, so the first match is
99
+ * the recipient; a miss means the bus could not say, which is why the caller falls back
100
+ * to the label alone.
101
+ */
102
+ export function peerFor(peers: readonly Peer[] | undefined, to: string): Peer | undefined {
103
+ if (peers === undefined) return undefined;
104
+ const target = to.trim();
105
+ const byId = peers.find((peer) => peer.id === target);
106
+ if (byId) return byId;
107
+ const wanted = target.toLowerCase();
108
+ return peers.find((peer) => peer.label.toLowerCase() === wanted);
109
+ }
110
+
111
+ /**
112
+ * The labels a message reached that cannot be woken: their record says they are not
113
+ * listening.
114
+ *
115
+ * A label the bus could not answer for is left out — "the record does not say" is not
116
+ * "nobody will read it" — and so is a peer the bus lists as listening.
117
+ */
118
+ export function notListening(peers: readonly Peer[] | undefined, labels: readonly string[]): string[] {
119
+ if (peers === undefined) return [];
120
+ return labels.filter((label) => {
121
+ const peer = peerFor(peers, label);
122
+ return peer !== undefined && peer.listening === false;
123
+ });
124
+ }
125
+
126
+ /**
127
+ * The peer a handoff names, as the card records it when the bus cannot say who that
128
+ * is: the label a message file was written for, else the one that was asked for.
129
+ */
130
+ export function ownerFor(to: string, delivered: readonly string[]): CardOwner {
131
+ const label = delivered[0] ?? to;
132
+ return { label };
133
+ }