@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 (298) hide show
  1. package/README.md +46 -30
  2. package/README.zh-CN.md +40 -25
  3. package/assets/screens/board.png +0 -0
  4. package/assets/screens/chrome.png +0 -0
  5. package/assets/screens/file-browse.png +0 -0
  6. package/assets/screens/plan-review.png +0 -0
  7. package/assets/screens/plan.png +0 -0
  8. package/docs/extensions/README.md +22 -23
  9. package/docs/extensions/board.md +397 -168
  10. package/docs/extensions/bytefly.md +234 -197
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +57 -63
  14. package/docs/extensions/file-open.md +9 -11
  15. package/docs/extensions/file-read.md +12 -14
  16. package/docs/extensions/goal.md +15 -17
  17. package/docs/extensions/job.md +54 -42
  18. package/docs/extensions/memory.md +17 -19
  19. package/docs/extensions/plan.md +197 -140
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +145 -0
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +83 -44
  26. package/docs/extensions/todo.md +18 -18
  27. package/docs/extensions/watch.md +31 -31
  28. package/docs/extensions/web-access.md +2 -4
  29. package/docs/extensions/wechat.md +34 -35
  30. package/docs/extensions/worktree.md +56 -50
  31. package/extensions/board/commands.ts +79 -25
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +17 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +5 -20
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +65 -93
  38. package/extensions/board/result.ts +30 -39
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +49 -11
  41. package/extensions/board/snapshot.ts +8 -6
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +154 -55
  44. package/extensions/board/transport/router.ts +3 -5
  45. package/extensions/board/transport/server.ts +59 -28
  46. package/extensions/board/types.ts +36 -42
  47. package/extensions/board/view/card.ts +229 -0
  48. package/extensions/board/view/client.ts +35 -16
  49. package/extensions/board/view/dialog.ts +169 -6
  50. package/extensions/board/view/empty.ts +47 -0
  51. package/extensions/board/view/keys.ts +13 -2
  52. package/extensions/board/view/live.ts +90 -28
  53. package/extensions/board/view/names.ts +13 -4
  54. package/extensions/board/view/page.ts +38 -13
  55. package/extensions/board/view/render.ts +219 -277
  56. package/extensions/board/view/styles.ts +130 -32
  57. package/extensions/board/view/team.ts +297 -76
  58. package/extensions/bytefly/block/fallback.ts +1 -1
  59. package/extensions/bytefly/doctor/runtime.ts +5 -2
  60. package/extensions/bytefly/footer/collect/label.ts +60 -0
  61. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  62. package/extensions/bytefly/footer/config.ts +7 -8
  63. package/extensions/bytefly/footer/register.ts +33 -4
  64. package/extensions/bytefly/footer/render/footer.ts +18 -4
  65. package/extensions/bytefly/footer/render/key.ts +46 -0
  66. package/extensions/bytefly/footer/render/layout.ts +13 -3
  67. package/extensions/bytefly/footer/render/lines.ts +10 -7
  68. package/extensions/bytefly/footer/render/segments.ts +41 -38
  69. package/extensions/bytefly/footer/render/status.ts +0 -11
  70. package/extensions/bytefly/footer/types.ts +7 -0
  71. package/extensions/bytefly/header/component.ts +4 -20
  72. package/extensions/bytefly/header/config.ts +1 -1
  73. package/extensions/bytefly/index.ts +32 -6
  74. package/extensions/bytefly/mode/editor.ts +115 -0
  75. package/extensions/bytefly/panel/commands.ts +210 -120
  76. package/extensions/bytefly/panel/format.ts +4 -4
  77. package/extensions/bytefly/panel/settings.ts +73 -6
  78. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  79. package/extensions/bytefly/update/cache.ts +1 -1
  80. package/extensions/bytefly/update/register.ts +4 -4
  81. package/extensions/bytefly/working/config.ts +5 -7
  82. package/extensions/bytefly/working/format.ts +10 -13
  83. package/extensions/bytefly/working/register.ts +22 -27
  84. package/extensions/check/commands.ts +3 -2
  85. package/extensions/check/render.ts +10 -1
  86. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  87. package/extensions/context/commands.ts +2 -2
  88. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  89. package/extensions/file-browse/config/config.ts +4 -19
  90. package/extensions/file-browse/index.ts +89 -79
  91. package/extensions/file-browse/live/live.ts +1 -1
  92. package/extensions/file-browse/transport/router.ts +12 -4
  93. package/extensions/file-browse/transport/server.ts +61 -28
  94. package/extensions/file-browse/view/chrome.ts +4 -4
  95. package/extensions/file-browse/view/client.ts +3 -1
  96. package/extensions/file-browse/view/html.ts +73 -40
  97. package/extensions/file-browse/view/live-client.ts +2 -2
  98. package/extensions/file-browse/view/markdown.ts +13 -14
  99. package/extensions/file-browse/view/palette.ts +2 -2
  100. package/extensions/file-browse/view/raw-html.ts +9 -9
  101. package/extensions/file-browse/view/tree.ts +1 -1
  102. package/extensions/file-browse/view/urls.ts +20 -6
  103. package/extensions/file-read/render.ts +4 -4
  104. package/extensions/goal/plan-route.ts +2 -2
  105. package/extensions/goal/tui.ts +12 -2
  106. package/extensions/job/child.ts +527 -0
  107. package/extensions/job/clock.ts +142 -0
  108. package/extensions/job/commands.ts +4 -2
  109. package/extensions/job/config.ts +8 -1
  110. package/extensions/job/format.ts +22 -1
  111. package/extensions/job/log-writer.ts +13 -2
  112. package/extensions/job/registry.ts +63 -7
  113. package/extensions/job/render.ts +34 -0
  114. package/extensions/job/runtime.ts +296 -655
  115. package/extensions/job/session.ts +132 -19
  116. package/extensions/job/store.ts +7 -7
  117. package/extensions/job/tools.ts +21 -9
  118. package/extensions/job/types.ts +47 -0
  119. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
  120. package/extensions/job/waiters.ts +44 -1
  121. package/extensions/memory/commands.ts +4 -2
  122. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  123. package/extensions/plan/commands.ts +12 -9
  124. package/extensions/plan/feedback.ts +62 -16
  125. package/extensions/plan/format.ts +8 -2
  126. package/extensions/plan/git-bash.ts +115 -16
  127. package/extensions/plan/index.ts +58 -51
  128. package/extensions/plan/plan-index.ts +4 -2
  129. package/extensions/plan/plan-text.ts +114 -0
  130. package/extensions/plan/plans.ts +5 -54
  131. package/extensions/plan/policy.ts +8 -10
  132. package/extensions/plan/review-lazy.ts +85 -0
  133. package/extensions/plan/review.ts +2 -2
  134. package/extensions/plan/runtime.ts +61 -41
  135. package/extensions/plan/sections.ts +1 -1
  136. package/extensions/plan/steps.ts +3 -3
  137. package/extensions/plan/todos.ts +1 -1
  138. package/extensions/plan/tools/cancel.ts +27 -62
  139. package/extensions/plan/tools/enter.ts +55 -81
  140. package/extensions/plan/tools/exit.ts +221 -238
  141. package/extensions/plan/tools/refusal.ts +18 -0
  142. package/extensions/plan/tools/steps.ts +75 -92
  143. package/extensions/plan/tools/write.ts +77 -120
  144. package/extensions/plan/tools.ts +188 -30
  145. package/extensions/plan/tui.ts +5 -5
  146. package/extensions/plan/types.ts +48 -5
  147. package/extensions/plan/web-blocks.ts +76 -5
  148. package/extensions/plan/web-chrome.ts +4 -0
  149. package/extensions/plan/web-client.ts +102 -2
  150. package/extensions/plan/web-collab.ts +4 -4
  151. package/extensions/plan/web-comments.ts +39 -8
  152. package/extensions/plan/web-composer.ts +14 -3
  153. package/extensions/plan/web-keys.ts +149 -25
  154. package/extensions/plan/web-lazy.ts +53 -0
  155. package/extensions/plan/web-markdown.ts +10 -68
  156. package/extensions/plan/web-outline.ts +22 -2
  157. package/extensions/plan/web-page.ts +56 -86
  158. package/extensions/plan/web-payload.ts +188 -0
  159. package/extensions/plan/web-plan.ts +23 -13
  160. package/extensions/plan/web-router.ts +18 -9
  161. package/extensions/plan/web-settle.ts +14 -7
  162. package/extensions/plan/web-share-session.ts +24 -55
  163. package/extensions/plan/web-share.ts +19 -37
  164. package/extensions/plan/web-write.ts +112 -0
  165. package/extensions/plan/web.ts +67 -131
  166. package/extensions/question/tools.ts +6 -2
  167. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  168. package/extensions/rewind/commands/commands.ts +6 -3
  169. package/extensions/rewind/state/runtime.ts +1 -1
  170. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  171. package/extensions/rewind/view/review-tui.ts +4 -4
  172. package/extensions/serve/config.ts +39 -0
  173. package/extensions/serve/index.ts +85 -0
  174. package/extensions/serve/registry.ts +260 -0
  175. package/extensions/session-bus/commands.ts +11 -6
  176. package/extensions/session-bus/join-request.ts +17 -9
  177. package/extensions/session-bus/mail-watch.ts +26 -17
  178. package/extensions/session-bus/protocol.ts +15 -3
  179. package/extensions/session-bus/resolve.ts +5 -7
  180. package/extensions/session-bus/result.ts +1 -27
  181. package/extensions/session-bus/runtime.ts +40 -12
  182. package/extensions/session-bus/store.ts +101 -19
  183. package/extensions/session-bus/tools.ts +5 -4
  184. package/extensions/session-bus/types.ts +19 -0
  185. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  186. package/extensions/subagent/agents.ts +1 -1
  187. package/extensions/subagent/commands.ts +4 -2
  188. package/extensions/subagent/format.ts +15 -15
  189. package/extensions/subagent/index.ts +1 -1
  190. package/extensions/subagent/render.ts +3 -3
  191. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  192. package/extensions/team/commands.ts +24 -5
  193. package/extensions/team/dispatch.ts +18 -4
  194. package/extensions/team/format.ts +17 -2
  195. package/extensions/team/index.ts +9 -8
  196. package/extensions/team/schema.ts +6 -5
  197. package/extensions/team/tools.ts +107 -45
  198. package/extensions/team/types.ts +3 -2
  199. package/extensions/todo/commands.ts +4 -2
  200. package/extensions/todo/format.ts +11 -0
  201. package/extensions/todo/render.ts +57 -0
  202. package/extensions/todo/runtime.ts +1 -1
  203. package/extensions/todo/tools.ts +1 -1
  204. package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
  205. package/extensions/todo/widget.ts +47 -0
  206. package/extensions/watch/commands.ts +4 -2
  207. package/extensions/watch/format.ts +36 -7
  208. package/extensions/watch/render.ts +57 -0
  209. package/extensions/watch/runtime.ts +27 -22
  210. package/extensions/watch/tools.ts +4 -4
  211. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  212. package/extensions/web-access/fetch/tool.ts +12 -4
  213. package/extensions/wechat/commands/commands.ts +18 -14
  214. package/extensions/wechat/index.ts +72 -37
  215. package/extensions/wechat/state/credentials.ts +1 -1
  216. package/extensions/wechat/state/state.ts +1 -1
  217. package/extensions/wechat/text/status.ts +112 -0
  218. package/extensions/wechat/types.ts +2 -2
  219. package/extensions/wechat/view/status-screen.ts +4 -103
  220. package/extensions/worktree/commands.ts +11 -9
  221. package/extensions/worktree/enter.ts +2 -2
  222. package/extensions/worktree/exit.ts +2 -2
  223. package/extensions/worktree/index.ts +5 -5
  224. package/extensions/worktree/screen.ts +3 -3
  225. package/extensions/worktree/status.ts +2 -2
  226. package/extensions/worktree/tools.ts +113 -116
  227. package/lib/README.md +28 -13
  228. package/lib/board/actions.ts +111 -0
  229. package/lib/board/artifact.ts +149 -0
  230. package/lib/board/card.ts +163 -52
  231. package/lib/board/config.ts +58 -0
  232. package/lib/board/contract.ts +113 -0
  233. package/lib/board/join.ts +38 -0
  234. package/lib/board/lanes.ts +182 -0
  235. package/lib/board/lead.ts +130 -0
  236. package/lib/board/limits.ts +78 -0
  237. package/lib/board/links.ts +31 -10
  238. package/lib/board/page-pointer.ts +30 -5
  239. package/lib/board/read.ts +83 -0
  240. package/lib/board/reason.ts +102 -0
  241. package/lib/board/round-text.ts +68 -6
  242. package/lib/board/round.ts +31 -15
  243. package/lib/board/rules.ts +151 -307
  244. package/lib/board/seat.ts +35 -0
  245. package/lib/board/session.ts +199 -51
  246. package/lib/board/skipped.ts +22 -0
  247. package/lib/board/steps.ts +487 -0
  248. package/lib/board/store.ts +119 -36
  249. package/lib/board/tone.ts +92 -0
  250. package/lib/board/view.ts +81 -63
  251. package/lib/bus-request.ts +83 -0
  252. package/{extensions/watch → lib}/child.ts +40 -21
  253. package/lib/env.ts +65 -7
  254. package/lib/format.ts +13 -0
  255. package/lib/http/index.ts +17 -11
  256. package/lib/http/mount.ts +142 -0
  257. package/lib/http/port.ts +30 -0
  258. package/lib/http/serve.ts +170 -46
  259. package/lib/http/server.ts +3 -3
  260. package/lib/lazy.ts +45 -0
  261. package/lib/plan-entry.ts +46 -21
  262. package/lib/plans/paths.ts +82 -0
  263. package/lib/policy.ts +9 -6
  264. package/lib/report.ts +3 -2
  265. package/lib/requirements.ts +10 -10
  266. package/lib/session-label.ts +129 -7
  267. package/lib/session-roster.ts +43 -1
  268. package/lib/session-tool.ts +39 -22
  269. package/lib/stray-fields.ts +7 -2
  270. package/lib/tool-names.ts +5 -10
  271. package/lib/tui/README.md +29 -11
  272. package/lib/tui/chrome.ts +16 -15
  273. package/lib/tui/cursor.ts +18 -3
  274. package/lib/tui/glyphs.ts +11 -11
  275. package/lib/tui/render-cache.ts +29 -0
  276. package/lib/tui/tokens.ts +33 -2
  277. package/lib/tui/tool.ts +33 -4
  278. package/lib/web/README.md +21 -0
  279. package/lib/web/client-runtime.ts +27 -1
  280. package/lib/web/components.ts +5 -1
  281. package/lib/web/index.ts +1 -0
  282. package/lib/web/markdown.ts +83 -0
  283. package/lib/web/markup.ts +20 -2
  284. package/lib/web/runtime.ts +9 -10
  285. package/package.json +8 -2
  286. package/skills/init/SKILL.md +5 -5
  287. package/skills/next/SKILL.md +3 -5
  288. package/extensions/board/config.ts +0 -36
  289. package/extensions/board/seat.ts +0 -28
  290. package/lib/tui/index.ts +0 -34
  291. package/skills/project-docs/SKILL.md +0 -133
  292. package/skills/project-docs/references/audit.md +0 -131
  293. package/skills/project-docs/references/changelog.md +0 -105
  294. package/skills/project-docs/references/discovery.md +0 -127
  295. package/skills/project-docs/references/readme.md +0 -80
  296. package/skills/project-docs/references/release.md +0 -84
  297. package/skills/project-docs/references/roadmap.md +0 -93
  298. package/skills/project-docs/scripts/state.ts +0 -316
