@gavin-hu/my-pi-agent 0.21.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 (254) hide show
  1. package/README.md +46 -31
  2. package/README.zh-CN.md +40 -26
  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 -24
  9. package/docs/extensions/board.md +361 -147
  10. package/docs/extensions/bytefly.md +152 -111
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +30 -31
  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 +166 -126
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +34 -18
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +72 -33
  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 +56 -15
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +5 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +4 -40
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +13 -11
  38. package/extensions/board/result.ts +30 -16
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +42 -10
  41. package/extensions/board/snapshot.ts +2 -1
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +131 -44
  44. package/extensions/board/types.ts +36 -32
  45. package/extensions/board/view/card.ts +229 -0
  46. package/extensions/board/view/client.ts +33 -15
  47. package/extensions/board/view/dialog.ts +169 -6
  48. package/extensions/board/view/empty.ts +47 -0
  49. package/extensions/board/view/keys.ts +13 -2
  50. package/extensions/board/view/live.ts +89 -21
  51. package/extensions/board/view/names.ts +13 -4
  52. package/extensions/board/view/page.ts +23 -9
  53. package/extensions/board/view/render.ts +208 -271
  54. package/extensions/board/view/styles.ts +123 -31
  55. package/extensions/board/view/team.ts +297 -76
  56. package/extensions/bytefly/block/fallback.ts +1 -1
  57. package/extensions/bytefly/doctor/runtime.ts +5 -2
  58. package/extensions/bytefly/footer/collect/label.ts +60 -0
  59. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  60. package/extensions/bytefly/footer/config.ts +4 -2
  61. package/extensions/bytefly/footer/register.ts +33 -4
  62. package/extensions/bytefly/footer/render/lines.ts +7 -4
  63. package/extensions/bytefly/footer/render/segments.ts +37 -10
  64. package/extensions/bytefly/footer/render/status.ts +0 -11
  65. package/extensions/bytefly/footer/types.ts +7 -0
  66. package/extensions/bytefly/header/config.ts +1 -1
  67. package/extensions/bytefly/index.ts +32 -6
  68. package/extensions/bytefly/mode/editor.ts +115 -0
  69. package/extensions/bytefly/panel/commands.ts +210 -120
  70. package/extensions/bytefly/panel/format.ts +4 -4
  71. package/extensions/bytefly/panel/settings.ts +73 -6
  72. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  73. package/extensions/bytefly/update/cache.ts +1 -1
  74. package/extensions/bytefly/update/register.ts +4 -4
  75. package/extensions/bytefly/working/config.ts +5 -7
  76. package/extensions/bytefly/working/format.ts +10 -13
  77. package/extensions/bytefly/working/register.ts +22 -27
  78. package/extensions/check/commands.ts +3 -2
  79. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  80. package/extensions/context/commands.ts +2 -2
  81. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  82. package/extensions/file-browse/index.ts +67 -36
  83. package/extensions/file-read/render.ts +4 -4
  84. package/extensions/goal/plan-route.ts +2 -2
  85. package/extensions/job/child.ts +527 -0
  86. package/extensions/job/clock.ts +142 -0
  87. package/extensions/job/commands.ts +4 -2
  88. package/extensions/job/config.ts +8 -1
  89. package/extensions/job/format.ts +22 -1
  90. package/extensions/job/log-writer.ts +13 -2
  91. package/extensions/job/registry.ts +63 -7
  92. package/extensions/job/render.ts +34 -0
  93. package/extensions/job/runtime.ts +296 -655
  94. package/extensions/job/session.ts +132 -19
  95. package/extensions/job/store.ts +7 -7
  96. package/extensions/job/tools.ts +3 -3
  97. package/extensions/job/types.ts +47 -0
  98. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
  99. package/extensions/job/waiters.ts +44 -1
  100. package/extensions/memory/commands.ts +4 -2
  101. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  102. package/extensions/plan/commands.ts +12 -9
  103. package/extensions/plan/format.ts +8 -2
  104. package/extensions/plan/git-bash.ts +115 -16
  105. package/extensions/plan/index.ts +53 -52
  106. package/extensions/plan/plan-index.ts +4 -2
  107. package/extensions/plan/plans.ts +5 -54
  108. package/extensions/plan/policy.ts +8 -10
  109. package/extensions/plan/review-lazy.ts +85 -0
  110. package/extensions/plan/review.ts +2 -2
  111. package/extensions/plan/runtime.ts +49 -39
  112. package/extensions/plan/sections.ts +1 -1
  113. package/extensions/plan/steps.ts +3 -3
  114. package/extensions/plan/todos.ts +1 -1
  115. package/extensions/plan/tools/cancel.ts +27 -62
  116. package/extensions/plan/tools/enter.ts +55 -81
  117. package/extensions/plan/tools/exit.ts +221 -238
  118. package/extensions/plan/tools/refusal.ts +18 -0
  119. package/extensions/plan/tools/steps.ts +75 -92
  120. package/extensions/plan/tools/write.ts +77 -120
  121. package/extensions/plan/tools.ts +188 -30
  122. package/extensions/plan/tui.ts +5 -5
  123. package/extensions/plan/types.ts +48 -5
  124. package/extensions/plan/web-client.ts +3 -0
  125. package/extensions/plan/web-lazy.ts +53 -0
  126. package/extensions/plan/web-markdown.ts +9 -67
  127. package/extensions/plan/web-payload.ts +3 -3
  128. package/extensions/plan/web-router.ts +18 -9
  129. package/extensions/plan/web.ts +1 -12
  130. package/extensions/question/tools.ts +6 -2
  131. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  132. package/extensions/rewind/commands/commands.ts +6 -3
  133. package/extensions/rewind/state/runtime.ts +1 -1
  134. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  135. package/extensions/rewind/view/review-tui.ts +4 -4
  136. package/extensions/serve/config.ts +10 -8
  137. package/extensions/serve/index.ts +17 -3
  138. package/extensions/serve/registry.ts +26 -17
  139. package/extensions/session-bus/commands.ts +11 -6
  140. package/extensions/session-bus/join-request.ts +2 -1
  141. package/extensions/session-bus/mail-watch.ts +26 -17
  142. package/extensions/session-bus/protocol.ts +15 -3
  143. package/extensions/session-bus/resolve.ts +5 -7
  144. package/extensions/session-bus/runtime.ts +40 -12
  145. package/extensions/session-bus/store.ts +101 -19
  146. package/extensions/session-bus/types.ts +19 -0
  147. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  148. package/extensions/subagent/agents.ts +1 -1
  149. package/extensions/subagent/commands.ts +4 -2
  150. package/extensions/subagent/format.ts +15 -15
  151. package/extensions/subagent/index.ts +1 -1
  152. package/extensions/subagent/render.ts +3 -3
  153. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  154. package/extensions/team/commands.ts +24 -7
  155. package/extensions/team/dispatch.ts +18 -4
  156. package/extensions/team/format.ts +17 -2
  157. package/extensions/team/index.ts +9 -8
  158. package/extensions/team/schema.ts +6 -5
  159. package/extensions/team/tools.ts +107 -45
  160. package/extensions/team/types.ts +3 -2
  161. package/extensions/todo/commands.ts +4 -2
  162. package/extensions/todo/format.ts +11 -0
  163. package/extensions/todo/render.ts +57 -0
  164. package/extensions/todo/runtime.ts +1 -1
  165. package/extensions/todo/tools.ts +1 -1
  166. package/extensions/todo/view/todo-screen.ts +122 -0
  167. package/extensions/todo/widget.ts +47 -0
  168. package/extensions/watch/commands.ts +4 -2
  169. package/extensions/watch/format.ts +36 -7
  170. package/extensions/watch/render.ts +57 -0
  171. package/extensions/watch/runtime.ts +27 -22
  172. package/extensions/watch/tools.ts +4 -4
  173. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  174. package/extensions/web-access/fetch/tool.ts +12 -4
  175. package/extensions/wechat/commands/commands.ts +18 -14
  176. package/extensions/wechat/index.ts +72 -37
  177. package/extensions/wechat/state/credentials.ts +1 -1
  178. package/extensions/wechat/state/state.ts +1 -1
  179. package/extensions/wechat/text/status.ts +112 -0
  180. package/extensions/wechat/types.ts +2 -2
  181. package/extensions/wechat/view/status-screen.ts +4 -103
  182. package/extensions/worktree/commands.ts +11 -9
  183. package/extensions/worktree/enter.ts +2 -2
  184. package/extensions/worktree/exit.ts +2 -2
  185. package/extensions/worktree/index.ts +5 -5
  186. package/extensions/worktree/screen.ts +3 -3
  187. package/extensions/worktree/status.ts +2 -2
  188. package/extensions/worktree/tools.ts +113 -116
  189. package/lib/README.md +27 -13
  190. package/lib/board/actions.ts +111 -0
  191. package/lib/board/artifact.ts +149 -0
  192. package/lib/board/card.ts +163 -52
  193. package/lib/board/config.ts +58 -0
  194. package/lib/board/contract.ts +113 -0
  195. package/lib/board/join.ts +38 -0
  196. package/lib/board/lanes.ts +182 -0
  197. package/lib/board/lead.ts +130 -0
  198. package/lib/board/limits.ts +78 -0
  199. package/lib/board/links.ts +31 -10
  200. package/lib/board/page-pointer.ts +1 -1
  201. package/lib/board/read.ts +83 -0
  202. package/lib/board/reason.ts +102 -0
  203. package/lib/board/round-text.ts +68 -6
  204. package/lib/board/round.ts +31 -15
  205. package/lib/board/rules.ts +151 -307
  206. package/lib/board/seat.ts +35 -0
  207. package/lib/board/session.ts +136 -54
  208. package/lib/board/skipped.ts +22 -0
  209. package/lib/board/steps.ts +487 -0
  210. package/lib/board/store.ts +87 -30
  211. package/lib/board/tone.ts +92 -0
  212. package/lib/board/view.ts +70 -58
  213. package/{extensions/watch → lib}/child.ts +40 -21
  214. package/lib/env.ts +65 -7
  215. package/lib/format.ts +13 -0
  216. package/lib/http/index.ts +14 -12
  217. package/lib/http/port.ts +15 -10
  218. package/lib/lazy.ts +45 -0
  219. package/lib/plan-entry.ts +39 -10
  220. package/lib/plans/paths.ts +82 -0
  221. package/lib/policy.ts +9 -6
  222. package/lib/report.ts +3 -2
  223. package/lib/requirements.ts +8 -8
  224. package/lib/session-label.ts +111 -5
  225. package/lib/session-roster.ts +43 -1
  226. package/lib/session-tool.ts +5 -4
  227. package/lib/stray-fields.ts +7 -2
  228. package/lib/tool-names.ts +5 -10
  229. package/lib/tui/README.md +23 -8
  230. package/lib/tui/chrome.ts +16 -15
  231. package/lib/tui/cursor.ts +18 -3
  232. package/lib/tui/glyphs.ts +9 -7
  233. package/lib/tui/tokens.ts +33 -2
  234. package/lib/tui/tool.ts +3 -3
  235. package/lib/web/README.md +21 -0
  236. package/lib/web/components.ts +5 -1
  237. package/lib/web/index.ts +1 -0
  238. package/lib/web/markdown.ts +83 -0
  239. package/lib/web/markup.ts +2 -1
  240. package/lib/web/runtime.ts +9 -10
  241. package/package.json +6 -1
  242. package/skills/init/SKILL.md +5 -5
  243. package/skills/next/SKILL.md +3 -5
  244. package/extensions/board/seat.ts +0 -28
  245. package/extensions/todo/tui.ts +0 -212
  246. package/lib/tui/index.ts +0 -34
  247. package/skills/project-docs/SKILL.md +0 -133
  248. package/skills/project-docs/references/audit.md +0 -131
  249. package/skills/project-docs/references/changelog.md +0 -105
  250. package/skills/project-docs/references/discovery.md +0 -127
  251. package/skills/project-docs/references/readme.md +0 -80
  252. package/skills/project-docs/references/release.md +0 -84
  253. package/skills/project-docs/references/roadmap.md +0 -93
  254. package/skills/project-docs/scripts/state.ts +0 -316
