@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
@@ -82,7 +82,7 @@ export async function exitWorktree(
82
82
  if (!state) throw new Error("Not currently in a worktree.");
83
83
  if (state.borrowed) {
84
84
  throw new Error(
85
- "This worktree was inherited from the parent session, which owns it. Ask the parent to call exit_worktree.",
85
+ "This worktree was inherited from the parent session, which owns it. Ask the parent to call `worktree` action `exit`.",
86
86
  );
87
87
  }
88
88
  clearConfigCache();
@@ -177,7 +177,7 @@ export async function exitWorktree(
177
177
  lines.push(
178
178
  worktreePath(state.repoRoot, config, label) === state.path
179
179
  ? `Re-enter with /worktree enter ${label}`
180
- : `Re-enter with enter_worktree (path: ${state.path}); /worktree enter only manages ${config.dir}`,
180
+ : `Re-enter with \`worktree\` action \`enter\` (path: ${state.path}); /worktree enter only manages ${config.dir}`,
181
181
  );
182
182
  }
183
183
  touchRecord(state.repoRoot, config, state.path);
@@ -2,10 +2,10 @@
2
2
  * worktree — Isolated git worktrees (EnterWorktree / ExitWorktree) for Pi.
3
3
  *
4
4
  * Mimics Claude Code's worktree isolation:
5
- * - `enter_worktree` creates (or enters) an isolated `git worktree` under
5
+ * - `worktree` action `enter` creates (or enters) an isolated `git worktree` under
6
6
  * `.pi/worktrees/<name>` on branch `worktree-<name>`, and rebinds every
7
7
  * path-taking tool to it.
8
- * - `exit_worktree` returns to the main checkout and cleans up, checking for
8
+ * - `worktree` action `exit` returns to the main checkout and cleans up, checking for
9
9
  * uncommitted work and unpushed commits first.
10
10
  *
11
11
  * Pi has no mutable session cwd and its built-ins capture cwd at construction,
@@ -72,10 +72,10 @@ export interface WorktreeRegistration {
72
72
  /** Prompt section describing the active worktree, shown while the session is isolated. */
73
73
  export function formatWorktreeSection(state: WorktreeState): string {
74
74
  return (
75
- `You are in an isolated git worktree; relative paths resolve there, not in the main checkout.\n` +
75
+ `You are in an isolated git worktree: relative paths resolve there, not in the checkout.\n` +
76
76
  `${worktreeFacts(state, "worktree", 10)}\n` +
77
- `Do not edit files or run git against the main checkout.` +
78
- (state.borrowed ? "\nThis worktree is inherited; do not exit it." : "\nUse exit_worktree to return.")
77
+ `Do not edit files or run git in the checkout.` +
78
+ (state.borrowed ? "\nThis worktree is inherited; do not exit it." : "\nLeave with `worktree` action `exit`.")
79
79
  );
80
80
  }
81
81
 
@@ -5,7 +5,7 @@
5
5
  * The frame, navigation, y/N confirmation, and pane come from the shared
6
6
  * `DockListScreen`, so this module owns only the worktree vocabulary. Rows come
7
7
  * from the same `worktreeSnapshot` the text status renders and their marks from the
8
- * same `statusMarks`, so the screen and the `list_worktrees` tool cannot disagree
8
+ * same `statusMarks`, so the screen and the `worktree` tool's `list` action cannot disagree
9
9
  * about a worktree.
10
10
  *
11
11
  * Every verb but `p` closes the screen and hands the action back to the command:
@@ -167,8 +167,8 @@ export class WorktreeListComponent extends DockListScreen<WorktreeRow> {
167
167
  "↑/↓ or j/k select",
168
168
  { text: "Enter or l detail", color: "accent" },
169
169
  { text: "e enter", color: "accent" },
170
- { text: "x exit", color: "warning" },
171
- { text: "p prune", color: "warning" },
170
+ { text: "x exit", color: "warn" },
171
+ { text: "p prune", color: "warn" },
172
172
  ];
173
173
  }
174
174
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Worktree status and reporting: labels, summaries, refusal messages, and the
3
- * override/`list_worktrees` view.
3
+ * override/`list` view.
4
4
  *
5
5
  * One snapshot feeds both surfaces: `formatWorktreeStatus` renders it as the text
6
6
  * a tool result or a headless command prints, and `screen.ts` renders the same
@@ -205,7 +205,7 @@ export function formatWorktreeStatus(snapshot: WorktreeSnapshot): string {
205
205
  return lines.join("\n");
206
206
  }
207
207
 
208
- /** Read the status and render it: what the `list_worktrees` tool and a headless `/worktree` print. */
208
+ /** Read the status and render it: what the `list` action and a headless `/worktree` print. */
209
209
  export async function worktreeStatus(pi: ExtensionAPI, ctx: ExtensionContext): Promise<string> {
210
210
  return formatWorktreeStatus(await worktreeSnapshot(pi, ctx));
211
211
  }
@@ -1,39 +1,75 @@
1
1
  /**
2
- * Model-facing tools for the worktree extension.
2
+ * The model-facing tool for the worktree extension.
3
+ *
4
+ * One tool with four actions, like `plan` and `board`: `enter` creates or enters an
5
+ * isolated checkout, `exit` returns to the main checkout, `list` reports the current
6
+ * isolation and the managed worktrees, and `prune` removes the ones nobody is
7
+ * using. Each action refuses outside the state it applies to, and the lifecycle
8
+ * itself belongs to `lifecycle.ts`.
3
9
  */
4
10
 
5
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+ import { StringEnum } from "@earendil-works/pi-ai";
6
13
  import { Type } from "typebox";
7
14
  import { messageOf } from "../../lib/errors.ts";
8
15
  import {
9
16
  enterWorktree,
17
+ enteredWorktree,
10
18
  exitWorktree,
19
+ exitedWorktree,
11
20
  isWorktreeCancelled,
12
21
  pruneWorktrees,
13
- enteredWorktree,
14
- exitedWorktree,
15
22
  worktreeStatus,
16
23
  } from "./lifecycle.ts";
17
24
  import { getActive, getInactiveOverrides } from "./runtime.ts";
18
25
 
19
26
  /**
20
- * Model-facing tool names, verb-first to match Claude Code's plan/worktree tools
21
- * (`EnterWorktree`/`ExitWorktree`). Exported so the naming guard test can check
22
- * them without loading the extension.
27
+ * The model-facing tool name. `worktree` is the domain, the way `plan` and `board`
28
+ * are, and the four lifecycle verbs are its actions.
23
29
  */
24
- export const WORKTREE_TOOLS = {
25
- enter: "enter_worktree",
26
- exit: "exit_worktree",
27
- prune: "prune_worktrees",
28
- list: "list_worktrees",
29
- } as const;
30
+ export const WORKTREE_TOOL = "worktree";
31
+
32
+ /** The tool's actions, in the order the description offers them. */
33
+ export const WORKTREE_ACTIONS = ["enter", "exit", "list", "prune"] as const;
34
+
35
+ /** One verb of the `worktree` tool. */
36
+ export type WorktreeAction = (typeof WORKTREE_ACTIONS)[number];
37
+
38
+ const WorktreeParams = Type.Object({
39
+ action: StringEnum(WORKTREE_ACTIONS, {
40
+ description:
41
+ "`enter` creates or enters an isolated worktree, `exit` returns to the main checkout, `list` reports the isolation and the managed worktrees, `prune` removes the unused ones.",
42
+ }),
43
+ name: Type.Optional(
44
+ Type.String({
45
+ description: "`enter`: name for a new worktree (branch worktree-<name>, directory .pi/worktrees/<name>).",
46
+ }),
47
+ ),
48
+ path: Type.Optional(
49
+ Type.String({
50
+ description: "`enter`: absolute or repo-relative path of an existing worktree to enter instead of creating one.",
51
+ }),
52
+ ),
53
+ remove: Type.Optional(Type.Boolean({ description: "`exit`: force removal (or keep when false) without prompting." })),
54
+ keepBranch: Type.Optional(Type.Boolean({ description: "`exit`: keep the worktree branch when removing." })),
55
+ });
56
+
57
+ /** The fields one action reads, so a test or a caller can name them without the whole schema. */
58
+ export type WorktreeArgs = {
59
+ action: WorktreeAction;
60
+ name?: string;
61
+ path?: string;
62
+ remove?: boolean;
63
+ keepBranch?: boolean;
64
+ };
30
65
 
31
66
  /**
32
- * The result a lifecycle tool answers when the user declined.
67
+ * The result a lifecycle action answers when the user declined.
33
68
  *
34
- * Entering and exiting ask the same question and are refused the same way: the message is
35
- * the refusal the user just chose, and `cancelled` is what tells a caller it was a choice
36
- * rather than a failure. A cancelled call still throws — every other error is a real one.
69
+ * Entering and exiting ask the same question and are refused the same way: the
70
+ * message is the refusal the user just chose, and `cancelled` is what tells a
71
+ * caller it was a choice rather than a failure. A cancelled call still throws —
72
+ * every other error is a real one.
37
73
  */
38
74
  function cancelledResult(error: unknown): { content: [{ type: "text"; text: string }]; details: { cancelled: true } } {
39
75
  if (!isWorktreeCancelled(error)) throw error;
@@ -41,114 +77,75 @@ function cancelledResult(error: unknown): { content: [{ type: "text"; text: stri
41
77
  }
42
78
 
43
79
  export function registerTools(pi: ExtensionAPI): void {
44
- pi.registerTool({
45
- name: WORKTREE_TOOLS.enter,
46
- label: "Enter worktree",
80
+ pi.registerTool<typeof WorktreeParams, unknown>({
81
+ name: WORKTREE_TOOL,
82
+ label: "Worktree",
47
83
  description:
48
- "Create or enter an isolated git worktree and switch the session's working directory into it. Pass `name` to " +
49
- "create one or `path` to enter an existing one. Errors when the session is already isolated.",
50
- promptSnippet: "Create or enter an isolated git worktree (only when instructed).",
84
+ "An isolated git worktree for this session. `enter` creates one on its own branch (or enters an existing " +
85
+ "checkout) and switches the session's working directory into it; `exit` leaves it and returns to the main " +
86
+ "checkout, removing it when clean and asking when it holds work; `list` reports whether the session is " +
87
+ "isolated, its path and branch, the inactive overrides, and the managed worktrees on disk; `prune` removes " +
88
+ "the managed worktrees that are clean, have no new commits, are not the current one, are not locked by a " +
89
+ "live process, and are older than the configured age. Every field but `action` says which action reads it.",
90
+ promptSnippet: "Create, enter, leave, list, or prune the session's isolated git worktrees.",
51
91
  promptGuidelines: [
52
- "Use enter_worktree only when the user or project instructions explicitly ask for a worktree; never start one on your own initiative.",
53
- "Call list_worktrees first if unsure whether the session is already isolated: enter_worktree errors when it is.",
92
+ "Use worktree with action enter only when the user or project instructions explicitly ask for a worktree; never start one on your own initiative.",
93
+ "Call action list first if unsure whether the session is already isolated: action enter errors when it is.",
94
+ "Call action exit after work started with action enter; it is the only way back to the main checkout, and asking before ending a task keeps uncommitted work from being discarded.",
95
+ "Use action prune to clean up worktrees left behind by earlier sessions; worktrees with any work are kept.",
54
96
  ],
55
- parameters: Type.Object({
56
- name: Type.Optional(
57
- Type.String({
58
- description: "Name for a new worktree (branch worktree-<name>, directory .pi/worktrees/<name>).",
59
- }),
60
- ),
61
- path: Type.Optional(
62
- Type.String({
63
- description: "Absolute or repo-relative path of an existing worktree to enter instead of creating one.",
64
- }),
65
- ),
66
- }),
97
+ parameters: WorktreeParams,
67
98
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
68
99
  executionMode: "sequential",
69
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
70
- try {
71
- const { state, summary } = await enterWorktree(pi, ctx, { name: params.name, path: params.path });
72
- ctx.ui.notify(enteredWorktree(state), "info");
73
- return {
74
- content: [{ type: "text", text: summary }],
75
- details: { state },
76
- };
77
- } catch (error) {
78
- return cancelledResult(error);
79
- }
80
- },
81
- });
82
100
 
83
- pi.registerTool({
84
- name: WORKTREE_TOOLS.exit,
85
- label: "Exit worktree",
86
- description:
87
- "Leave the current git worktree and return to the main checkout. The worktree is removed when clean; with " +
88
- "uncommitted changes or new commits the user chooses whether to keep it. The result names the branch's " +
89
- "commits and the merge or pull-request command that finishes it.",
90
- promptSnippet: "Leave the current git worktree and return to the main checkout.",
91
- promptGuidelines: [
92
- "Use exit_worktree after work started with enter_worktree; it is the only way back to the main checkout.",
93
- "Ask before ending the task rather than exiting on your own initiative, so uncommitted work is not discarded without a choice.",
94
- ],
95
- parameters: Type.Object({
96
- remove: Type.Optional(Type.Boolean({ description: "Force removal (or keep when false) without prompting." })),
97
- keepBranch: Type.Optional(Type.Boolean({ description: "Keep the worktree branch when removing." })),
98
- }),
99
- annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
100
- executionMode: "sequential",
101
101
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
102
- try {
103
- const result = await exitWorktree(pi, ctx, { remove: params.remove, keepBranch: params.keepBranch });
104
- ctx.ui.notify(exitedWorktree(result.state), "info");
105
- return {
106
- content: [{ type: "text", text: result.output }],
107
- details: { state: result.state, removed: result.removed, handoff: result.handoff },
108
- };
109
- } catch (error) {
110
- return cancelledResult(error);
102
+ switch (params.action) {
103
+ case "enter": {
104
+ try {
105
+ const { state, summary } = await enterWorktree(pi, ctx, { name: params.name, path: params.path });
106
+ ctx.ui.notify(enteredWorktree(state), "info");
107
+ return {
108
+ content: [{ type: "text", text: summary }],
109
+ details: { action: "enter", state },
110
+ };
111
+ } catch (error) {
112
+ return cancelledResult(error);
113
+ }
114
+ }
115
+ case "exit": {
116
+ try {
117
+ const result = await exitWorktree(pi, ctx, { remove: params.remove, keepBranch: params.keepBranch });
118
+ ctx.ui.notify(exitedWorktree(result.state), "info");
119
+ return {
120
+ content: [{ type: "text", text: result.output }],
121
+ details: {
122
+ action: "exit",
123
+ state: result.state,
124
+ removed: result.removed,
125
+ handoff: result.handoff,
126
+ },
127
+ };
128
+ } catch (error) {
129
+ return cancelledResult(error);
130
+ }
131
+ }
132
+ case "prune":
133
+ return {
134
+ content: [{ type: "text", text: await pruneWorktrees(pi, ctx) }],
135
+ details: { action: "prune" },
136
+ };
137
+ case "list": {
138
+ const current = getActive();
139
+ return {
140
+ content: [{ type: "text", text: await worktreeStatus(pi, ctx) }],
141
+ details: {
142
+ action: "list",
143
+ active: current ? { path: current.path, branch: current.branch, borrowed: !!current.borrowed } : null,
144
+ inactiveOverrides: getInactiveOverrides(),
145
+ },
146
+ };
147
+ }
111
148
  }
112
149
  },
113
150
  });
114
-
115
- pi.registerTool({
116
- name: WORKTREE_TOOLS.prune,
117
- label: "Prune worktrees",
118
- description:
119
- "Remove managed git worktrees that are clean, have no new commits, are not the current one, are not locked by " +
120
- "a live process, and are older than the configured pruneAfterDays. Worktrees with any work are kept.",
121
- promptSnippet: "Remove clean, unused managed git worktrees older than the configured age.",
122
- promptGuidelines: [
123
- "Use prune_worktrees to clean up worktrees left behind by earlier sessions; worktrees with any work are kept.",
124
- ],
125
- parameters: Type.Object({}),
126
- annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
127
- executionMode: "sequential",
128
- async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
129
- return { content: [{ type: "text", text: await pruneWorktrees(pi, ctx) }], details: {} };
130
- },
131
- });
132
-
133
- pi.registerTool({
134
- name: WORKTREE_TOOLS.list,
135
- label: "List worktrees",
136
- description:
137
- "Report whether the session is isolated in a git worktree, its path and branch, any inactive isolation " +
138
- "overrides, and the managed worktrees on disk. Read-only.",
139
- promptSnippet: "Report whether the session is isolated in a git worktree and list managed worktrees.",
140
- promptGuidelines: ["Use list_worktrees to check isolation and see managed worktrees before entering or pruning."],
141
- parameters: Type.Object({}),
142
- annotations: { readOnlyHint: true, openWorldHint: false },
143
- async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
144
- const current = getActive();
145
- return {
146
- content: [{ type: "text", text: await worktreeStatus(pi, ctx) }],
147
- details: {
148
- active: current ? { path: current.path, branch: current.branch, borrowed: !!current.borrowed } : null,
149
- inactiveOverrides: getInactiveOverrides(),
150
- },
151
- };
152
- },
153
- });
154
151
  }
package/lib/README.md CHANGED
@@ -4,7 +4,7 @@ Small modules imported by two or more extensions. Pi loads each extension with
4
4
  an **isolated module cache**, so these are *value-only* libraries: pure
5
5
  functions, constants, and classes. Never keep cross-extension state here — a
6
6
  module-level variable would have one copy per extension, not one shared copy.
7
- `test/structure.test.ts` fails a module-scope `let`, `var`, `Map`, `WeakMap`, or
7
+ `test/boundaries.test.ts` fails a module-scope `let`, `var`, `Map`, `WeakMap`, or
8
8
  empty array, so this is a test and not a convention. The single exception is the
9
9
  event-bus pattern in `rails.ts`, where the state lives in the caller's closure,
10
10
  not in this module.
@@ -18,8 +18,9 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
18
18
  | Module | Responsibility |
19
19
  |---|---|
20
20
  | [`browser.ts`](./browser.ts) | Cross-platform default-browser opener shared by `file-browse` and `plan`: pure `browserCommand(url, platform)` plus best-effort `openInBrowser(url, { platform, spawn })` with an injectable spawn. |
21
+ | [`bus-request.ts`](./bus-request.ts) | The request/response handshake over `pi.events` for a question one extension asks another: `BusAnswer` (a value, or the promise of one an owner is still producing), `BusRequest` (the channel, the payload built around the helper's one-shot `respond`, the `unclaimed` sentence for a session where nothing listens, and an optional `failed`), and `requestOverBus`, which settles exactly once — on the claimant's answer, on the microtask fallback when nobody claims, or on `failed` when a claim's promise rejects. Shared by [`plan-entry.ts`](./plan-entry.ts) (enter plan mode), [`http/mount.ts`](./http/mount.ts) (a page's prefix), and [`session-tool.ts`](./session-tool.ts) (join the team), so one guarantee has one body. |
21
22
  | [`config.ts`](./config.ts) | Config loading, two ways: `loadConfigFile` for a whole file, and the shared `bytefly.json` (`SHARED_CONFIG_FILE`) via `configSection` / `configValueAt` / `loadConfigSection` (global, then project), plus the atomic `writeConfigFile` / `writeConfigSection` / `writeConfigValue` / `writeConfigValueAt` / `writeConfigValuesAt` / `unsetConfigValueAt` (the leaf writers preserve siblings and metadata, `writeConfigValuesAt` applies several leaves in one document pass, and the last removes one and prunes what it emptied). Includes `clampInteger` / `clampNumber` / `cleanString` / `optionalString` / `asBoolean` / `stringList` / `asRecord` / `asNumber` (the untrusted-JSON narrowing pair) / `readJson` / `readJsonResult`. |
22
- | [`env.ts`](./env.ts) | Env-var contracts shared across extensions: the worktree environment (`ENV_ROOT`, `worktreeRoot`, `resolveEffectiveCwd`) used by worktree, file-browse, job, and rewind, plus `isExtensionEnabled` / `ENV_DISABLED_EXTENSIONS` for the package-wide disable list. |
23
+ | [`env.ts`](./env.ts) | Env-var contracts shared across extensions: the worktree environment (`ENV_ROOT`, `worktreeRoot`, `resolveEffectiveCwd`) used by worktree, file-browse, job, and rewind, plus `extensionEnablement` for the package-wide disable list (`ENV_DISABLED_EXTENSIONS`) and the opt-in extensions only `ENV_ENABLED_EXTENSIONS` can load (`OPT_IN_EXTENSIONS`). |
23
24
  | [`escape.ts`](./escape.ts) | `escapeHtml`: the single HTML text/attribute escape used by the browse page renderers and the Markdown and syntax highlighters, plus `decodeCodePoint`, the other half of the same boundary — one numeric character reference as text, with a surrogate or out-of-range value decoded to U+FFFD. |
24
25
  | [`events.ts`](./events.ts) | Inter-extension `pi.events` channels and payloads: `JOB_FINISHED_CHANNEL` / `JobFinishedEvent` (published by `job`, consumed by bytefly's notify capability), `SUBAGENT_FINISHED_CHANNEL` / `SubagentFinishedEvent` (published by `subagent`, consumed by bytefly's footer roll-up), and `WEB_SEARCH_PROVIDER_CHANNEL` / `WebSearchProviderEvent` (published by `web-access` at session start, consumed by bytefly's doctor so it can report the resolved provider without importing the extension — the key itself is never in the payload). |
25
26
  | [`notice.ts`](./notice.ts) | The shared transcript notice: `NOTICE_TYPE`, the `NoticeDetails` / `NoticeItem` shape, `noticeMessage` (the `display: true` message both send paths accept), `isNotice`, and `pruneStaleNotices` — posted by `job`, `watch`, and `session-bus`, rendered by bytefly — plus `burstText`, the one assembly of the model-facing text for a burst of pending outcomes (marker, counted headline, rows, an optional `… N more`, an optional hint). |
@@ -31,24 +32,38 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
31
32
  | [`pool.ts`](./pool.ts) | `mapConcurrent`: one async read per entry with at most `limit` calls in flight and results in input order, so a directory listing or a status sweep fans out without an unbounded burst or a per-entry round trip. Shared by `file-browse` and `worktree`. |
32
33
  | [`qr.ts`](./qr.ts) | The QR seam shared by `wechat`'s login and `plan`'s shared review link: `qrMatrix` (the module matrix), `qrLines` (Unicode half-block terminal lines of a fixed width), and `qrSvg` (inline markup a page can paste); the `qrcode` package is imported on first use, never at registration. |
33
34
  | [`interaction.ts`](./interaction.ts) | Turn-affinity prompt contract: the channel a remote transport marks on the UI context it owns (`INTERACTION_TURN_KEY`), the `InteractionChannel` interface (`request` / `post` / `postFile`, plus an optional `setPromptTimeout` scope for a dialog the human must read something before answering), and `remoteChannel` / `isRemoteTurn` / `supportsCustomUI` / `askHuman` (with an optional file, fallback text, digest preface, and prompt `timeoutMs`) plus the shared “Other” label. |
35
+ | [`lazy.ts`](./lazy.ts) | The one-load seam for a surface a command needs but registration does not: `lazy(load)` returns `get()` (concurrent callers share one load) and `peek()` (the loaded value, or `undefined`, so a shutdown path never starts a load the session never used). State lives in the returned closure; a rejected load is forgotten so a transient failure can be retried. Shared by `plan`'s review server and `board`'s page. |
34
36
  | [`git/`](./git/) | Shared git plumbing. [`git/runner.ts`](./git/runner.ts) is the `RunGit` seam (`createExecRunner` with timeout/env handling, `runGitOrThrow`, `GitError`); [`git/read.ts`](./git/read.ts) holds the read helpers (`repoRoot`, `repoRootFor`, `gitDir`, `gitCommonDir`, `revParse`, `revParseMany` for several revisions in one call, `hasCommits`, `currentBranch`); [`git/index.ts`](./git/index.ts) is the barrel. |
35
37
  | [`highlight.ts`](./highlight.ts) | First-party, escape-first syntax tokenizer (`detectLanguage`, `fenceLanguage`, `highlightLines`, `HighlightLanguage`, `TokenClass`) for js/ts, json, css, html, shell, python, java, yaml, and sql; shared by `file-browse` and plan's code blocks. |
36
- | [`http/`](./http/) | The loopback HTTP layer shared by `file-browse`, `plan`, and `board`: [`http/serve.ts`](./http/serve.ts) (the one server entry, `serveLoopback` — bind with a port retry, the capped request body, `HEAD` of a stream, the SSE head, idle grace, and the idempotent close — so an extension supplies a handler and no extension builds a `node:http` server), [`http/sink.ts`](./http/sink.ts) (`EventSink`, the one thing a route writes, which owns liveness — a write after the end is a no-op — plus the open-stream registry and the idle grace — armed when the last one goes, and, for a server that asks, from the bind so a page no tab ever opened cannot hold its port), [`http/headers.ts`](./http/headers.ts) (the page policy `PAGE_CSP` and the `pageHeaders` / `scriptHeaders` / `jsonHeaders` sets, plus `eventHeaders` for the one long-lived stream), [`http/response.ts`](./http/response.ts) (`ServeResponse` — a buffered body, a `filePath` stream, or an SSE `stream` — plus `htmlResponse` / `jsonResponse` / `jsonError` / `textResponse`), [`http/router.ts`](./http/router.ts) (`parseRequestUrl`, and `isLocalHostHeader` with the `required` mode each server chooses), [`http/guard.ts`](./http/guard.ts) (the request guards a served page shares: `postOnly`, `checkOrigin`, `requireJsonBody`, and `envelopeRefusal` — the method allowlist, then the fail-closed `Host` rule, then the origin check on every state-changing method), [`http/token.ts`](./http/token.ts) (`mintToken`, 128 random bits as hex, and `tokenMatches`, the constant-time comparison a route trusts), [`http/server.ts`](./http/server.ts) (the socket primitives `serveLoopback` is built from — `bindHost` with the accept-error listener, `respond` for HEAD / streamed file / body, `openEventStream`, and the idempotent `createCloser` — layer-internal, so the barrel does not export them), and the [`http/index.ts`](./http/index.ts) barrel. |
37
- | [`tui/`](./tui/) | The Bytefly terminal design system shared by every interactive surface, whose conventions are its own doc, [`tui/README.md`](./tui/README.md). Modules: [`tui/tokens.ts`](./tui/tokens.ts) (semantic role -> theme color, `paint`), [`tui/glyphs.ts`](./tui/glyphs.ts) (`GLYPHS`, `SEPARATORS`, `STATUS_KEYS`, `EXPAND_KEYBINDING` with `expandKey` / `expandHint`, `GLYPH_GAP` — the vocabulary both sides of a status must agree on), [`tui/chrome.ts`](./tui/chrome.ts) (the docked-list frame `dockListFrame`, the `screenFooter` that closes a screen's own rows, `screenHint`, `detailBlockRows` (the rows its detail block costs), `viewportRows`, `FULL_SCREEN_OVERLAY`, `reuseText`, `editorTheme`), [`tui/rules.ts`](./tui/rules.ts) (the rule glyph `RULE_GLYPH`, and `plainRule` / `titleRule` / `centeredRule`), [`tui/indent.ts`](./tui/indent.ts) (the transcript block indent: `BODY_INDENT`, an optional rail prefix that a wrapped call line is kept out of, and `bodyLines` / `indentBlockLines` / `indentedLines`), [`tui/rows.ts`](./tui/rows.ts) (`listRow` with its `align` option for a value held beside its label, `statusRow`, `checkbox`, `chip`, `keyValueRow`, `emptyState`, `byRunningFirst` for a live list's order), [`tui/columns.ts`](./tui/columns.ts) (`splitRow` for two cells at the two edges, `inlineRow` for two cells held together, `fitCells`, `flowCells`, `padTo`, `clip`, `clipMiddle`, `labelLine` / `clipLabel` with `LABEL_WIDTH`), [`tui/gauge.ts`](./tui/gauge.ts) (`computeGauge`, `gaugeRole`, `gaugeBar`), [`tui/line-pane.ts`](./tui/line-pane.ts) (`LinePane`, the scrollable log/match pane), [`tui/status-controller.ts`](./tui/status-controller.ts) (`createStatusController`, the footer chips `job`, `watch`, and `subagent` share, with the attached-context `notify` and a `detach` that clears its keys), [`tui/dock-screen.ts`](./tui/dock-screen.ts) (`DockListScreen`, the two-mode docked list + detail-pane screen with navigation, wheel, range, and confirm), [`tui/confirm.ts`](./tui/confirm.ts) (`ConfirmState`, the y/N layer behind every destructive screen action), [`tui/tool.ts`](./tui/tool.ts) (`toolCallTitle`, the width-safe `ToolCallText` / `reuseCallText`, `callLine` — the one headline every renderer draws — `detailsOrLine` (a result's details, or the one `errorLine` / `rejectionLine` a failure shows), sanitized `errorLine` / `rejectionLine`, `moreHint` / `overflowHint` / `earlierHint`, the `TailResult` transcript block, `syncElapsedTimer`, `elapsedSuffix` — the tool-transcript kit), [`tui/render-cache.ts`](./tui/render-cache.ts) (`RenderCache`: the per-instance line memo a component that derives its lines from another component's output renders through, so a repeat frame is one comparison), and [`tui/cursor.ts`](./tui/cursor.ts) (scroll and navigation: `fitRows`, `keepVisible`, `clampScroll`, `navIntent`, `navigateList`, `navigateScroll`, `wheelDelta`, `scrollList`, `isCloseKey`, `selectionMarker`, `formatRange`, and the `ListCursor` (by index) and `IdCursor` (by stable id) state). |
38
+ | [`http/`](./http/) | The loopback HTTP layer shared by `file-browse`, `plan`, and `board`: [`http/serve.ts`](./http/serve.ts) (the one server entry, `serveHttp` — one socket per session, dispatch to the longest matching mount prefix, the body cap of whichever mount was reached, `HEAD` of a stream, the SSE head, one idle grace per mount, and an idempotent close that the last mount's release reaches on its own — so an extension supplies routes and no extension binds a socket), [`http/mount.ts`](./http/mount.ts) (the contract a page and that server agree on: `HTTP_MOUNT_CHANNEL`, the `MountSpec` it asks with — its session, prefix, handler, cap and grace — the `HttpMount` it gets back (`base`, `origin`, `release`, and `share` for one LAN address), `MountOutcome`, and `requestMount`, whose handshake is *claimed* synchronously so a bind that is still starting is not mistaken for a session with no server), [`http/sink.ts`](./http/sink.ts) (`EventSink`, the one thing a route writes, which owns liveness — a write after the end is a no-op — plus the open-stream registry and the idle grace — armed when the last one goes, and, for a mount that asks, from the mount so a page no tab ever opened cannot hold its prefix), [`http/headers.ts`](./http/headers.ts) (the page policy `PAGE_CSP` and the `pageHeaders` / `scriptHeaders` / `jsonHeaders` sets, plus `eventHeaders` for the one long-lived stream), [`http/response.ts`](./http/response.ts) (`ServeResponse` — a buffered body, a `filePath` stream, or an SSE `stream` — plus `htmlResponse` / `jsonResponse` / `jsonError` / `textResponse`), [`http/router.ts`](./http/router.ts) (`parseRequestUrl`, and `isLocalHostHeader` with the `required` mode each page chooses), [`http/guard.ts`](./http/guard.ts) (the request guards a served page shares: `postOnly`, `checkOrigin`, `requireJsonBody`, and `envelopeRefusal` — the method allowlist, then the fail-closed `Host` rule, then the origin check on every state-changing method), [`http/token.ts`](./http/token.ts) (`mintToken`, 128 random bits as hex, and `tokenMatches`, the constant-time comparison a route trusts), [`http/port.ts`](./http/port.ts) (`PORT_BASE`, `PORT_RANGE`, and `projectPort` — the band `serve` takes a session's one port from, and the number a project keeps in it, its repository key hashed, so a bookmark survives a restart and every doc states the range), [`http/server.ts`](./http/server.ts) (the socket primitives `serveHttp` is built from — `bindHost` with the accept-error listener, `respond` for HEAD / streamed file / body, `openEventStream`, and the idempotent `createCloser` — layer-internal, so the barrel does not export them), and the [`http/index.ts`](./http/index.ts) barrel, which is the served-page kit: a registration graph may reach [`http/mount.ts`](./http/mount.ts) and nothing else in this directory, so the rest loads with the page that needs it. |
39
+ | [`tui/`](./tui/) | The Bytefly terminal design system shared by every interactive surface, whose conventions are its own doc, [`tui/README.md`](./tui/README.md). Modules: [`tui/tokens.ts`](./tui/tokens.ts) (semantic role -> theme color, `paint`), [`tui/glyphs.ts`](./tui/glyphs.ts) (`GLYPHS`, `SEPARATORS`, `STATUS_KEYS`, `EXPAND_KEYBINDING` with `expandKey` / `expandHint`, `GLYPH_GAP` — the vocabulary both sides of a status must agree on), [`tui/chrome.ts`](./tui/chrome.ts) (the docked-list frame `dockListFrame`, the `screenFooter` that closes a screen's own rows, `screenHint`, `detailBlockRows` (the rows its detail block costs), `viewportRows`, `FULL_SCREEN_OVERLAY`, `reuseText`, `editorTheme`), [`tui/rules.ts`](./tui/rules.ts) (the rule glyph `RULE_GLYPH`, and `plainRule` / `titleRule` / `centeredRule`), [`tui/indent.ts`](./tui/indent.ts) (the transcript block indent: `BODY_INDENT`, an optional rail prefix that a wrapped call line is kept out of, and `bodyLines` / `indentBlockLines` / `indentedLines`), [`tui/rows.ts`](./tui/rows.ts) (`listRow` with its `align` option for a value held beside its label, `statusRow`, `checkbox`, `chip`, `keyValueRow`, `emptyState`, `byRunningFirst` for a live list's order), [`tui/columns.ts`](./tui/columns.ts) (`splitRow` for two cells at the two edges, `inlineRow` for two cells held together, `fitCells`, `flowCells`, `padTo`, `clip`, `clipMiddle`, `labelLine` / `clipLabel` with `LABEL_WIDTH`), [`tui/gauge.ts`](./tui/gauge.ts) (`computeGauge`, `gaugeRole`, `gaugeBar`), [`tui/line-pane.ts`](./tui/line-pane.ts) (`LinePane`, the scrollable log/match pane), [`tui/status-controller.ts`](./tui/status-controller.ts) (`createStatusController`, the footer chips `job`, `watch`, and `subagent` share, with the attached-context `notify` and a `detach` that clears its keys), [`tui/dock-screen.ts`](./tui/dock-screen.ts) (`DockListScreen`, the two-mode docked list + detail-pane screen with navigation, wheel, range, and confirm), [`tui/confirm.ts`](./tui/confirm.ts) (`ConfirmState`, the y/N layer behind every destructive screen action), [`tui/tool.ts`](./tui/tool.ts) (`toolCallTitle`, the width-safe `ToolCallText` / `reuseCallText`, `callLine` — the one headline every renderer draws — `detailsOrLine` (a result's details, or the one `errorLine` / `rejectionLine` a failure shows), sanitized `errorLine` / `rejectionLine`, `moreHint` / `overflowHint` / `earlierHint`, the `TailResult` transcript block and `isTailResult` (the guard that puts back the row type `instanceof` erases, so an extension reuses its block without a cast), `syncElapsedTimer`, `elapsedSuffix` — the tool-transcript kit), [`tui/render-cache.ts`](./tui/render-cache.ts) (`RenderCache`: the per-instance line memo — `lines()` for a component that derives its lines from another component's output, `keyed()` for one with no inner component that describes its own inputs — so a repeat frame is one comparison), and [`tui/cursor.ts`](./tui/cursor.ts) (scroll and navigation: `fitRows`, `keepVisible`, `clampScroll`, `navIntent`, `navigateList`, `navigateScroll`, `wheelDelta`, `scrollList`, `isCloseKey`, `SHIFT_TAB` / `isShiftTab` (the raw legacy sequence included), `selectionMarker`, `formatRange`, and the `ListCursor` (by index) and `IdCursor` (by stable id) state). |
38
40
  | [`path.ts`](./path.ts) | `isInside` (string containment) plus `realPathOfNearest` / `isInsideReal` (symlink-aware): the containment predicates the file-browse, worktree, doc, and plan guards share, and the canonical-path helper the worktree registry keys records by. |
39
41
  | [`pdf.ts`](./pdf.ts) | The optional `unpdf` seam shared by `web-access` and `file-read`: `pdfGuidance`, `PdfUnavailableError`, `pdfTextFromResult`, and the lazy `loadPdfExtractor` / `extractPdfText` (an injected extractor wins), so a missing package and a merged-pages result read the same way through either surface. |
40
42
  | [`pi-invocation.ts`](./pi-invocation.ts) | `getPiInvocation`: how to launch a nested `pi` process, shared by `subagent` and bytefly's doctor surface. |
41
- | [`policy.ts`](./policy.ts) | `createReadOnlyPolicy`: default-deny tool classification from `readOnlyHint` + explicit allow/deny, and `WORKSPACE_READERS` — the built-in readers plus `read_doc` that plan, rewind, and subagent compose their allowlists from. |
43
+ | [`policy.ts`](./policy.ts) | `createReadOnlyPolicy`: default-deny tool classification from `readOnlyHint` + explicit allow/deny, `WORKSPACE_READERS` — the built-in readers plus `read_doc` that plan, rewind, and subagent compose their allowlists from — and `isBoardRead`, which asks `board/actions.ts` rather than keeping an action list of its own. |
42
44
  | [`plan-state.ts`](./plan-state.ts) | The plan-mode flag replayed from the session branch: `PLAN_MODE_ENTRY_TYPE` and `isPlanModeEnabled(entries)`, so `rewind` can refuse a write while planning without importing the plan extension. |
43
- | [`plan-entry.ts`](./plan-entry.ts) | Plan-mode entry shared by the extension that owns the mode and the one that offers it: `READ_ONLY_SUMMARY` and `PLAN_CONSENT` (one wording for the read-only guarantee), the `PLAN_ENTER_REQUEST` channel with its `PlanEnterRequest` payload, and `requestPlanMode(pi, ctx)` — a request/response over `pi.events` that resolves `false` instead of hanging when `plan` is not loaded, so a command context (which has no `executeTool`) can still enter the mode after asking the user once. |
45
+ | [`plan-entry.ts`](./plan-entry.ts) | Plan-mode entry shared by the extension that owns the mode and the ones that ask for it: `READ_ONLY_SUMMARY` and `PLAN_CONSENT` (one wording for the read-only guarantee), the `PLAN_ENTER_REQUEST` and `PLAN_TOGGLE_REQUEST` channels with their one `PlanModeRequest` payload, `isPlanExtensionLoaded(pi)` so every caller answers availability the same way, and `requestPlanMode(pi, ctx)` / `requestPlanToggle(pi, ctx)` — request/response over `pi.events` that resolve `false`/`undefined` instead of hanging when `plan` is not loaded, so a command context (which has no `executeTool`) can still enter the mode after asking the user once, and bytefly's editor can flip it from Shift+Tab. |
44
46
  | [`rails.ts`](./rails.ts) | Above-editor widget ordering (`goal` → `todo`) and dock-screen suppression, carried on `pi.events`. |
45
47
  | [`repo-key.ts`](./repo-key.ts) | The per-repository storage key that outlives one checkout: `repoKeyOf(value)` hashes an identity to `REPO_KEY_LENGTH` hex characters, and `repoKeyFor(pi, cwd)` returns `{ root, key, fromGit }` from the repository's *common* git directory, so every worktree of one repository files state under the same key, while a directory outside git keys on itself. Shared by `session-bus` (sessions, mail, claims) and `plan` (plans). |
48
+ | [`plans/paths.ts`](./plans/paths.ts) | Where a plan lives and which paths are one, for the two extensions that have to agree about the file a card is executed against: `PLANS_DIR_NAME` / `plansDir(agentDir, key?)` (`<agent-dir>/plans/`, keyed by the repository, unkeyed outside one — the `fromGit` branch both stores read off `repoKeyFor`), `isPlanFileName` (markdown, never a dotfile), the symlink-safe `isContainedPlan`, and `resolvePlanArg` (a bare name inside the store, the `~/…` form the tools display, or an absolute path). Read by `plan` before it writes and by `board` before it records a plan path, so a copy that dropped the symlink half cannot be the one that ships. |
46
49
  | [`screen-session.ts`](./screen-session.ts) | `withPolledScreen`: mount one `ctx.ui.custom` dock screen — rails suppressed for its lifetime, an optional repaint tick through an injected `RepeatTimer`, and one teardown — handing the mount the host's `tui`, `theme`, `requestRender`, `close`, and `viewportRows`. Shared by `/jobs`, `/watch`, and `/memory`, whose rails window and timer window were the same one. |
47
- | [`board/`](./board/) | The shared feature board. [`board/card.ts`](./board/card.ts) is the record: `CARD_VERSION`, `Card` / `CardStatus` / `CardOwner` / `CardNote`, the field caps `MAX_TITLE` / `MAX_PATHS` / `MAX_PLAN` / `MAX_NOTE` / `MAX_NOTES`, the `normalize*` boundary the model's arguments pass (`normalizeTitle`, `normalizePathHints` — repo-relative hints, never claims —, `normalizeNote`, `normalizePlanPath`, `normalizeCardId`, `normalizePriority`), the priority (`PRIORITIES`, `priorityOf`, `priorityRank`), the lane clock (`CardTransition`, `MAX_LOG`, and `laneSince` — when the card entered the lane it is in, or nothing when its history does not say) and the links (`dependsOf`), `matchesQuery` — the rule `list`'s `query` and the page's filter box both answer, spliced into the bundle as `MATCHES_QUERY_JS` so it cannot become two rules — `cardRef`, `signatureOf`, and `isCard`, the one gate a file read passes. [`board/rules.ts`](./board/rules.ts) is the lifecycle: `Actor`, `CardOp` (including `edit`, whose fields are each optional because an omitted one must be left alone), `createCard`, the pure `applyOp` over the card *and* the board index (every transition, its owner rule, the dependency gates a claim, a move, and a drop pass, `archive` — the one op that moves no field, because the pile a finished card's file lies in is the whole of it — and the sentence a refusal carries, with a no-op reported as `changed: false`), and `holds`. [`board/links.ts`](./board/links.ts) is the dependency graph that index answers: `BoardIndex` / `indexOf`, `blockersOf` (what is unfinished) and `blockersText` (the same list as `#2 (doing)`, one wording for a refusal and for the page), `dependentsOf` / `dependedOn` (what a drop would strand), `wouldCycle`, and `normalizeDepends` — one edge stored once, as an id in the waiting card's file. [`board/session.ts`](./board/session.ts) is the session a call works as (`BoardSession`, `openBoardSession` — one store per working directory, and the id/label pair a card records, shared by `board` and `team`). [`board/store.ts`](./board/store.ts) is the backend: `BoardStore` over `<agentDir>/board/<repo-key>/cards/*.json`, with `createStore(dir)` and the one construction point `openBoard(pi, cwd)` — `create` allocates the id by hard-linking a temp file onto the name (so two adds cannot share one), `mutate` re-reads the board and re-checks an operation whose card changed underneath instead of overwriting it, `list` orders a lane by priority before age, `archive` renames a finished card's file into `archive/` beside the cards without rewriting it — so `read` finds a card in either pile and writes it back to that one, and `list` still reads the board alone —, `archived()` / `archivedCount()` are the pile on request and its count, `nextCardId` spends an archived number as well as an ended one, and neither pile is pruned by a janitor: `archive` and `drop` are the two ways a card leaves the board. [`board/round.ts`](./board/round.ts) is the lead's rollup (`buildRound`, `IDLE_MS` — owners, unassigned, unplanned, blocked, idle, done, read from the cards alone so it works with the bus disabled), and [`board/round-text.ts`](./board/round-text.ts) is that rollup as the sentences a reader gets (`formatRound`, `plural`), so the tool's result and `team`'s `/team` cannot describe one board two ways. [`board/view.ts`](./board/view.ts) is the page's snapshot (`buildView` → lanes, counts, rollup, bytes, signature, where, and the session reading it), the one shape a client is allowed to know: each card arrives as a `ViewCard` carrying the priority it acts on, the moment it entered its lane (`since`), and, when something unfinished blocks it, `waits` — so the browser renders an answer instead of re-deciding one. [`board/page-pointer.ts`](./board/page-pointer.ts) is how a second session finds a page already serving (`PAGE_FILE`, `PagePointer`, `readPointer`, the exclusive `createPointer`, `clearPointer` for its own note only, `probePage` — the file is believed only while it answers, and it carries the page's one token, which reads and nothing else — and `settlePointer`, the ladder a starting page runs: write, attach to a winner that answers, take over a note proved dead, and name the file rather than blame a session when it cannot be read). |
48
- | [`session-roster.ts`](./session-roster.ts) | Who is on this repository's bus, read as data: `readRoster(dir)` returns `RosterSession` (`id`, `label`, `mode`, `lastSeen`, `mail`, `pid` and `startToken` when the record names them, and `watching` when it says) or `undefined` when the bus has never been used here, `rosterStateOf` answers `live` / `quiet` / `gone` — a dead pid outranks a fresh heartbeat, and the start token settles a reused pid — and `BUS_DIR_NAME` / `SESSIONS_DIR_NAME` / `INBOX_DIR_NAME` / `SESSION_TTL_MS` are the one copy of the layout, imported by `session-bus` and read by `board`'s page so the Team tab can list teammates without the board depending on the bus extension. |
50
+ | [`board/`](./board/) | The shared feature board. [`board/card.ts`](./board/card.ts) is the record: `CARD_VERSION`, `Card` / `CardStatus` / `CardOwner` / `CardNote`, the field caps `MAX_TITLE` / `MAX_PATHS` / `MAX_PLAN` / `MAX_NOTE` / `MAX_NOTES`, the `normalize*` boundary the model's arguments pass (`normalizeTitle`, `normalizePathHints` — repo-relative hints, never claims —, `normalizeNote`, `normalizePlanPath`, `normalizeCardId`, `normalizePriority`), the priority (`PRIORITIES`, `priorityOf`, `priorityRank`), the lane clock (`CardTransition`, `MAX_LOG`, and `laneSince` — when the card entered the lane it is in, or nothing when its history does not say) and the links (`dependsOf`), `matchesQuery` — the rule `list`'s `query` and the page's filter box both answer, spliced into the bundle as `MATCHES_QUERY_JS` so it cannot become two rules — `cardRef`, `signatureOf`, `holds` (the id decides when both sides have one), `isCard` — the shape a read is held to, whose ownership invariant comes off the lane table — and the `CardBlock` record a blocked card carries. [`board/read.ts`](./board/read.ts) is the reader: `readCard`, the one gate a file read passes, which maps a v1 file's `status` / `plan` / `blockedBy`+`blocker` onto this build's `lane` / `planPath` / `blocked`, reads the four-lane log's `todo` / `doing` as `backlog` / `execute`, and stamps `CARD_VERSION`, so an older card reads rather than disappearing. [`board/rules.ts`](./board/rules.ts) is the lifecycle: `Actor`, `CardOp`, `createCard`, and the pure dispatch — `mayOp` (the same decision without the write, which is what `team` asks before it sends a handoff) and `applyOp`, one switch over the card *and* the board index with a no-op reported as `changed: false`; the one write (`write`) that bumps the revision, appends the lane log, and clears the fields a lane left behind; and the operations themselves are `board/steps.ts`. [`board/links.ts`](./board/links.ts) is the dependency graph that index answers: `BoardIndex` / `indexOf`, `blockersOf` (what is unfinished) and `blockersText` (the same list as `#2 (execute)`, one wording for a refusal and for the page), `dependentsOf` / `dependedOn` (what a drop would strand), `wouldCycle`, and `normalizeDepends` — one edge stored once, as an id in the waiting card's file. [`board/join.ts`](./board/join.ts) is the one join both tools ask for (`joinOnce` — ask only while the standing does not say this session is on the team, and never let a failed join undo the card it came after) , so a claim through `board` and one through `team` cannot answer it differently; [`board/session.ts`](./board/session.ts) is the session a call works as (`BoardSession`, `TeamStanding` — what the last join did, so a claim joins once and `/board status` can say — and `openBoardSession`: one store per working directory, the seat it serves under, the id a card records and the name the bus already holds for it — else the id-derived one —, and one session per host so the seat `board` sets is the name `team` writes), plus `busDirFor(agentDir, store)` — the bus's directory for a repository, built from the key the store carries, so the roster cannot be moved by regrouping the board's own files — and `planDir(cwd)`, this repository's plan store through `plans/paths.ts`, which is what a card's plan file has to be inside. [`board/store.ts`](./board/store.ts) is the backend: `BoardStore` over `<agent-dir>/board/<repo-key>/cards/*.json`, with `createStore(dir, { root, key, config })` and the one construction point `openBoard(pi, cwd)` — `root()` and `name()` say which repository a hashed board belongs to, which is the one part of its identity a reader can say out loud, and `key` is the hash it was filed under, carried rather than read back off `dir`'s last segment because `session-bus` files this repository's records under that same key — `create` allocates the id by hard-linking a temp file onto the name (so two adds cannot share one), `mutate` re-reads the board and re-checks an operation whose card changed underneath instead of overwriting it, `list` orders a lane by priority, then by id, `scan` is one pass over the pile answering both the cards and the files it refused — so a page's frame reads the board once, and `list` and `skipped` are its two halves —, `archive` renames a finished card's file into `archive/` beside the cards without rewriting it — so `read` finds a card in either pile and writes it back to that one, and `list` still reads the board alone —, `archived()` / `archivedCount()` are the pile on request and its count, `nextCardId` spends an archived number as well as an ended one, and neither pile is pruned by a janitor: `archive` and `drop` are the two ways a card leaves the board. [`board/round.ts`](./board/round.ts) is the lead's rollup (`buildRound`, `IDLE_MS` — owners, unassigned, unplanned (a contract still missing a line, or a plan never recorded), unapproved, blocked, idle, done, read from the cards alone so it works with the bus disabled), and [`board/round-text.ts`](./board/round-text.ts) is that rollup as the sentences a reader gets (`formatRound` — which folds the owners a bus has no record of and that hold nothing into one line, given the roster —, `plural`, and the fold's own two bodies, `isHistoryOwner` and `historyLine`, spliced into the page as `HISTORY_OWNER_JS` / `HISTORY_LINE_JS` the way `PLURAL_JS` is), so the tool's result, `team`'s `/team`, and the Team tab cannot describe one board two ways, and [`board/skipped.ts`](./board/skipped.ts) is the sentence for the card files a board refused (`skippedText`, over the store's `Skip`), which the tool prints under a listing and the page carries on its footer's hover title so the two name the same files. [`board/view.ts`](./board/view.ts) is the page's snapshot (`buildView` → lanes (each carrying its words, colour, limit, and count), the total, the rollup, the signature, the repository's name and root, and the store's path), the one shape a client is allowed to know: each card arrives as a `ViewCard` carrying the priority it acts on, the moment it entered its lane (`since`), and, when something unfinished blocks it, `waits` — so the browser renders an answer instead of re-deciding one. [`board/page-pointer.ts`](./board/page-pointer.ts) is how a second session finds a page already serving (`PAGE_FILE`, the two routes a probe asks for (`PING_PATH`, `SNAPSHOT_PATH` — the router serves the same constants, so a route cannot move out from under the probe), `PagePointer`, `readPointer`, the exclusive `createPointer`, `clearPointer` for its own note only, `probePage` — the file is believed only while it answers, and it carries the page's one token, which reads and nothing else — and `settlePointer`, the ladder a starting page runs: write, attach to a winner that answers, take over a note proved dead, and name the file rather than blame a session when it cannot be read). |
51
+ | [`board/lanes.ts`](./board/lanes.ts) | The board's lane vocabulary and its gates, as data: `LANES` / `Lane` (backlog, plan, execute, blocked, done) and `LANE_RULES` — each lane's label, its empty word, its colour role, and the three facts the rest of the board reads off it (`ownerless`, the pool; `terminal`, finished; `pulled`, held to a WIP limit). `TRANSITIONS` is the other half: every lane change a `move` may make, with the `Requirement`s (contract, approval, review, artifact, blocker, waiting) in front of it, so `tools.ts` offers exactly the targets `steps.ts` accepts instead of carrying a second list that drifts. `isLane`, `laneIndex`, `targetsFrom`, `movableLanes`. |
52
+ | [`board/actions.ts`](./board/actions.ts) | The board's actions and which of them only read it: `BOARD_ACTIONS` / `BoardAction` — the list the tool publishes, in its description order — and `BOARD_ACTION_KIND`, a `Record` over every one of them, so an action added to the tuple does not compile until somebody says whether it reads. `BOARD_READ_ACTIONS` is the same answer for a reader that has to name the reads, `readActionsText` is that list as the phrase plan mode's refusal prints (both inflections tested, since only one action reads today), and `isBoardReadAction` answers for one unnormalized value, where anything unknown is not a read. One table, because the tool's enum and the read-only policy were once two: `lib/policy.ts`'s `isBoardRead` and `board`'s own schema both read this one, so a second read action cannot be admitted by one and refused by the other. |
53
+ | [`board/artifact.ts`](./board/artifact.ts) | Whether a path is a plan of this repository, which is the one thing the board asks of the plan store: `PlanArtifact` (the resolved path that was proved), `ArtifactCheck` (that path, or the sentence refusing it — one fact for the state machine to order), `planForContract` (a `.md` this repository's store holds and the file is there), and `planForExecute` (the plan a card recorded, still there, refused with the card named and `contract` as the way out). The rules come from [`plans/paths.ts`](./plans/paths.ts), so the board and the plan tool cannot disagree about what a plan path is; the board still never reads a plan. |
54
+ | [`board/contract.ts`](./board/contract.ts) | What a card promises before it may be executed: `Contract` (goal, acceptance, scopeOut, and the `Sign` that approved the wording), `CONTRACT_FIELDS` / `MAX_CONTRACT`, the boundary `normalizeContractField` applies to one line of a model's argument, `missingContractFields` / `contractComplete` / `contractApproved` — the gate `steps.ts` asks — and `mergedContract`, which drops the sign because an edited line is an unapproved one. |
55
+ | [`board/steps.ts`](./board/steps.ts) | What each operation does, split from the table that dispatches it: `claim` pulls a pool card into `plan`, `assign` hands one over (and moves only a pool card's lane), `release` / `reclaim` return it, `move` walks `TRANSITIONS` and asks every gate on the way, `contractOp` writes the three lines and the plan path, `approve` / `review` record the two human gates (both refused to the card's own holder), and `note` / `edit` / `offer` / `archive` / `drop` keep their rules. Each answers a `Prepared`: a refusal sentence, a no-op, or the fields a write would apply. |
56
+ | [`board/limits.ts`](./board/limits.ts) | How many cards a pulled lane may hold, and the sentence that refuses the next one: `WipLimits` (plan, execute), `DEFAULT_WIP` (3 and 5), `MAX_WIP`, `normalizeWip` over `clampInteger`, `limitOf` / `countIn` (what a lane's requirement asks, all the empty-object and one-object answers included), and `wipRefusal` — the refusal that names the count, the limit, and the ways out. The arithmetic only: where the numbers are read from is `board/config.ts`. |"
57
+ | [`board/config.ts`](./board/config.ts) | The `board` section of `bytefly.json`, read once: `BoardConfig` (`wip` and `lead`), `DEFAULT_BOARD_CONFIG` (3 and 5, and the lead `lead`), `normalizeBoardConfig` — each key falling back on its own, so a file that sets one keeps the other — and `readBoardConfig`, the project's section over the global one. Handed to `applyOp` whole, so `rules.ts` stays pure; `extensions/bytefly` imports it too, so the settings screen shows what the loader keeps rather than a copy of the default. |"
58
+ | [`board/lead.ts`](./board/lead.ts) | Who may sign a card's two gates: `Lead` (a team name), `normalizeLead` over `session-label.ts`'s rule, `isLead` (a case-insensitive label compare, which is not `holds`), and the person at the keyboard — `PERSON_LABEL` (`you`), `PERSON_ID` (`person:you`), `personActor`, `isPerson`, which is the actor `/board approve` and `/board review` write as. `signRefusal` / `holderRefusal` / `missingSign` are the three sentences the gates and a lane's requirement are refused with, so the words of each sign live in one table. Pure: a name compare and a sentence. |
59
+ | [`board/seat.ts`](./board/seat.ts) | The seat a page is served as: `SEAT_ID_PREFIX` (`board:`) and `pageActor` — the actor a page reads as, built from the board's lead (`board.lead`), with an id derived from the label so a handover does not change it. The name arrives normalized by `config.ts`, so there is no second label rule here and no default name: a constant would be a second answer to what the config already says. Pure, and the page writes nothing, so the id decides no ownership. |
60
+ | [`board/tone.ts`](./board/tone.ts) | What colour a board surface wears, stated once: `BoardTone` (the page kit's own colour words, plus `dim`, so a tone is the name of a CSS property), `BOARD_TONES`, `REASON_TONE` — the tone of every reason kind, `waiting` included because the card face invents it — and `DECISION_KINDS` / `DECISION_TONE`, the Team queue's kinds in rank order with the tone each wears, so a row's position and its colour cannot disagree. `DECISION_RANK_JS` / `DECISION_TONE_JS` / `REASON_TONE_JS` are those tables as the browser bundle's own source, spliced in like every other rule the page shares. The terminal's vocabulary is not here: `extensions/board/tone.ts` maps a tone to a `TuiRole`. |
61
+ | [`board/reason.ts`](./board/reason.ts) | Why a card sits where it does, as one rule: `reasonFor(card, ago)` returns a `Reason` (`kind` plus one sentence) for an unanswered offer, a blocked card's recorded reason, a contract's missing line, an approval nobody gave, a spot-check nobody did, and a finished card's commit — the card face, the lead's queue, and the transcript all read it. Spliced into the page as `REASON_FOR_JS`, which is why it takes its clock as a parameter and calls nothing else. |
62
+ | [`session-roster.ts`](./session-roster.ts) | Who is on this repository's bus, read as data: `readRoster(dir)` returns `RosterSession` (`id`, `label`, `mode`, `lastSeen`, `mail`, `pid` and `startToken` when the record names them, and `watching` when it says) or `undefined` when the bus has never been used here, `rosterStateOf` answers `live` / `quiet` / `gone` — a dead pid outranks a fresh heartbeat, and the start token settles a reused pid — and `BUS_DIR_NAME` / `SESSIONS_DIR_NAME` / `INBOX_DIR_NAME` / `SESSION_TTL_MS` are the one copy of the layout, imported by `session-bus` and read by `board`'s page so the Team tab can list teammates without the board depending on the bus extension. `job` asks `rosterStateOf` too, over a session marker rather than a bus record, so "is that session still there" has one answer for ownership as well as for display. |
49
63
  | [`session-tool.ts`](./session-tool.ts) | The `session` tool as the other extensions call it: `join(ctx, {label?, wake?, listen?})`, `peers(ctx)`, and `send(ctx, {kind, to, text})`, each a typed result that never throws (`{ ok, error }` or `undefined` for a read the bus cannot answer), plus `MAX_MESSAGE_TEXT`, the send cap `session-bus` re-exports as `MAX_TEXT`. One copy, because the alternative is two extensions knowing the tool's details shape. |
50
- | [`session-label.ts`](./session-label.ts) | The label rules a session is addressed by and a card records: `MAX_LABEL`, `normalizeLabel` (one line, capped, with a fallback), `shortId`, and `defaultLabel` (the Pi session name, else the working directory's basename, else the id). Shared by `session-bus` (which settles the unique one) and `board` (which records it on a claimed card), so a card's owner names a target the bus answers to. |
64
+ | [`session-label.ts`](./session-label.ts) | The label rules a session is addressed by and a card records: `MAX_LABEL`, `normalizeLabel` (one line, capped, with a fallback), `shortId`, `defaultLabel` (the Pi session name, else the working directory's basename, else the id — the bus's own fallback for a session that asked for no name), and the team's vocabulary, `TEAM_AGENT_PREFIX` / `agentLabel` (the id's tail, so a name cannot drift on a rename or collide) and `TEAMMATE_NAMES` / `presetNameFor(id, taken)` (the familiar name a session with none of its own draws: the id hashes to a start, a taken name walks on, and every name taken is `undefined` so the caller keeps `agentLabel`). Shared by `session-bus` (which settles the unique one) and `board` (which writes and joins under it), so a card's owner names a target the bus answers to. |
51
65
  | [`requirements.ts`](./requirements.ts) | Per-extension runtime prerequisite table plus a pure `probeExtension(seams)` over injected seams (exec, config reads, optional imports, the published search provider, env, and a refresh-only reachability probe): bytefly's doctor surface reports on it and the extensions that own a dependency format their errors from it. A check kind is either a prerequisite (`binary`, `module`, `state`, `invocation`, `notify`, `provider`) or a `safety` switch whose off value trades a protection away, read from `bytefly.json` global-then-project. |
66
+ | [`child.ts`](./child.ts) | A spawned command's process lifecycle, shared by `job` and `watch`: `ChildSlot` (the caller's `pid`/`child`/`killTimer`, mutated in place), `ChildSink` (`onData` for merged output, `onSettled` for a close or a failed spawn), and `createChildSupervisor` — `start` over `SpawnFn` with the caller's `SpawnOptions`, `terminate` as the signal the caller asked for then SIGKILL after `graceMs`, `kill`, `clearTimer`, and a bounded `wait`. One escalation body and one default grace (`KILL_GRACE_MS`), with each owner passing its configured one, and no state of its own. |
52
67
  | [`shell.ts`](./shell.ts) | Shell escape rule shared by the worktree isolation guard and plan mode's read-only git guard: `SHELL_ESCAPABLE`, `isShellEscapable`, `hasShellEscape`. |
53
68
  | [`spawn.ts`](./spawn.ts) | The process seam for extensions that run a shell command: `SpawnedProcess`, `SpawnFn`, `KillTreeFn`, the pure `spawnOptions` / `killPlan`, `applyKillPlan` over injected seams, the liveness pair `isPidAlive` / `startTokenOf` (the second settles a pid reused by another process), and the `defaultSpawn` / `defaultKillTree` that execute them (process group on POSIX, `taskkill /T` on Windows). Shared by `job`, `watch`, `check`, `worktree`, `wechat`, and the session roster. |
54
69
  | [`goal-state.ts`](./goal-state.ts) | The published `goal` state replayed from the session branch: `PublishedGoal`, `PublishedGoalStatus` (`planning` / `active` / `achieved`), `GOAL_ENTRY_TYPE`, `MAX_GOAL_OBJECTIVE`, `isPursuedGoal(goal)` and `lastGoal(entries)` (the last valid goal, skipping a rejected call and honouring an explicit clear), so `plan` can restate a pursued goal while planning, promote a `planning` one at approval, and offer to record one when a plan is approved with none, and `goal` itself reads the branch through the same replay — with no extension importing another's modules. |
@@ -57,7 +72,7 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
57
72
  | [`tool-names.ts`](./tool-names.ts) | Tool names and one parameter name that more than one extension must agree on, so an orchestrating `ctx.executeTool()` call or schema probe breaks the build on a rename. |
58
73
  | [`tool-result.ts`](./tool-result.ts) | The tool-result envelope: `textResult` builds the one text block, the caller's `details`, and the optional `structuredContent` mirror; `failedResult` is the same shape marked `isError`, keeping the previous state for a retry; `toolError` builds the rejected-call content plus the caller's details, and `errorResult` is the action-and-message form `job` and `watch` share. |
59
74
  | [`usage.ts`](./usage.ts) | The usage counters an accumulator is measured in: `USAGE_COUNTERS`, `UsageCounter`, `UsageCounts`, and `zeroUsage` — shared by bytefly's session and `↳` roll-up accounting and by `subagent`'s fold of a child's `message_end` events, so two extensions cannot drift on which numbers a run is made of. `turns` and the context size are not counters and stay with each owner. |
60
- | [`web/`](./web/) | The Bytefly web brand system shared by the browser surfaces, whose conventions are its own doc, [`web/README.md`](./web/README.md). Modules: [`web/tokens.ts`](./web/tokens.ts) (palette derived from the Bytefly terminal themes, scales, the `tok-*` map, and the derived `--on-accent` / `--scrim` / `--shadow` overlays), [`web/base.ts`](./web/base.ts) (reset, one focus ring, the print and forced-colours layers), [`web/components.ts`](./web/components.ts) (the `bf-*` kit, including the dialog, shortcut-sheet, and `bf-kbd` rules), [`web/layout.ts`](./web/layout.ts) (the app shell and its one breakpoint), [`web/client-runtime.ts`](./web/client-runtime.ts) (the browser runtime every page bundle splices in: `el`, `typing`, `paintStatus`, `postJson`, `ClientNode` — the node shape those answer — and `CLIENT_RUNTIME_JS`), [`web/runtime.ts`](./web/runtime.ts) (theme storage and bootstrap, `THEME_JS_PATH`, `THEME_TOGGLE_JS`, `dialogScript`, and `keysDialogScript`), [`web/markup.ts`](./web/markup.ts) (the HTML atoms, including `keysSheet` and its `KeyHint` rows, the mark's one geometry, which `brandLockup` draws and `markIcon` links as the tab icon, and `tabTitle`, the one shape the served pages' tab titles take), [`web/names.ts`](./web/names.ts) (a page's class names as one table: `ClassNames`, the `KEBAB` / `IDENTIFIER` shape a name must have to survive being spliced into a bundle, and `namesScript`), and the [`web/index.ts`](./web/index.ts) barrel. |
75
+ | [`web/`](./web/) | The Bytefly web brand system shared by the browser surfaces, whose conventions are its own doc, [`web/README.md`](./web/README.md). Modules: [`web/tokens.ts`](./web/tokens.ts) (palette derived from the Bytefly terminal themes, scales, the `tok-*` map, and the derived `--on-accent` / `--scrim` / `--shadow` overlays), [`web/base.ts`](./web/base.ts) (reset, one focus ring, the print and forced-colours layers), [`web/components.ts`](./web/components.ts) (the `bf-*` kit, including the dialog, shortcut-sheet, and `bf-kbd` rules), [`web/layout.ts`](./web/layout.ts) (the app shell and its one breakpoint), [`web/client-runtime.ts`](./web/client-runtime.ts) (the browser runtime every page bundle splices in: `el`, `typing`, `paintStatus`, `postJson`, `ClientNode` — the node shape those answer — and `CLIENT_RUNTIME_JS`), [`web/runtime.ts`](./web/runtime.ts) (theme storage and bootstrap, `THEME_JS_PATH`, `THEME_TOGGLE_JS`, `dialogScript`, and `keysDialogScript`), [`web/markup.ts`](./web/markup.ts) (the HTML atoms, including `keysSheet` and its `KeyHint` rows, the mark's one geometry, which `brandLockup` draws and `markIcon` links as the tab icon, and `tabTitle`, the one shape the served pages' tab titles take), [`web/names.ts`](./web/names.ts) (a page's class names as one table: `ClassNames`, the `KEBAB` / `IDENTIFIER` shape a name must have to survive being spliced into a bundle, and `namesScript`), [`web/markdown.ts`](./web/markdown.ts) (the inline Markdown renderer both served pages splice: `safeHref`'s one link policy — `http`, `https`, `mailto`, no credentials, no disguised relative target — and `renderInline`, which appends text nodes and `code` / `strong` / `em` / `a` only, as `INLINE_MARKDOWN_JS`), and the [`web/index.ts`](./web/index.ts) barrel. |
61
76
 
62
77
  ## Conventions
63
78
 
@@ -79,6 +94,6 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
79
94
  - **A color is a role, not a token.** The rules and the primitives are in
80
95
  [`tui/README.md`](./tui/README.md#color-is-a-role): draw through
81
96
  [`tui/tokens.ts`](./tui/tokens.ts) (`paint(theme, role, text)`) so a theme swap
82
- re-skins every surface. The one exception is Pi's tool-rendering vocabulary
83
- (`toolOutput`, `toolTitle`), which stays direct: those tokens style a tool
84
- block, and no role maps to them.
97
+ re-skins every surface. `TUI_TOKENS` is the one place a Pi token is named, Pi's
98
+ tool vocabulary (`toolTitle`, `toolOutput`) included — a renderer names the
99
+ role, and a theme that maps the token elsewhere is respected.