@@ -3,9 +3,16 @@
3
3
  *
4
4
  * One store per working directory, opened on first use and kept for the session:
5
5
  * a tool call is frequent and the repository lookup shells out to git, so the
6
- * answer is remembered per `cwd`. The identity is the same pair a card records —
7
- * the session id Pi knows, and the label `session-bus` would settle by default —
8
- * which is why the board can name its owner without reading the bus.
6
+ * answer is remembered per `cwd`. A failed open is not: one bad read must not
7
+ * outlive the call that hit it, so the next call asks again. The identity is the
8
+ * same pair a card records — the session id Pi knows, and the name the team
9
+ * addresses it by — so the id is what keeps a card held across a rename, and the
10
+ * name is read from the bus record where it is.
11
+ *
12
+ * One session per *host*, not one per extension: Pi loads `board` and `team` with
13
+ * separate module caches, so two sessions would mean the seat `/board` sets is
14
+ * invisible to every write `team` makes, and one session would name its own cards
15
+ * two ways.
9
16
  *
10
17
  * The store and its cache live in this closure, not in a module variable: Pi loads
11
18
  * each extension with an isolated module cache, so module state would be one copy
@@ -14,92 +21,233 @@
14
21
 
15
22
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
23
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
17
- import { basename, join } from "node:path";
18
- import { defaultLabel, sessionNameOf } from "../session-label.ts";
19
- import { BUS_DIR_NAME } from "../session-roster.ts";
24
+ import { join } from "node:path";
25
+ import { resolveEffectiveCwd } from "../env.ts";
26
+ import { plansDirFor } from "../plans/paths.ts";
27
+ import { repoKeyFor } from "../repo-key.ts";
28
+ import {
29
+ agentLabel,
30
+ defaultLabel,
31
+ normalizeLabel,
32
+ presetNameFor,
33
+ sessionIdOf,
34
+ sessionNameOf,
35
+ } from "../session-label.ts";
36
+ import { BUS_DIR_NAME, readRoster, type RosterSession, rosterStateOf } from "../session-roster.ts";
37
+ import type { JoinResult } from "../session-tool.ts";
20
38
  import type { Actor } from "./rules.ts";
