@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
@@ -8,7 +8,10 @@
8
8
  */
9
9
 
10
10
  import { Type } from "typebox";
11
- import { CARD_STATUSES, 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(), {
@@ -49,9 +64,31 @@ export const BoardParams = Type.Object({
49
64
  "`add` and `edit`: repository-relative hints (directories or files) where the work happens. Not claims.",
50
65
  }),
51
66
  ),
52
- note: Type.Optional(Type.String({ description: "`add`: a first note, e.g. why the card exists." })),
53
- text: Type.Optional(Type.String({ description: "`note`: the note to append." })),
54
- path: Type.Optional(Type.String({ description: "`plan`: the plan file this card is executing." })),
67
+ note: Type.Optional(
68
+ Type.String({
69
+ description: `\`add\`: a first note, e.g. why the card exists; one line, at most ${MAX_NOTE} characters.`,
70
+ }),
71
+ ),
72
+ text: Type.Optional(
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." }),
91
+ ),
55
92
  priority: Type.Optional(
56
93
  Type.Union(
57
94
  PRIORITIES.map((priority) => Type.Literal(priority)),
@@ -64,7 +101,7 @@ export const BoardParams = Type.Object({
64
101
  dependsOn: Type.Optional(
65
102
  Type.Array(Type.String(), {
66
103
  description:
67
- "`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.",
68
105
  }),
69
106
  ),
70
107
  mine: Type.Optional(
@@ -78,13 +115,14 @@ export const BoardParams = Type.Object({
78
115
  ),
79
116
  round: Type.Optional(
80
117
  Type.Boolean({
81
- 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.",
82
120
  }),
83
121
  ),
84
122
  archived: Type.Optional(
85
123
  Type.Boolean({
86
124
  description:
87
- "`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.",
88
126
  }),
89
127
  ),
90
128
  });
@@ -18,8 +18,6 @@ export interface SnapshotContext {
18
18
  store: BoardStore;
19
19
  /** The actor every request is made and reported as: the page's seat. */
20
20
  actor: () => Actor;
21
- /** The session serving the page, which is who is reading, not who it writes as. */
22
- session?: () => Actor;
23
21
  now: () => number;
24
22
  /** Where this repository's bus files its records. */
25
23
  busDir: () => Promise<string>;
@@ -28,15 +26,19 @@ export interface SnapshotContext {
28
26
  /** The snapshot the page renders, and the body of `/api/board`. */
29
27
  export async function viewOf(context: SnapshotContext): Promise<BoardView> {
30
28
  const roster = await readRoster(await context.busDir());
31
- const session = context.session?.();
29
+ // One read of the pile answers both: a frame that asked `list` and `skipped` separately
30
+ // would parse every card twice, once a second, for as long as a tab is open.
31
+ const { cards, skipped } = await context.store.scan();
32
32
  return buildView({
33
33
  me: context.actor(),
34
- ...(session === undefined ? {} : { session }),
35
- cards: await context.store.list(),
34
+ cards,
36
35
  now: context.now(),
37
36
  bytes: await context.store.bytes(),
38
- skipped: (await context.store.skipped()).length,
37
+ skipped,
38
+ repo: context.store.name(),
39
+ root: context.store.root(),
39
40
  where: context.store.where(),
41
+ limits: context.store.config().wip,
40
42
  ...(roster === undefined ? {} : { roster }),
41
43
  });
42
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
+ };
@@ -14,30 +14,35 @@
14
14
 
15
15
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
16
  import type { Static } from "typebox";
17
- import { cardRef, matchesQuery, normalizeCardId } from "../../lib/board/card.ts";
17
+ import { type ArtifactCheck, planForContract, planForExecute } from "../../lib/board/artifact.ts";
18
+ import { type Card, cardRef, holds, matchesQuery, normalizeCardId } from "../../lib/board/card.ts";
19
+ import { movableLanes } from "../../lib/board/lanes.ts";
18
20
  import { indexOf } from "../../lib/board/links.ts";
19
- import { type CardOp, holds } from "../../lib/board/rules.ts";
21
+ import type { CardOp } from "../../lib/board/rules.ts";
20
22
  import { buildRound } from "../../lib/board/round.ts";
23
+ import { readRoster, type RosterSession } from "../../lib/session-roster.ts";
21
24
  import type { BoardStore } from "../../lib/board/store.ts";
22
25
  import { messageOf } from "../../lib/errors.ts";
26
+ import { listOf } from "../../lib/format.ts";
23
27
  import { revParse } from "../../lib/git/read.ts";
24
28
  import { strayField } from "../../lib/stray-fields.ts";
25
29
  import { createExecRunner } from "../../lib/git/runner.ts";
26
30
  import { failedResult, textResult } from "../../lib/tool-result.ts";
27
31
  import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
28
- import { callLine, detailsOrLine } from "../../lib/tui/tool.ts";
32
+ import { callLine, detailsOrLine, isTailResult, TailResult } from "../../lib/tui/tool.ts";
29
33
  import { formatRound } from "../../lib/board/round-text.ts";
34
+ import { skippedText } from "../../lib/board/skipped.ts";
30
35
  import {
31
36
  archiveNote,
37
+ CLAIM_NEXT,
32
38
  formatArchive,
33
39
  formatArchived,
34
40
  formatBoard,
35
41
  formatCard,
36
42
  formatDropped,
37
- formatSkipped,
38
43
  } from "./format.ts";
39
44
  import { createTeamJoin, type TeamJoin } from "../../lib/session-tool.ts";
40
- import { BoardResult, collapsedLine, resultInput } from "./result.ts";
45
+ import { collapsedLine, resultInput } from "./result.ts";
41
46
  import { createInput } from "./input.ts";
42
47
  import type { BoardSession } from "../../lib/board/session.ts";
43
48
  import { BoardParams } from "./schema.ts";
@@ -45,7 +50,6 @@ import {
45
50
  ACTION_FIELDS,
46
51
  BOARD_ACTION_DOCS,
47
52
  BOARD_ACTIONS,
48
- BOARD_MUTATING_ACTIONS,
49
53
  BOARD_TOOL,
50
54
  type BoardAction,
51
55
  type BoardDetails,
@@ -76,14 +80,14 @@ function callSubject(action: BoardAction, args: Record<string, unknown>): string
76
80
  if (action === "list") {
77
81
  const parts = ["list"];
78
82
  if (args.round === true) parts.push("round");
79
- if (typeof args.status === "string") parts.push(args.status);
83
+ if (typeof args.lane === "string") parts.push(args.lane);
80
84
  if (typeof args.query === "string" && args.query.trim() !== "") parts.push(`"${args.query.trim()}"`);
81
85
  if (card !== "") parts.push(card);
82
86
  return parts.join(" ");
83
87
  }
84
88
  if (card === "") return action;
85
- const status = typeof args.status === "string" ? ` ${args.status}` : "";
86
- return `${action} ${card}${status}`;
89
+ const lane = typeof args.lane === "string" ? ` ${args.lane}` : "";
90
+ return `${action} ${card}${lane}`;
87
91
  }
88
92
 
89
93
  /** One refusal: the reason for the model, the state for the renderer. */
@@ -96,6 +100,17 @@ function refusal(action: BoardAction, me: BoardDetails["me"], message: string, c
96
100
  });
97
101
  }
98
102
 
103
+ /**
104
+ * Whether a result this tool built is a refusal.
105
+ *
106
+ * `refusal` marks one with `isError`, and the success shape (`ToolResult`) has no such
107
+ * field, so this is the one probe that tells the two apart — read here rather than
108
+ * widening the shared envelope with a flag that would then be optional everywhere.
109
+ */
110
+ function refused(result: unknown): boolean {
111
+ return typeof result === "object" && result !== null && (result as { isError?: unknown }).isError === true;
112
+ }
113
+
99
114
  /**
100
115
  * The commit at `cwd`'s `HEAD`, shortened, or nothing when there is none.
101
116
  *
@@ -116,14 +131,16 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
116
131
  label: "Board",
117
132
  description:
118
133
  "The shared feature list for this repository: one card per feature, which any session reads and writes. " +
119
- "A card carries the feature's outcome, the session that holds it, path hints, the plan it is executing, " +
120
- `its priority, what it waits for, and its notes. ${verbHelp()} A card's steps do not belong on the ` +
121
- "board - they belong in the plan and in `todo`.",
134
+ "A card moves through five lanes - backlog, plan, execute, blocked, done - and carries the feature's " +
135
+ "outcome, the session that holds it, the contract it promises (goal, acceptance, scopeOut), the plan it " +
136
+ `is executed against, its priority, what it waits for, and its notes. ${verbHelp()} A card's steps do not ` +
137
+ "belong on the board - they belong in the plan and in `todo`.",
122
138
  promptSnippet: `The shared feature list for this repository: ${BOARD_ACTIONS.join(", ")}.`,
123
139
  promptGuidelines: [
124
- "Claim a card before working on it; release or finish it when you stop, and reclaim a card whose owner is gone.",
125
- "Record the plan file with `board` action `plan` once the plan is approved, then execute it.",
126
- "Note each phase of the work, and move the card to `done` or `blocked` when it changes state.",
140
+ "Claim a card to pull it from backlog into plan, design it in plan mode, then record the contract with `contract`: goal, acceptance, scopeOut, and the plan file `plan` action `write` returned — a path outside this repository's plan store is refused. Move it to execute only once the lead (`board.lead` in `bytefly.json`, `lead` by default, the name the page serves as) has approved that contract, or the person has signed with `/board approve` — a lead releases a card before signing it.",
141
+ "Before a card is done, the lead records a spot-check with `review`, or the person signs with `/board review`; the plan it recorded must still be in the store, and `contract` is how it is re-recorded.",
142
+ "Move a card to `blocked` with a `blocker` kind and one line about what is waited on: it resumes in `plan`, or in `execute` once its contract is approved.",
143
+ "Note each phase of the work, and release or reclaim a card you stop holding, so the lane count means what it says.",
127
144
  "A card that waits on another is linked with `edit` and `dependsOn`, so the order is on the board instead of in a refusal.",
128
145
  "A feature's steps belong in `todo`, not on the board: one card is one feature, whatever it takes.",
129
146
  "A `handoff` in your inbox is work you now own: read it with `session inbox`, record the plan, and start it.",
@@ -143,57 +160,84 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
143
160
  `unknown action "${String(args.action)}"; use ${BOARD_ACTIONS.join(", ")}.`,
144
161
  );
145
162
  }
146
- const me = runtime.who(ctx, ctx.cwd);
163
+ const me = await runtime.who(ctx, ctx.cwd);
147
164
  try {
148
- // Before the join and the store: a refused call has done nothing, so it cannot
149
- // have enlisted this session as a teammate on its way to the refusal.
165
+ // Before the join and before a card is read: a refused call has written nothing, so it
166
+ // cannot have enlisted this session as a teammate on its way to the refusal.
150
167
  const stray = strayField(action, args, ACTION_FIELDS);
151
168
  if (stray !== undefined) return refusal(action, me, stray);
152
- // Working a card makes this session a teammate, once: the board is the user's
153
- // surface for the team, so nobody has to know the bus exists. A failed join is
154
- // not a failed card write — the board works with no bus at all — and it is not
155
- // marked, so the next write asks again.
156
- if (BOARD_MUTATING_ACTIONS.has(action) && !runtime.joined()) {
157
- if ((await team.join(ctx)).ok) runtime.markJoined();
158
- }
159
169
  const store = await runtime.store(ctx.cwd);
160
170
  const now = runtime.now();
161
171
  switch (action) {
162
172
  case "list":
163
- return await read(store, action, me, args, now);
173
+ // The round folds the owners a bus has no record of, and the rollup is the only read
174
+ // that needs the bus for its own sake: a plain `list` reads no bus file beyond the
175
+ // record that names whoever is calling.
176
+ return await read(store, action, me, args, now, async () => readRoster(await runtime.busDir(ctx.cwd)));
164
177
  case "add":
165
178
  return await add(store, me, args, now);
166
- case "claim":
167
- return await mutate(store, action, me, now, args, { kind: "claim" });
179
+ case "claim": {
180
+ const claimed = await mutate(store, action, me, now, args, { kind: "claim" }, CLAIM_NEXT);
181
+ // Taking a card is what makes this session a teammate. It is the one action here
182
+ // worth being addressed about, and the board is the user's surface for the team, so
183
+ // nobody has to know the bus exists. A refused claim wrote nothing and enlists
184
+ // nobody; a refused join is not a failed claim, so the card stands and the next
185
+ // claim asks again.
186
+ if (!refused(claimed)) {
187
+ // Loaded here, not at registration: the join is the claim's side effect, so a
188
+ // session that never claims pays nothing for it.
189
+ const { joinOnce } = await import("../../lib/board/join.ts");
190
+ await joinOnce(runtime.team, team, ctx, me.label);
191
+ }
192
+ return claimed;
193
+ }
168
194
  case "release":
169
195
  return await mutate(store, action, me, now, args, { kind: "release" });
170
196
  case "reclaim":
171
197
  return await mutate(store, action, me, now, args, { kind: "reclaim" });
172
198
  case "move": {
173
199
  if (args.card === undefined) return refusal(action, me, 'card is required: pass the id, e.g. "4".');
174
- const status = args.status;
175
- if (status === undefined || status === "todo") {
176
- return refusal(
177
- action,
178
- me,
179
- 'move needs a target lane: "doing", "blocked", or "done". Use release to return a card to todo.',
180
- );
200
+ const lane = args.lane;
201
+ if (lane === undefined) {
202
+ return refusal(action, me, `move needs a target lane: ${listOf(movableLanes())}.`);
181
203
  }
182
- // Only the lane that has something to say reads for it: a move to `done`
183
- // records the commit that closed the card, and a board outside a
184
- // repository simply has none to record.
185
- const landed = status === "done" ? await headCommit(pi, ctx.cwd) : undefined;
204
+ // Only the lanes that have something to say read for it: a move to `done`
205
+ // records the commit that closed the card, and both `execute` and `done` carry
206
+ // the proof that the plan the card recorded is still there. A board outside a
207
+ // repository simply has no commit to record.
208
+ const landed = lane === "done" ? await headCommit(pi, ctx.cwd) : undefined;
209
+ const proved =
210
+ lane === "execute" || lane === "done"
211
+ ? await recordedPlan(runtime, ctx.cwd, store, args.card)
212
+ : undefined;
186
213
  return await mutate(store, action, me, now, args, {
187
214
  kind: "move",
188
- status,
215
+ lane,
189
216
  ...(args.blockedBy === undefined ? {} : { blockedBy: args.blockedBy }),
217
+ ...(args.blocker === undefined ? {} : { blocker: args.blocker }),
190
218
  ...(landed === undefined ? {} : { landed }),
219
+ ...(proved === undefined ? {} : { artifact: proved }),
191
220
  });
192
221
  }
193
222
  case "note":
194
223
  return await mutate(store, action, me, now, args, { kind: "note", text: args.text });
195
- case "plan":
196
- return await mutate(store, action, me, now, args, { kind: "plan", path: args.path });
224
+ case "contract": {
225
+ // The plan is proved here, at the boundary: the state machine decides, but only
226
+ // a caller holding the working directory can answer whether the file is there.
227
+ const plan =
228
+ args.path === undefined ? undefined : planForContract(await runtime.planDir(ctx.cwd), args.path);
229
+ return await mutate(store, action, me, now, args, {
230
+ kind: "contract",
231
+ ...(plan === undefined ? {} : { artifact: plan }),
232
+ goal: args.goal,
233
+ acceptance: args.acceptance,
234
+ scopeOut: args.scopeOut,
235
+ });
236
+ }
237
+ case "approve":
238
+ return await mutate(store, action, me, now, args, { kind: "approve", text: args.text });
239
+ case "review":
240
+ return await mutate(store, action, me, now, args, { kind: "review", text: args.text });
197
241
  case "edit": {
198
242
  if (
199
243
  args.title === undefined &&
@@ -239,14 +283,50 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
239
283
  text.setText(collapsedLine(theme, outcome.details));
240
284
  return text;
241
285
  }
242
- const view =
243
- context?.lastComponent instanceof BoardResult ? context.lastComponent : new BoardResult(input, theme);
286
+ const previous = context?.lastComponent;
287
+ const view = isTailResult<Card>(previous) ? previous : new TailResult<Card>(input, theme);
244
288
  view.setInput(input, theme);
245
289
  return view;
246
290
  },
247
291
  });
248
292
  }
249
293
 
294
+ /**
295
+ * The plan file a card entering `execute` recorded, proved to be there.
296
+ *
297
+ * A `move` names the card, not its plan, so the tool reads the card — one read, on the one
298
+ * lane whose gate needs it — and hands `steps.ts` the fact. A card with no plan recorded
299
+ * gets no fact and the state machine refuses it; a recorded plan that is gone, or outside
300
+ * this repository's store, is refused here with the sentence that names the card and the
301
+ * way out.
302
+ */
303
+ async function recordedPlan(
304
+ runtime: BoardSession,
305
+ cwd: string,
306
+ store: BoardStore,
307
+ rawCard: string | undefined,
308
+ ): Promise<ArtifactCheck | undefined> {
309
+ const id = rawCard === undefined ? undefined : normalizeCardId(rawCard);
310
+ const card = id === undefined ? undefined : await store.read(id);
311
+ if (card?.planPath === undefined) return undefined;
312
+ return planForExecute(await runtime.planDir(cwd), card);
313
+ }
314
+
315
+ /** The `list` fields that narrow a listing, in the order a refusal names them. */
316
+ const CARD_FILTERS = ["lane", "query", "mine", "round"] as const;
317
+
318
+ /**
319
+ * The refusal for the filters a `list` that names a card would otherwise drop.
320
+ *
321
+ * `card` answers for that one card, so a filter beside it changes nothing — and a call that
322
+ * sent both meant one of them. A dropped field is what `strayField` exists to prevent one
323
+ * level up, so it is refused here in the same voice the archive's own reader uses.
324
+ */
325
+ function cardFiltersRefusal(args: BoardArgs, fields: readonly (keyof BoardArgs)[] = CARD_FILTERS): string | undefined {
326
+ const given = fields.filter((field) => args[field] !== undefined);
327
+ return given.length === 0 ? undefined : `card does not read ${given.join(", ")}: it answers for that one card.`;
328
+ }
329
+
250
330
  /** `list`: the board, one card, the lead's rollup, or the archive. */
251
331
  async function read(
252
332
  store: BoardStore,
@@ -254,6 +334,7 @@ async function read(
254
334
  me: BoardDetails["me"],
255
335
  args: BoardArgs,
256
336
  now: number,
337
+ roster: () => Promise<readonly RosterSession[] | undefined>,
257
338
  ): Promise<ReturnType<typeof textResult<BoardDetails>>> {
258
339
  if (args.archived === true) return readArchive(store, action, me, args, now);
259
340
  const cards = await store.list();
@@ -263,6 +344,8 @@ async function read(
263
344
  // A directory listing, not a read: a listing says what it left aside without opening one.
264
345
  const archived = await store.archivedCount();
265
346
  if (args.card !== undefined) {
347
+ const ignored = cardFiltersRefusal(args);
348
+ if (ignored !== undefined) return refusal(action, me, ignored);
266
349
  const id = normalizeCardId(args.card);
267
350
  if (id === undefined) return refusal(action, me, `"${args.card}" is not a card id.`);
268
351
  const card = cards.find((entry) => entry.id === id);
@@ -284,7 +367,7 @@ async function read(
284
367
  }
285
368
  const listed = cards.filter(
286
369
  (card) =>
287
- (args.status === undefined || card.status === args.status) &&
370
+ (args.lane === undefined || card.lane === args.lane) &&
288
371
  (args.query === undefined || matchesQuery(card, args.query)) &&
289
372
  // The same rule a claim is checked against, so "mine" cannot list a card this
290
373
  // session could not release.
@@ -294,18 +377,23 @@ async function read(
294
377
  // repository's own board is mostly finished cards — so a listing that names no lane
295
378
  // leaves them out and says how many. A named lane, `card`, and `round` are unchanged;
296
379
  // the rollup counts the whole board, which is what makes it a rollup.
297
- const hiddenDone = args.status === undefined ? listed.filter((card) => card.status === "done").length : 0;
298
- const shown = hiddenDone === 0 ? listed : listed.filter((card) => card.status !== "done");
380
+ const hiddenDone = args.lane === undefined ? listed.filter((card) => card.lane === "done").length : 0;
381
+ const shown = hiddenDone === 0 ? listed : listed.filter((card) => card.lane !== "done");
299
382
  if (args.round !== true) {
300
- return textResult(formatBoard(shown, { hiddenDone, archived, skipped }), { action, me, cards: shown });
383
+ return textResult(formatBoard(shown, { hiddenDone, archived, skipped }), {
384
+ action,
385
+ me,
386
+ cards: shown,
387
+ ...(args.mine === true ? { mine: true } : {}),
388
+ });
301
389
  }
302
390
  const round = buildRound(cards, now);
303
391
  // One line under the rollup, because a reader who cannot see a card file should know why,
304
392
  // and one for the archive, because a rollup that silently left it out would be a board
305
393
  // smaller than the one on disk.
306
- const notes = [formatSkipped(skipped), archiveNote(archived)].filter((line) => line !== "");
307
- const text = [formatRound(cards, round), ...notes].join("\n");
308
- return textResult(text, { action, me, cards: listed, round });
394
+ const notes = [skippedText(skipped), archiveNote(archived)].filter((line) => line !== "");
395
+ const text = [formatRound(cards, round, await roster()), ...notes].join("\n");
396
+ return textResult(text, { action, me, cards: listed, round, ...(args.mine === true ? { mine: true } : {}) });
309
397
  }
310
398
 
311
399
  /**
@@ -322,7 +410,7 @@ async function readArchive(
322
410
  args: BoardArgs,
323
411
  now: number,
324
412
  ): Promise<ReturnType<typeof textResult<BoardDetails>>> {
325
- const wrong = (["status", "round"] as const).filter((field) => args[field] !== undefined);
413
+ const wrong = (["lane", "round"] as const).filter((field) => args[field] !== undefined);
326
414
  if (wrong.length > 0) {
327
415
  return refusal(
328
416
  action,
@@ -332,6 +420,9 @@ async function readArchive(
332
420
  }
333
421
  const pile = await store.archived();
334
422
  if (args.card !== undefined) {
423
+ // `query` and `mine` narrow the pile, not the one card it would answer for.
424
+ const ignored = cardFiltersRefusal(args, ["query", "mine"]);
425
+ if (ignored !== undefined) return refusal(action, me, ignored);
335
426
  const id = normalizeCardId(args.card);
336
427
  if (id === undefined) return refusal(action, me, `"${args.card}" is not a card id.`);
337
428
  const card = pile.cards.find((entry) => entry.id === id);
@@ -348,7 +439,12 @@ async function readArchive(
348
439
  const shown = pile.cards.filter(
349
440
  (card) => (args.query === undefined || matchesQuery(card, args.query)) && (args.mine !== true || holds(card, me)),
350
441
  );
351
- return textResult(formatArchive(shown, pile.skipped), { action, me, cards: shown });
442
+ return textResult(formatArchive(shown, pile.skipped), {
443
+ action,
444
+ me,
445
+ cards: shown,
446
+ ...(args.mine === true ? { mine: true } : {}),
447
+ });
352
448
  }
353
449
 
354
450
  /** `add`: the only action that creates a card. */
@@ -372,13 +468,16 @@ async function mutate(
372
468
  now: number,
373
469
  args: BoardArgs,
374
470
  op: CardOp,
471
+ /** One line a card's own text does not carry, for the action that has a next step. */
472
+ next?: string,
375
473
  ): Promise<ReturnType<typeof textResult<BoardDetails>>> {
376
474
  if (args.card === undefined) return refusal(action, me, 'card is required: pass the id, e.g. "4".');
377
475
  const id = normalizeCardId(args.card);
378
476
  if (id === undefined) return refusal(action, me, `"${args.card}" is not a card id.`);
379
477
  const result = await store.mutate(id, op, me, now);
380
478
  if (!result.ok) return refusal(action, me, result.error, result.card);
381
- return textResult(formatCard(result.card), { action, me, card: result.card });
479
+ const lines = [formatCard(result.card), ...(next === undefined ? [] : [next])];
480
+ return textResult(lines.join("\n"), { action, me, card: result.card });
382
481
  }
383
482
 
384
483
  /**
@@ -403,7 +502,7 @@ async function archiveCards(
403
502
  if (!result.changed) return textResult(`${cardRef(id)} is already archived.`, { action: "archive", me });
404
503
  return textResult(formatArchived([id]), { action: "archive", me, card: result.card, archived: [id] });
405
504
  }
406
- const finished = (await store.list()).filter((card) => card.status === "done");
505
+ const finished = (await store.list()).filter((card) => card.lane === "done");
407
506
  const moved: string[] = [];
408
507
  const failed: Array<{ card: string; error: string }> = [];
409
508
  for (const card of finished) {
@@ -23,6 +23,7 @@ import {
23
23
  type ServeResponse,
24
24
  } from "../../../lib/http/index.ts";
25
25
  import type { BoardView } from "../../../lib/board/view.ts";
26
+ import { PING_PATH, SNAPSHOT_PATH } from "../../../lib/board/page-pointer.ts";
26
27
  import { THEME_JS, THEME_JS_PATH } from "../../../lib/web/index.ts";
27
28
  import type { PageView } from "../view/page.ts";
28
29
 
@@ -45,13 +46,10 @@ export interface BoardRouterContext {
45
46
  page: (view: PageView) => Promise<string>;
46
47
  /** The client script. */
47
48
  script: string;
48
- /** The snapshot the page renders, and the body of `/api/board`. */
49
+ /** The snapshot the page renders, and the body of the snapshot route. */
49
50
  view: () => Promise<BoardView>;
50
51
  }
51
52
 
52
- /** The cheap liveness route: a probe reads this, and never the snapshot. */
53
- export const PING_PATH = "/api/ping";
54
-
55
53
  /**
56
54
  * Handle one request. `method`, `rawUrl`, and `headers` are the raw HTTP values.
57
55
  */
@@ -77,7 +75,7 @@ export async function handleBoardRequest(
77
75
  // must not pay for the snapshot, which is a store read and a roster read on the same
78
76
  // event loop the probe is racing.
79
77
  if (pathname === PING_PATH) return jsonResponse(200, { ok: true });
80
- if (pathname === "/api/board") return jsonResponse(200, await ctx.view());
78
+ if (pathname === SNAPSHOT_PATH) return jsonResponse(200, await ctx.view());
81
79
  if (pathname === "/" || pathname === "/index.html") {
82
80
  return { status: 200, headers: pageHeaders(), body: await ctx.page(viewOf(search.get("view"))) };
83
81
  }