@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
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The board's page, loaded when `/board` first needs it.
3
+ *
4
+ * The page module owns the HTTP surface, the snapshots, and the view; none of it
5
+ * is needed to register the command or the tool. The factory therefore returns a
6
+ * proxy over `lib/lazy`: every command path loads the real page once, and
7
+ * `closeAll` on shutdown only closes a page that was already opened, so a
8
+ * session that never touched the board imports none of it.
9
+ *
10
+ * The loader is a seam: a test drives the proxy without a socket.
11
+ */
12
+
13
+ import type { BoardSession } from "../../lib/board/session.ts";
14
+ import { lazy } from "../../lib/lazy.ts";
15
+ import type { BoardPage, BoardPageDeps } from "./page.ts";
16
+
17
+ /** The page module, as the proxy loads it. */
18
+ export type BoardPageModule = typeof import("./page.ts");
19
+
20
+ /** How the proxy reaches the page module; production uses the dynamic import. */
21
+ export type BoardPageLoader = () => Promise<BoardPageModule>;
22
+
23
+ export function createLazyBoardPage(
24
+ session: BoardSession,
25
+ deps: BoardPageDeps,
26
+ load: BoardPageLoader = () => import("./page.ts"),
27
+ ): BoardPage {
28
+ const page = lazy(async () => (await load()).createBoardPage(session, deps));
29
+ return {
30
+ start: async (ctx, view) => (await page.get()).start(ctx, view),
31
+ stop: async () => (await page.get()).stop(),
32
+ status: async () => (await page.get()).status(),
33
+ dropStale: async (ctx) => (await page.get()).dropStale(ctx),
34
+ // Shutdown must not start a load: a page that never opened has nothing to close.
35
+ closeAll: async () => {
36
+ const opened = page.peek();
37
+ if (opened !== undefined) await opened.closeAll();
38
+ },
39
+ };
40
+ }
@@ -33,7 +33,8 @@ import { mintToken, type MountSource } from "../../lib/http/index.ts";
33
33
  import { messageOf } from "../../lib/errors.ts";
34
34
  import type { IdleReason } from "../../lib/http/sink.ts";
35
35
  import { join } from "node:path";
36
- import { pageActor } from "./seat.ts";
36
+ import type { Actor } from "../../lib/board/rules.ts";
37
+ import { pageActor } from "../../lib/board/seat.ts";
37
38
  import { viewOf } from "./snapshot.ts";
38
39
  import { type BoardServer, createBoardServer } from "./transport/server.ts";
39
40
  import { BOARD_CLIENT_JS } from "./view/client.ts";
@@ -120,13 +121,9 @@ export interface BoardPage {
120
121
  }
121
122
 
122
123
  export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps): BoardPage {
123
- let running: { server: BoardServer; store: BoardStore; token: string } | undefined;
124
+ let running: { server: BoardServer; store: BoardStore; token: string; seat: Actor } | undefined;
124
125
  /** The page another session serves, when this session opened that one instead. */
125
126
  let attachedTo: PagePointer | undefined;
126
- // The page is a seat, not a session: one name and one id for its whole life, so a card
127
- // a browser claimed is still the seat's after the session that served it ends.
128
- const seat = pageActor();
129
-
130
127
  const storeOf = async (ctx: ExtensionContext): Promise<BoardStore> => runtime.store(ctx.cwd);
131
128
 
132
129
  /**
@@ -174,6 +171,11 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps): Boa
174
171
  } catch (error) {
175
172
  return failed(error);
176
173
  }
174
+ // The page is a seat, not a session: one name and one id for its whole life, so a card a
175
+ // browser claimed is still the seat's after the session that served it ends. That name is
176
+ // the board's lead, so serving the page is what makes a session the lead — a repository
177
+ // that names one gets a page served under that name, and the gate accepts it.
178
+ const seat = pageActor(store.config().lead);
177
179
  const open = (url: string) => (deps.open ?? openInBrowser)(view === "board" ? url : `${url}&view=team`);
178
180
  /** Watch the page another session serves: its note is the URL, its seat the name. */
179
181
  const attach = (pointer: PagePointer): PageStart => {
@@ -218,7 +220,7 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps): Boa
218
220
  idleMs: deps.idleMs ?? IDLE_STOP_MS,
219
221
  firstIdleMs: deps.firstIdleMs ?? IDLE_FIRST_MS,
220
222
  onIdle: (reason) => void stopIdle(ctx, reason),
221
- page: async (view) => renderBoardPage({ view, base: BOARD_BASE }),
223
+ page: async (view) => renderBoardPage({ view, base: BOARD_BASE, repo: store.name(), root: store.root() }),
222
224
  script: BOARD_CLIENT_JS,
223
225
  view: () =>
224
226
  viewOf({
@@ -269,7 +271,7 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps): Boa
269
271
  ctx.ui.notify(`board: the page did not start (${reason})`, "error");
270
272
  return { kind: "failed", reason };
271
273
  }
272
- running = { server, store, token };
274
+ running = { server, store, token, seat };
273
275
  open(server.url);
274
276
  return describe(running);
275
277
  } catch (error) {
@@ -288,9 +290,9 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps): Boa
288
290
  );