21
39
  import { type BoardStore, openBoard } from "./store.ts";
22
40
 
41
+ /**
42
+ * Where this session stands on the team, as the one place that keeps it.
43
+ *
44
+ * Two surfaces ask: `board` when it joins on the page or on a claim, and `team` for
45
+ * `/team join`. It is a standing rather than a flag because both need to *say* what
46
+ * happened — `/board status` names the label and whether mail is listened for — and
47
+ * because a failure has to be asked again while a success must not be. A failure is
48
+ * kept so a line can say why, and `ok` is what a caller reads to decide.
49
+ */
50
+ export interface TeamStanding {
51
+ /** What the last join did, or undefined when this session has not asked. */
52
+ last(): JoinResult | undefined;
53
+ /** Record what an attempt did; a failed one is kept, and asked again. */
54
+ record(result: JoinResult): void;
55
+ }
56
+
23
57
  /** What a tool, a page, and a round need from the session, without any of them owning it. */
24
58
  export interface BoardSession {
25
- /** The board for `cwd`'s repository. */
59
+ /**
60
+ * The board for `cwd`'s repository.
61
+ *
62
+ * Opened once and kept, unless the open failed: a rejection is dropped so the next call
63
+ * asks again, the rule `lib/lazy.ts` states for a load it owns.
64
+ */
26
65
  store(cwd: string): Promise<BoardStore>;
27
- /** The session a write is made as. */
28
- who(ctx: ExtensionContext, cwd: string): Actor;
66
+ /**
67
+ * The session a write is made as: its team name, and the id a card keeps.
68
+ *
69
+ * Three answers, in order: the seat while this session serves the page; else the name
70
+ * the bus already records for it, so `/team join core-claims` is what its cards and
71
+ * its notes carry and a claim does not rename it back; else a preset teammate name
72
+ * drawn from the id, or `team-agent-<id>` once every preset name is held by a live
73
+ * peer. A host that reports no id has no record to read, so it keeps the readable
74
+ * chain instead. One name on every surface is the point: the card, the bus record,
75
+ * and the roster say the same word.
76
+ *
77
+ * Read rather than remembered, because a join is not this session's event alone: the
78
+ * label can arrive through `/team join`, through the bus's own `join`, or on the record
79
+ * a `/resume` inherits, and a name cached at `session_start` would miss all three.
80
+ */
81
+ who(ctx: ExtensionContext, cwd: string): Promise<Actor>;
29
82
  /** The clock, so a test drives a deadline instead of waiting on one. */
30
83
  now(): number;
31
84
  /**
32
- * Where this repository's session bus files its records, for the page's roster.
85
+ * Where this repository's session bus files its records, for the roster.
33
86
  *
34
87
  * A path, not a dependency: the board reads those files when they are there and
35
88
  * behaves exactly as before when they are not, which is what keeps the page working
36
- * with `session-bus` unloaded.
89
+ * with `session-bus` unloaded. Three callers read it — the page's snapshot, `/team`'s
90
+ * round, and the tool's rollup, which folds the owners a bus has no record of — and all
91
+ * of them want the same directory. Resolved once per working directory, and asked again
92
+ * when the store it needs failed, because a failure is no more this cache's to keep.
37
93
  */
38
94
  busDir(cwd: string): Promise<string>;
39
- /** Whether this session has already joined the team, so a card write joins once. */
40
- joined(): boolean;
41
- /** Record that the join landed; a failed one leaves this false so the next write asks. */
42
- markJoined(): void;
95
+ /**
96
+ * This repository's plan store, where a card's plan file has to live.
97
+ *
98
+ * The board records the artifact a card is executed against, so it has to answer the
99
+ * question `plan` answers: which directory that store is. One key rule — `repoKeyFor`
100
+ * over the effective cwd — and the unkeyed branch outside a repository, so a card cannot
101
+ * name a plan the plan tool would refuse to read. Resolved once per working directory,
102
+ * with a failed resolution as unremembered as a failed store.
103
+ */
104
+ planDir(cwd: string): Promise<string>;
105
+ /** The seat this session serves the page as, or undefined when it is not serving. */
106
+ serving(): string | undefined;
107
+ /** Serve the page under `seat`, or stand it down when the page stops. */
108
+ serve(seat: string | undefined): void;
109
+ /** Where this session stands on the team, so a claim joins once and status can say. */
110
+ team: TeamStanding;
43
111
  }