@@ -3,9 +3,11 @@
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 — the
7
- * session id Pi knows, and the team name derived from it — which is why the board can
8
- * name its owner without reading the bus, and why the name survives a rename.
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.
9
11
  *
10
12
  * One session per *host*, not one per extension: Pi loads `board` and `team` with
11
13
  * separate module caches, so two sessions would mean the seat `/board` sets is
@@ -19,9 +21,19 @@
19
21
 
20
22
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
21
23
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
22
- import { basename, join } from "node:path";
23
- import { agentLabel, defaultLabel, sessionNameOf } from "../session-label.ts";
24
- 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";
25
37
  import type { JoinResult } from "../session-tool.ts";
26
38
  import type { Actor } from "./rules.ts";
27
39
  import { type BoardStore, openBoard } from "./store.ts";
@@ -44,26 +56,52 @@ export interface TeamStanding {
44
56
 
45
57
  /** What a tool, a page, and a round need from the session, without any of them owning it. */
46
58
  export interface BoardSession {
47
- /** 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
+ */
48
65
  store(cwd: string): Promise<BoardStore>;
49
66
  /**
50
67
  * The session a write is made as: its team name, and the id a card keeps.
51
68
  *
52
- * The label is the seat while this session serves the page, else `team-agent-<id>`,
53
- * else — on a host that reports no id — the readable chain. One name on every
54
- * surface is the point: the card, the bus record, and the roster say the same word.
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.
55
80
  */
56
- who(ctx: ExtensionContext, cwd: string): Actor;
81
+ who(ctx: ExtensionContext, cwd: string): Promise<Actor>;
57
82
  /** The clock, so a test drives a deadline instead of waiting on one. */
58
83
  now(): number;
59
84
  /**
60
- * 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.
61
86
  *
62
87
  * A path, not a dependency: the board reads those files when they are there and
63
88
  * behaves exactly as before when they are not, which is what keeps the page working
64
- * 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.
65
93
  */
66
94
  busDir(cwd: string): Promise<string>;
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>;
67
105
  /** The seat this session serves the page as, or undefined when it is not serving. */
68
106
  serving(): string | undefined;
69
107
  /** Serve the page under `seat`, or stand it down when the page stops. */
@@ -72,19 +110,43 @@ export interface BoardSession {
72
110
  team: TeamStanding;
73
111
  }
74
112
 
75
- /**
76
- * The part of a session a command needs: what it is called, and where it stands.
77
- *
78
- * `/board` joins, sets the seat, and reports the standing; it never opens a store or
79
- * reads the bus, so a command takes this much and no more. A test drives it with four
80
- * stand-ins instead of a whole session with a temp agent directory.
81
- */
82
- export type BoardTeam = Pick<BoardSession, "who" | "serving" | "serve" | "team">;
83
-
84
113
  /** Test seams and the one place a caller may redirect the store. */
85
114
  export interface BoardSessionOptions {
86
115
  now?: () => number;
87
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;
88
150
  }
89
151
 
90
152
  /**
@@ -99,6 +161,30 @@ const SESSION_KEY = Symbol.for("@gavin-hu/my-pi-agent/board-session");
99
161
  /** A host with this module's key on it. */
100
162
  type BoardHost = ExtensionAPI & { [SESSION_KEY]?: BoardSession };
101
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;
186
+ }
187
+
102
188
  /**
103
189
  * The session around one host, built on the first call and shared after it.
104
190
  *
@@ -112,38 +198,44 @@ export function openBoardSession(pi: ExtensionAPI, options: BoardSessionOptions
112
198
  if (shared !== undefined) return shared;
113
199
  const stores = new Map<string, Promise<BoardStore>>();
114
200
  const busDirs = new Map<string, Promise<string>>();
201
+ const planDirs = new Map<string, Promise<string>>();
115
202
  let seat: string | undefined;
116
203
  let standing: JoinResult | undefined;
117
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 }));
118
207
  const session: BoardSession = {
119
208
  store(cwd) {
120
- const cached = stores.get(cwd);
121
- if (cached) return cached;
122
- const opened = openBoard(pi, cwd, agentDir === undefined ? {} : { agentDir });
123
- stores.set(cwd, opened);
124
- return opened;
209
+ return rememberLoad(stores, cwd, () => open(cwd));
125
210
  },
126
- who(ctx, cwd) {
211
+ async who(ctx, cwd) {
127
212
  const id = sessionIdOf(ctx);
128
- // The seat is a role and the agent name is derived from the id, so neither moves
129
- // when a user renames the session — which is what keeps a card's owner readable
130
- // against the roster a year later. A host that reports no id has nothing to derive
131
- // from, so it keeps the bus's own readable chain.
132
- const label = seat ?? (id === undefined ? defaultLabel(sessionNameOf(ctx), cwd) : agentLabel(id));
133
- 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) };
134
228
  },
135
229
  now: options.now ?? Date.now,
136
- // The board's own directory is `<agentDir>/board/<key>` and the bus's is
137
- // `<agentDir>/session-bus/<key>`, both keyed by the same repository, so the key is
138
- // read back off the board rather than resolved a second time through git.
139
230
  busDir(cwd) {
140
- const cached = busDirs.get(cwd);
141
- if (cached) return cached;
142
- const resolved = session
143
- .store(cwd)
144
- .then((store) => join(agentDir ?? getAgentDir(), BUS_DIR_NAME, basename(store.dir)));
145
- busDirs.set(cwd, resolved);
146
- return resolved;
231
+ return rememberLoad(busDirs, cwd, () =>
232
+ session.store(cwd).then((store) => busDirFor(agentDir ?? getAgentDir(), store)),
233
+ );
234
+ },
235
+ planDir(cwd) {
236
+ return rememberLoad(planDirs, cwd, () =>
237
+ repoKeyFor(pi, resolveEffectiveCwd(cwd)).then((repo) => plansDirFor(agentDir ?? getAgentDir(), repo)),
238
+ );
147
239
  },
148
240
  serving: () => seat,
149
241
  serve: (label) => {
@@ -159,13 +251,3 @@ export function openBoardSession(pi: ExtensionAPI, options: BoardSessionOptions
159
251
  host[SESSION_KEY] = session;
160
252
  return session;
161
253
  }
162
-
163
- /** The session id, or undefined when the host does not report one. */
164
- function sessionIdOf(ctx: ExtensionContext): string | undefined {
165
- try {
166
- const id = ctx.sessionManager.getSessionId();
167
- return typeof id === "string" && id !== "" ? id : undefined;
168
- } catch {
169
- return undefined;
170
- }
171
- }
@@ -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
+ }