289
291
  };
290
292
 
291
- const describe = (state: { server: BoardServer; store: BoardStore }): PageStart => ({
293
+ const describe = (state: { server: BoardServer; store: BoardStore; seat: Actor }): PageStart => ({
292
294
  kind: "served",
293
- seat: seat.label,
295
+ seat: state.seat.label,
294
296
  port: state.server.port,
295
297
  url: state.server.url,
296
298
  repo: state.store.name(),
@@ -309,7 +311,7 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps): Boa
309
311
  if (running !== undefined) {
310
312
  return {
311
313
  running: true,
312
- seat: seat.label,
314
+ seat: running.seat.label,
313
315
  port: running.server.port,
314
316
  url: running.server.url,
315
317
  repo: running.store.name(),
@@ -9,17 +9,19 @@
9
9
 
10
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
11
11
  import { type Card, cardRef } from "../../lib/board/card.ts";
12
+ import { LANE_RULES } from "../../lib/board/lanes.ts";
12
13
  import { sanitizeLine } from "../../lib/format.ts";
13
14
  import { SEPARATORS } from "../../lib/tui/glyphs.ts";
14
- import type { TailResultInput, TailSection } from "../../lib/tui/tool.ts";
15
+ import type { TailResultInput, TailSection, TailRowLine } from "../../lib/tui/tool.ts";
15
16
  import { plural } from "../../lib/board/round-text.ts";
16
17
  import { paint } from "../../lib/tui/tokens.ts";
17
- import { boardRowLine } from "./rows.ts";
18
+ import { boardRowLineFor } from "./rows.ts";
19
+ import { TONE_ROLE } from "./tone.ts";
18
20
  import type { BoardDetails } from "./types.ts";
19
21
 
20
- /** One section of the expanded block. */
21
- function section(label: string | undefined, cards: readonly Card[]): TailSection<Card> {
22
- return { label, rows: cards, more: 0, rowLine: boardRowLine };
22
+ /** One section of the expanded block, drawn as the session that asked for it. */
23
+ function section(label: string | undefined, cards: readonly Card[], rowLine: TailRowLine<Card>): TailSection<Card> {
24
+ return { label, rows: cards, more: 0, rowLine };
23
25
  }
24
26
 
25
27
  /**
@@ -32,19 +34,25 @@ function section(label: string | undefined, cards: readonly Card[]): TailSection
32
34
  */
33
35
  export function resultInput(details: BoardDetails): TailResultInput<Card> | undefined {
34
36
  if (details.action !== "list") return undefined;
35
- if (details.card) return { kind: "list", rows: [details.card], more: 0, rowLine: boardRowLine };
37
+ // Only a read narrowed to this session's own cards marks them, and it marks them with
38
+ // the rule the filter used: the rows and the filter agree about what "mine" means.
39
+ const rowLine = boardRowLineFor(details.mine === true ? details.me : undefined);
40
+ if (details.card) return { kind: "list", rows: [details.card], more: 0, rowLine };
36
41
  const round = details.round;
37
42
  if (!round) {
38
43
  const cards = details.cards ?? [];
39
- return cards.length === 0 ? undefined : { kind: "list", rows: cards, more: 0, rowLine: boardRowLine };
44
+ return cards.length === 0 ? undefined : { kind: "list", rows: cards, more: 0, rowLine };
40
45
  }
41
46
  const sections: TailSection<Card>[] = [];
42
- for (const group of round.owners) sections.push(section(group.label, group.cards));
43
- if (round.unassigned.length > 0) sections.push(section("Unassigned", round.unassigned));
44
- if (round.unplanned.length > 0) sections.push(section("Unplanned", round.unplanned));
45
- if (round.blocked.length > 0) sections.push(section("Blocked", round.blocked));
46
- if (round.idle.length > 0) sections.push(section("Idle", round.idle));
47
- if (round.done.length > 0) sections.push(section("Done", round.done));
47
+ for (const group of round.owners) sections.push(section(group.label, group.cards, rowLine));
48
+ if (round.unassigned.length > 0) sections.push(section("Unassigned", round.unassigned, rowLine));
49
+ if (round.unplanned.length > 0) sections.push(section("Unplanned", round.unplanned, rowLine));
50
+ // The lead's queue: a contract nobody has signed is a card the flow stops on, so it is
51
+ // drawn in the order `formatRound` prints it rather than left to the text above.
52
+ if (round.unapproved.length > 0) sections.push(section("Awaiting approval", round.unapproved, rowLine));
53
+ if (round.blocked.length > 0) sections.push(section("Blocked", round.blocked, rowLine));
54
+ if (round.idle.length > 0) sections.push(section("Idle", round.idle, rowLine));
55
+ if (round.done.length > 0) sections.push(section("Done", round.done, rowLine));
48
56
  return sections.length === 0 ? undefined : { kind: "sections", sections };
49
57
  }
50
58
 
@@ -77,9 +85,15 @@ export function collapsedLine(theme: Theme, details: BoardDetails): string {
77
85
  return `${paint(theme, "label", plural(count, "card"))} ${SEPARATORS.item} ${plural(owners, "owner")} ${SEPARATORS.item} ${me}`;
78
86
  }
79
87
 
80
- /** `#4 doing · Billing webhook accepts orders`, clipped by the caller's row helper. */
88
+ /**
89
+ * `#4 execute · Billing webhook accepts orders`, clipped by the caller's row helper.
90
+ *
91
+ * The lane wears the colour the page gives it — `LANE_RULES`' role, through the tone
92
+ * bridge — rather than one accent for all five.
93
+ */
81
94
  function cardLine(theme: Theme, card: Card): string {
82
95
  const id = paint(theme, "meta", cardRef(card.id));
83
- const lane = paint(theme, "accent", card.status);
84
- return `${id} ${lane} ${SEPARATORS.item} ${paint(theme, "label", sanitizeLine(card.title))}`;
96
+ const lane = paint(theme, TONE_ROLE[LANE_RULES[card.lane].role], card.lane);
97
+ const title = paint(theme, "label", sanitizeLine(card.title));
98
+ return `${id} ${lane} ${SEPARATORS.item} ${title}`;
85
99
  }
@@ -4,45 +4,82 @@
4
4
  * Every field comes from a card file, which the model wrote and a person may have
5
5
  * edited, so it is sanitized here at the boundary that reaches the terminal, and
6
6
  * every row is clipped to the width it was given by the shared row helpers.
7
+ *
8
+ * The colours come from `lib/board/tone.ts` — the table the page reads too — through
9
+ * `TONE_ROLE`, so a lane's word in the transcript is the lane's colour on the page. Each
10
+ * word is painted on its own, which means the separator between two words is painted as
11
+ * well: a bare ` · ` would fall back to the terminal's default foreground between two
12
+ * coloured words.
7
13
  */
8
14
 
9
15
  import type { Theme } from "@earendil-works/pi-coding-agent";
10
- import { type Card, cardRef, DEFAULT_PRIORITY, priorityOf } from "../../lib/board/card.ts";
16
+ import { type Card, cardRef, DEFAULT_PRIORITY, holds, priorityOf } from "../../lib/board/card.ts";
17
+ import { LANE_RULES } from "../../lib/board/lanes.ts";
18
+ import type { Actor } from "../../lib/board/rules.ts";
19
+ import { type BoardTone, REASON_TONE } from "../../lib/board/tone.ts";
11
20
  import { sanitizeLine } from "../../lib/format.ts";
21
+ import { clip, splitRow } from "../../lib/tui/columns.ts";
12
22
  import { SEPARATORS } from "../../lib/tui/glyphs.ts";
13
- import { listRow } from "../../lib/tui/rows.ts";
23
+ import { paint } from "../../lib/tui/tokens.ts";
14
24
  import type { TailRowLine } from "../../lib/tui/tool.ts";
25
+ import { TONE_ROLE } from "./tone.ts";
15
26
 
16
- /** Who holds a card, as the row's trailing text. */
17
- function holderOf(card: Card): string {
18
- if (card.owner === undefined) return "unassigned";
19
- return sanitizeLine(card.owner.label);
27
+ /** One fact the row states about a card, and the tone it wears. */
28
+ interface Fact {
29
+ text: string;
30
+ tone: BoardTone;
20
31
  }
21
32
 
22
33
  /**
23
- * The lane, and — for work in flight — whether it was planned first, and — when it is
24
- * not the ordinary one — how urgent it is.
34
+ * Who holds a card, the lane it sits in, and — for work in flight — whether it was planned
35
+ * first, with the priority when it is not the ordinary one.
25
36
  *
26
37
  * A row carries the lane because a flat listing has no section heading to say it, and
27
- * "planned" because a `doing` card nobody planned is the one a lead looks at. The
28
- * priority rides along for the same reason: inside a lane the order is the priority,
29
- * and an order nobody can see reads as an accident.
38
+ * "planned" because a card nobody planned is the one a lead looks at. The priority rides
39
+ * along for the same reason: inside a lane the order is the priority, and an order nobody
40
+ * can see reads as an accident.
30
41
  */
31
- function stateOf(card: Card): string[] {
32
- const urgency = priorityOf(card) === DEFAULT_PRIORITY ? [] : [priorityOf(card)];
33
- if (card.status === "todo" || card.status === "done") return [...urgency, card.status];
34
- return [...urgency, card.status, card.plan === undefined ? "unplanned" : "planned"];
42
+ function factsOf(card: Card): Fact[] {
43
+ const facts: Fact[] = [
44
+ {
45
+ text: card.owner === undefined ? "unassigned" : sanitizeLine(card.owner.label),
46
+ tone: "muted",
47
+ },
48
+ ];
49
+ const priority = priorityOf(card);
50
+ if (priority !== DEFAULT_PRIORITY) facts.push({ text: priority, tone: "muted" });
51
+ facts.push({ text: card.lane, tone: LANE_RULES[card.lane].role });
52
+ // The pool has no plan to speak of and a finished card is history, so only a lane
53
+ // somebody is working in says whether the work was planned first. An unplanned card
54
+ // wears the tone the page's reason line gives the same fact.
55
+ const rule = LANE_RULES[card.lane];
56
+ if (!rule.ownerless && !rule.terminal) {
57
+ facts.push(
58
+ card.planPath === undefined
59
+ ? { text: "unplanned", tone: REASON_TONE.contract }
60
+ : { text: "planned", tone: "muted" },
61
+ );
62
+ }
63
+ return facts;
35
64
  }
36
65
 
37
- /** One card: the id and title, with its state trailing. */
38
- function cardRowLine(card: Card, width: number, theme: Theme, options: { self?: boolean } = {}): string {
39
- return listRow(theme, width, {
40
- marker: false,
41
- label: `${cardRef(card.id)} ${sanitizeLine(card.title)}`,
42
- labelRole: options.self === true ? "accent" : "label",
43
- trailing: [holderOf(card), ...stateOf(card)].join(SEPARATORS.item),
44
- });
66
+ /** One card: the id and title, with its facts trailing. */
67
+ function cardRowText(card: Card, width: number, theme: Theme, self: boolean): string {
68
+ const head = paint(theme, self ? "accent" : "label", `${cardRef(card.id)} ${sanitizeLine(card.title)}`);
69
+ const gap = paint(theme, "meta", SEPARATORS.item);
70
+ const trailing = factsOf(card)
71
+ .map((fact) => paint(theme, TONE_ROLE[fact.tone], fact.text))
72
+ .join(gap);
73
+ return splitRow(head, clip(trailing, width), width);
45
74
  }
46
75
 
47
- /** The row renderer every board section uses. */
48
- export const boardRowLine: TailRowLine<Card> = (card, width, theme) => cardRowLine(card, width, theme);
76
+ /**
77
+ * The row renderer every board section uses, for the session reading it.
78
+ *
79
+ * `me` matters: a `list` narrowed to this session's own cards should look like it, so the
80
+ * holder's rows wear the accent the page gives the card the reader is looking at. Passing
81
+ * no actor draws every row the same, which is what a card produced by an action gets.
82
+ */
83
+ export function boardRowLineFor(me?: Actor): TailRowLine<Card> {
84
+ return (card, width, theme) => cardRowText(card, width, theme, me !== undefined && holds(card, me));
85
+ }
@@ -8,7 +8,10 @@
8
8
  */
9
9
 
10
10
  import { Type } from "typebox";
11
- import { CARD_STATUSES, MAX_NOTE, PRIORITIES } from "../../lib/board/card.ts";
11
+ import { MAX_NOTE, PRIORITIES } from "../../lib/board/card.ts";
12
+ import { listOf } from "../../lib/format.ts";
13
+ import { BLOCKERS } from "../../lib/board/card.ts";
14
+ import { LANES, movableLanes } from "../../lib/board/lanes.ts";
12
15
  import { BOARD_ACTIONS } from "./types.ts";
13
16
 
14
17
  export const BoardParams = Type.Object({
@@ -24,15 +27,18 @@ export const BoardParams = Type.Object({
24
27
  card: Type.Optional(
25
28
  Type.String({
26
29
  description:
27
- 'The card id for one card, e.g. "4" or "#4". `list` reads that card; every other action but `add` acts on it.',
30
+ 'The card id for one card, e.g. "4" or "#4". `list` reads that card, and refuses `lane`, `query`, `mine`, and `round` with it; every other action but `add` acts on it.',
28
31
  }),
29
32
  ),
30
- status: Type.Optional(
33
+ lane: Type.Optional(
31
34
  Type.Union(
32
- CARD_STATUSES.map((status) => Type.Literal(status)),
35
+ LANES.map((lane) => Type.Literal(lane)),
33
36
  {
37
+ // The lanes a move may name come from the transition table, so a route added
38
+ // there is offered here without a second list to forget.
34
39
  description:
35
- "`list`: show only this lane, and not skip the `done` cards. `move`: the lane to move to (not `todo` — use `release`).",
40
+ "`list`: show only this lane, and not skip the `done` cards. " +
41
+ `\`move\`: the lane to move to — ${listOf(movableLanes())}; \`backlog\` is \`release\`'s, not a move's, and a card may move only where a transition exists from its own lane.`,
36
42
  },
37
43
  ),
38
44
  ),
@@ -42,6 +48,15 @@ export const BoardParams = Type.Object({
42
48
  "`move` to `blocked`: what the card waits on — the decision a person has to make, one line, at most 200 characters.",
43
49
  }),
44
50
  ),
51
+ blocker: Type.Optional(
52
+ Type.Union(
53
+ BLOCKERS.map((blocker) => Type.Literal(blocker)),
54
+ {
55
+ description:
56
+ "`move` to `blocked`, required: why it is blocked — the errand, not the route. A blocked card resumes in `plan`, or in `execute` once its contract is approved.",
57
+ },
58
+ ),
59
+ ),
45
60
  title: Type.Optional(Type.String({ description: "`add` and `edit`: the feature's outcome, one line." })),
46
61
  paths: Type.Optional(
47
62
  Type.Array(Type.String(), {
@@ -55,9 +70,25 @@ export const BoardParams = Type.Object({
55
70
  }),
56
71
  ),
57
72
  text: Type.Optional(
58
- Type.String({ description: `\`note\`: the note to append; one line, at most ${MAX_NOTE} characters.` }),
73
+ Type.String({
74
+ description: `\`note\`: the note to append; \`approve\` and \`review\`: what the signer saw, recorded with the sign; one line, at most ${MAX_NOTE} characters.`,
75
+ }),
76
+ ),
77
+ path: Type.Optional(
78
+ Type.String({
79
+ description:
80
+ "`contract`, required: the plan file this card is executed against — the path `plan` action `write` returned, inside this repository's plan store.",
81
+ }),
82
+ ),
83
+ goal: Type.Optional(
84
+ Type.String({ description: "`contract`: what the feature is for, one line, at most 400 characters." }),
85
+ ),
86
+ acceptance: Type.Optional(
87
+ Type.String({ description: "`contract`: how a reader can tell it is done, one line, at most 400 characters." }),
88
+ ),
89
+ scopeOut: Type.Optional(
90
+ Type.String({ description: "`contract`: what this deliberately does not do, one line, at most 400 characters." }),
59
91
  ),
60
- path: Type.Optional(Type.String({ description: "`plan`: the plan file this card is executing." })),
61
92
  priority: Type.Optional(
62
93
  Type.Union(
63
94
  PRIORITIES.map((priority) => Type.Literal(priority)),
@@ -70,7 +101,7 @@ export const BoardParams = Type.Object({
70
101
  dependsOn: Type.Optional(
71
102
  Type.Array(Type.String(), {
72
103
  description:
73
- "`edit`: the cards this one waits for, as ids. It cannot be claimed or moved to doing or done while one of them is unfinished; `[]` removes every link.",
104
+ "`edit`: the cards this one waits for, as ids. It cannot be claimed, or moved to execute or done, while one of them is unfinished; `[]` removes every link.",
74
105
  }),
75
106
  ),
76
107
  mine: Type.Optional(
@@ -84,13 +115,14 @@ export const BoardParams = Type.Object({
84
115
  ),
85
116
  round: Type.Optional(
86
117
  Type.Boolean({
87
- description: "`list`: also return the lead's rollup — owners, unassigned, unplanned, blocked, idle.",
118
+ description:
119
+ "`list`: also return the lead's rollup — owners, unassigned, unplanned, awaiting approval, blocked, idle, and done.",
88
120
  }),
89
121
  ),
90
122
  archived: Type.Optional(
91
123
  Type.Boolean({
92
124
  description:
93
- "`list`: read the archive instead of the board — the finished cards `archive` set aside, which no other read opens. `query` and `mine` narrow it; `status` and `round` do not apply to a pile that is out of the board.",
125
+ "`list`: read the archive instead of the board — the finished cards `archive` set aside, which no other read opens. `query` and `mine` narrow it; `lane` and `round` do not apply to a pile that is out of the board.",
94
126
  }),
95
127
  ),
96
128
  });
@@ -34,10 +34,11 @@ export async function viewOf(context: SnapshotContext): Promise<BoardView> {
34
34
  cards,
35
35
  now: context.now(),
36
36
  bytes: await context.store.bytes(),
37
- skipped: skipped.length,
37
+ skipped,
38
38
  repo: context.store.name(),
39
39
  root: context.store.root(),
40
40
  where: context.store.where(),
41
+ limits: context.store.config().wip,
41
42
  ...(roster === undefined ? {} : { roster }),
42
43
  });
43
44
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The terminal's word for a board tone.
3
+ *
4
+ * The tone table (`lib/board/tone.ts`) says what a lane, a reason, and a decision row mean;
5
+ * the page wears those words as CSS properties directly. The terminal has its own vocabulary —
6
+ * `lib/tui/tokens.ts`'s roles are `ok`, `warn`, `bad`, `meta`, `faint`, and deliberately not
7
+ * `success`, `danger`, or `muted` — so this is the one place the two are bridged, in the
8
+ * extension, because only the transcript speaks the terminal's words.
9
+ */
10
+
11
+ import type { BoardTone } from "../../lib/board/tone.ts";
12
+ import type { TuiRole } from "../../lib/tui/tokens.ts";
13
+
14
+ /** The role each board tone is painted as, closest first. */
15
+ export const TONE_ROLE: Record<BoardTone, TuiRole> = {
16
+ muted: "meta",
17
+ dim: "faint",
18
+ warn: "warn",
19
+ accent: "accent",
20
+ danger: "bad",
21
+ success: "ok",
22
+ };