44
112
 
45
113
  /** Test seams and the one place a caller may redirect the store. */
46
114
  export interface BoardSessionOptions {
47
115
  now?: () => number;
48
116
  agentDir?: string;
117
+ /**
118
+ * Test seam: the store open itself, so a suite can fail one and watch the next call ask
119
+ * again. The product supplies nothing, and `store` is the only caller.
120
+ */
121
+ open?: (cwd: string) => Promise<BoardStore>;
122
+ }
123
+
124
+ /**
125
+ * Where this repository's session bus files its records.
126
+ *
127
+ * The board's store carries the repository key and `session-bus` files under the same one
128
+ * — `join(getAgentDir(), BUS_DIR_NAME, repo.key)` in `session-bus/runtime.ts` — so the two
129
+ * name one directory from one fact. A key read back off the board's own path would make
130
+ * the board's directory layout load-bearing for the roster, and regrouping those files
131
+ * would leave every peer reading as no record at all. It takes the key and nothing else,
132
+ * so there is no directory here to read one off.
133
+ */
134
+ export function busDirFor(agentDir: string, store: Pick<BoardStore, "key">): string {
135
+ return join(agentDir, BUS_DIR_NAME, store.key);
136
+ }
137
+
138
+ /**
139
+ * The labels live peers other than `id` hold, lowercased: the names a draw must skip.
140
+ *
141
+ * The bus's own rule, not a stricter one: a quiet or crashed record's label is free at
142
+ * join, so it is free to draw too, and a repository's history cannot use the pool up.
143
+ */
144
+ function liveLabels(roster: readonly RosterSession[] | undefined, id: string, now: number): Set<string> {
145
+ const held = new Set<string>();
146
+ for (const record of roster ?? []) {
147
+ if (record.id !== id && rosterStateOf(record, now) === "live") held.add(record.label.toLowerCase());
148
+ }
149
+ return held;
150
+ }
151
+
152
+ /**
153
+ * The key one host's session is kept under.
154
+ *
155
+ * `Symbol.for` is the global registry, so the copy of this module in `board`'s cache
156
+ * and the copy in `team`'s agree on the key — a plain `Symbol()` would be two keys on
157
+ * one host, which is the drift this reuse exists to stop.
158
+ */
159
+ const SESSION_KEY = Symbol.for("@gavin-hu/my-pi-agent/board-session");
160
+
161
+ /** A host with this module's key on it. */
162
+ type BoardHost = ExtensionAPI & { [SESSION_KEY]?: BoardSession };
163
+
164
+ /**
165
+ * One load per key, remembered once it has settled — a rejection is not.
166
+ *
167
+ * A keyed cache is what the three stores below need: two callers on one directory share one
168
+ * promise, so the second call neither opens a second store nor shells out to git again. What
169
+ * must not be shared is a failure — a config read or the agent directory can fail once and
170
+ * answer the next time — so the entry is dropped in its rejection handler, and `lib/lazy.ts`
171
+ * is the same rule for a load with no key.
172
+ */
173
+ function rememberLoad<T>(cache: Map<string, Promise<T>>, key: string, load: () => Promise<T>): Promise<T> {
174
+ const cached = cache.get(key);
175
+ if (cached !== undefined) return cached;
176
+ const pending = Promise.resolve()
177
+ .then(load)
178
+ .catch((error: unknown) => {
179
+ // The entry is this attempt's: a key is only written when absent, so dropping it
180
+ // cannot drop a newer attempt.
181
+ cache.delete(key);
182
+ throw error;
183
+ });
184
+ cache.set(key, pending);
185
+ return pending;
49
186
  }
50
187
 
51
188
  /**
52
- * Build the session around one extension's `pi`.
189
+ * The session around one host, built on the first call and shared after it.
53
190
  *
54
- * Shared by `board` and `team`: both work on the same store and must name a write
55
- * as the same session, and two copies are how a card's owner label drifts from the
56
- * label the board writes.
191
+ * `board` and `team` both work on the same store and must name a write as the same
192
+ * session, so the first caller's options are the ones built: a second extension joins
193
+ * the session that exists rather than opening a second one beside it.
57
194
  */
58
195
  export function openBoardSession(pi: ExtensionAPI, options: BoardSessionOptions = {}): BoardSession {
196
+ const host = pi as BoardHost;
197
+ const shared = host[SESSION_KEY];
198
+ if (shared !== undefined) return shared;
59
199
  const stores = new Map<string, Promise<BoardStore>>();
60
200
  const busDirs = new Map<string, Promise<string>>();
61
- let teamJoined = false;
201
+ const planDirs = new Map<string, Promise<string>>();
202
+ let seat: string | undefined;
203
+ let standing: JoinResult | undefined;
62
204
  const agentDir = options.agentDir;
205
+ /** The real open, unless a suite supplied one: the store a call works on. */
206
+ const open = options.open ?? ((cwd: string) => openBoard(pi, cwd, agentDir === undefined ? {} : { agentDir }));
63
207
  const session: BoardSession = {
64
208
  store(cwd) {
65
- const cached = stores.get(cwd);
66
- if (cached) return cached;
67
- const opened = openBoard(pi, cwd, agentDir === undefined ? {} : { agentDir });
68
- stores.set(cwd, opened);
69
- return opened;
209
+ return rememberLoad(stores, cwd, () => open(cwd));
70
210
  },
71
- who(ctx, cwd) {
211
+ async who(ctx, cwd) {
72
212
  const id = sessionIdOf(ctx);
73
- const label = defaultLabel(sessionNameOf(ctx), cwd, id ?? "session");
74
- return id === undefined ? { label } : { id, label };
213
+ // The seat is a role rather than a session, so it outranks the record: the page a
214
+ // session serves is the name its cards are read against. A host that reports no id
215
+ // has no record to look for, so it keeps the bus's own readable chain.
216
+ if (seat !== undefined) return id === undefined ? { label: seat } : { id, label: seat };
217
+ if (id === undefined) return { label: defaultLabel(sessionNameOf(ctx), cwd) };
218
+ // The record is the bus's word for this session, so it is read rather than derived;
219
+ // a record that names nothing is no record, and draws like one.
220
+ const roster = await readRoster(await session.busDir(cwd));
221
+ const held = normalizeLabel(roster?.find((entry) => entry.id === id)?.label, "");
222
+ if (held !== "") return { id, label: held };
223
+ // No record, so the team gives this session a preset name: the one its id draws,
224
+ // walked past the names a live peer holds. The fallback keeps a unique name for the
225
+ // day the whole list is taken, so two sessions cannot settle on one word.
226
+ const drawn = presetNameFor(id, liveLabels(roster, id, session.now()));
227
+ return { id, label: drawn ?? agentLabel(id) };
75
228
  },
76
229
  now: options.now ?? Date.now,
77
- // The board's own directory is `<agentDir>/board/<key>` and the bus's is
78
- // `<agentDir>/session-bus/<key>`, both keyed by the same repository, so the key is
79
- // read back off the board rather than resolved a second time through git.
80
230
  busDir(cwd) {
81
- const cached = busDirs.get(cwd);
82
- if (cached) return cached;
83
- const resolved = session
84
- .store(cwd)
85
- .then((store) => join(agentDir ?? getAgentDir(), BUS_DIR_NAME, basename(store.dir)));
86
- busDirs.set(cwd, resolved);
87
- return resolved;
231
+ return rememberLoad(busDirs, cwd, () =>
232
+ session.store(cwd).then((store) => busDirFor(agentDir ?? getAgentDir(), store)),
233
+ );
88
234
  },
89
- joined: () => teamJoined,
90
- markJoined: () => {
91
- teamJoined = true;
235
+ planDir(cwd) {
236
+ return rememberLoad(planDirs, cwd, () =>
237
+ repoKeyFor(pi, resolveEffectiveCwd(cwd)).then((repo) => plansDirFor(agentDir ?? getAgentDir(), repo)),
238
+ );
239
+ },
240
+ serving: () => seat,
241
+ serve: (label) => {
242
+ seat = label;
243
+ },
244
+ team: {
245
+ last: () => standing,
246
+ record: (result) => {
247
+ standing = result;
248
+ },
92
249
  },
93
250
  };
251
+ host[SESSION_KEY] = session;
94
252
  return session;
95
253
  }
96
-
97
- /** The session id, or undefined when the host does not report one. */
98
- function sessionIdOf(ctx: ExtensionContext): string | undefined {
99
- try {
100
- const id = ctx.sessionManager.getSessionId();
101
- return typeof id === "string" && id !== "" ? id : undefined;
102
- } catch {
103
- return undefined;
104
- }
105
- }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The sentence for the files a board could not read.
3
+ *
4
+ * A skipped file is not an absent one: a board written by a newer build, or a record this
5
+ * one cannot trust, would otherwise read as "nobody has planned anything" while the work
6
+ * sits on disk. The tool prints this under a listing, and the page prints the same sentence
7
+ * on the footer's hover title — one body, so the two cannot name different files for the
8
+ * same board.
9
+ *
10
+ * Value-only: no I/O, no clock, no store. `Skip` is the store's own record of a refusal,
11
+ * and it arrives already phrased for a reader (`bad JSON`, `version 3`).
12
+ */
13
+
14
+ import { plural } from "./round-text.ts";
15
+ import type { Skip } from "./store.ts";
16
+
17
+ /** The files this build refused, named with the reason, or nothing when it read them all. */
18
+ export function skippedText(skipped: readonly Skip[]): string {
19
+ if (skipped.length === 0) return "";
20
+ const files = skipped.map((entry) => `${entry.file} (${entry.why})`).join(", ");
21
+ return `${plural(skipped.length, "file")} this build cannot read: ${files}`;
22
+ }