@gavin-hu/my-pi-agent 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/README.md +46 -31
  2. package/README.zh-CN.md +40 -26
  3. package/assets/screens/board.png +0 -0
  4. package/assets/screens/chrome.png +0 -0
  5. package/assets/screens/file-browse.png +0 -0
  6. package/assets/screens/plan-review.png +0 -0
  7. package/assets/screens/plan.png +0 -0
  8. package/docs/extensions/README.md +22 -24
  9. package/docs/extensions/board.md +361 -147
  10. package/docs/extensions/bytefly.md +152 -111
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +30 -31
  14. package/docs/extensions/file-open.md +9 -11
  15. package/docs/extensions/file-read.md +12 -14
  16. package/docs/extensions/goal.md +15 -17
  17. package/docs/extensions/job.md +54 -42
  18. package/docs/extensions/memory.md +17 -19
  19. package/docs/extensions/plan.md +166 -126
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +34 -18
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +72 -33
  26. package/docs/extensions/todo.md +18 -18
  27. package/docs/extensions/watch.md +31 -31
  28. package/docs/extensions/web-access.md +2 -4
  29. package/docs/extensions/wechat.md +34 -35
  30. package/docs/extensions/worktree.md +56 -50
  31. package/extensions/board/commands.ts +56 -15
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +5 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +4 -40
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +13 -11
  38. package/extensions/board/result.ts +30 -16
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +42 -10
  41. package/extensions/board/snapshot.ts +2 -1
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +131 -44
  44. package/extensions/board/types.ts +36 -32
  45. package/extensions/board/view/card.ts +229 -0
  46. package/extensions/board/view/client.ts +33 -15
  47. package/extensions/board/view/dialog.ts +169 -6
  48. package/extensions/board/view/empty.ts +47 -0
  49. package/extensions/board/view/keys.ts +13 -2
  50. package/extensions/board/view/live.ts +89 -21
  51. package/extensions/board/view/names.ts +13 -4
  52. package/extensions/board/view/page.ts +23 -9
  53. package/extensions/board/view/render.ts +208 -271
  54. package/extensions/board/view/styles.ts +123 -31
  55. package/extensions/board/view/team.ts +297 -76
  56. package/extensions/bytefly/block/fallback.ts +1 -1
  57. package/extensions/bytefly/doctor/runtime.ts +5 -2
  58. package/extensions/bytefly/footer/collect/label.ts +60 -0
  59. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  60. package/extensions/bytefly/footer/config.ts +4 -2
  61. package/extensions/bytefly/footer/register.ts +33 -4
  62. package/extensions/bytefly/footer/render/lines.ts +7 -4
  63. package/extensions/bytefly/footer/render/segments.ts +37 -10
  64. package/extensions/bytefly/footer/render/status.ts +0 -11
  65. package/extensions/bytefly/footer/types.ts +7 -0
  66. package/extensions/bytefly/header/config.ts +1 -1
  67. package/extensions/bytefly/index.ts +32 -6
  68. package/extensions/bytefly/mode/editor.ts +115 -0
  69. package/extensions/bytefly/panel/commands.ts +210 -120
  70. package/extensions/bytefly/panel/format.ts +4 -4
  71. package/extensions/bytefly/panel/settings.ts +73 -6
  72. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  73. package/extensions/bytefly/update/cache.ts +1 -1
  74. package/extensions/bytefly/update/register.ts +4 -4
  75. package/extensions/bytefly/working/config.ts +5 -7
  76. package/extensions/bytefly/working/format.ts +10 -13
  77. package/extensions/bytefly/working/register.ts +22 -27
  78. package/extensions/check/commands.ts +3 -2
  79. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  80. package/extensions/context/commands.ts +2 -2
  81. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  82. package/extensions/file-browse/index.ts +67 -36
  83. package/extensions/file-read/render.ts +4 -4
  84. package/extensions/goal/plan-route.ts +2 -2
  85. package/extensions/job/child.ts +527 -0
  86. package/extensions/job/clock.ts +142 -0
  87. package/extensions/job/commands.ts +4 -2
  88. package/extensions/job/config.ts +8 -1
  89. package/extensions/job/format.ts +22 -1
  90. package/extensions/job/log-writer.ts +13 -2
  91. package/extensions/job/registry.ts +63 -7
  92. package/extensions/job/render.ts +34 -0
  93. package/extensions/job/runtime.ts +296 -655
  94. package/extensions/job/session.ts +132 -19
  95. package/extensions/job/store.ts +7 -7
  96. package/extensions/job/tools.ts +3 -3
  97. package/extensions/job/types.ts +47 -0
  98. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
  99. package/extensions/job/waiters.ts +44 -1
  100. package/extensions/memory/commands.ts +4 -2
  101. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  102. package/extensions/plan/commands.ts +12 -9
  103. package/extensions/plan/format.ts +8 -2
  104. package/extensions/plan/git-bash.ts +115 -16
  105. package/extensions/plan/index.ts +53 -52
  106. package/extensions/plan/plan-index.ts +4 -2
  107. package/extensions/plan/plans.ts +5 -54
  108. package/extensions/plan/policy.ts +8 -10
  109. package/extensions/plan/review-lazy.ts +85 -0
  110. package/extensions/plan/review.ts +2 -2
  111. package/extensions/plan/runtime.ts +49 -39
  112. package/extensions/plan/sections.ts +1 -1
  113. package/extensions/plan/steps.ts +3 -3
  114. package/extensions/plan/todos.ts +1 -1
  115. package/extensions/plan/tools/cancel.ts +27 -62
  116. package/extensions/plan/tools/enter.ts +55 -81
  117. package/extensions/plan/tools/exit.ts +221 -238
  118. package/extensions/plan/tools/refusal.ts +18 -0
  119. package/extensions/plan/tools/steps.ts +75 -92
  120. package/extensions/plan/tools/write.ts +77 -120
  121. package/extensions/plan/tools.ts +188 -30
  122. package/extensions/plan/tui.ts +5 -5
  123. package/extensions/plan/types.ts +48 -5
  124. package/extensions/plan/web-client.ts +3 -0
  125. package/extensions/plan/web-lazy.ts +53 -0
  126. package/extensions/plan/web-markdown.ts +9 -67
  127. package/extensions/plan/web-payload.ts +3 -3
  128. package/extensions/plan/web-router.ts +18 -9
  129. package/extensions/plan/web.ts +1 -12
  130. package/extensions/question/tools.ts +6 -2
  131. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  132. package/extensions/rewind/commands/commands.ts +6 -3
  133. package/extensions/rewind/state/runtime.ts +1 -1
  134. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  135. package/extensions/rewind/view/review-tui.ts +4 -4
  136. package/extensions/serve/config.ts +10 -8
  137. package/extensions/serve/index.ts +17 -3
  138. package/extensions/serve/registry.ts +26 -17
  139. package/extensions/session-bus/commands.ts +11 -6
  140. package/extensions/session-bus/join-request.ts +2 -1
  141. package/extensions/session-bus/mail-watch.ts +26 -17
  142. package/extensions/session-bus/protocol.ts +15 -3
  143. package/extensions/session-bus/resolve.ts +5 -7
  144. package/extensions/session-bus/runtime.ts +40 -12
  145. package/extensions/session-bus/store.ts +101 -19
  146. package/extensions/session-bus/types.ts +19 -0
  147. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  148. package/extensions/subagent/agents.ts +1 -1
  149. package/extensions/subagent/commands.ts +4 -2
  150. package/extensions/subagent/format.ts +15 -15
  151. package/extensions/subagent/index.ts +1 -1
  152. package/extensions/subagent/render.ts +3 -3
  153. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  154. package/extensions/team/commands.ts +24 -7
  155. package/extensions/team/dispatch.ts +18 -4
  156. package/extensions/team/format.ts +17 -2
  157. package/extensions/team/index.ts +9 -8
  158. package/extensions/team/schema.ts +6 -5
  159. package/extensions/team/tools.ts +107 -45
  160. package/extensions/team/types.ts +3 -2
  161. package/extensions/todo/commands.ts +4 -2
  162. package/extensions/todo/format.ts +11 -0
  163. package/extensions/todo/render.ts +57 -0
  164. package/extensions/todo/runtime.ts +1 -1
  165. package/extensions/todo/tools.ts +1 -1
  166. package/extensions/todo/view/todo-screen.ts +122 -0
  167. package/extensions/todo/widget.ts +47 -0
  168. package/extensions/watch/commands.ts +4 -2
  169. package/extensions/watch/format.ts +36 -7
  170. package/extensions/watch/render.ts +57 -0
  171. package/extensions/watch/runtime.ts +27 -22
  172. package/extensions/watch/tools.ts +4 -4
  173. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  174. package/extensions/web-access/fetch/tool.ts +12 -4
  175. package/extensions/wechat/commands/commands.ts +18 -14
  176. package/extensions/wechat/index.ts +72 -37
  177. package/extensions/wechat/state/credentials.ts +1 -1
  178. package/extensions/wechat/state/state.ts +1 -1
  179. package/extensions/wechat/text/status.ts +112 -0
  180. package/extensions/wechat/types.ts +2 -2
  181. package/extensions/wechat/view/status-screen.ts +4 -103
  182. package/extensions/worktree/commands.ts +11 -9
  183. package/extensions/worktree/enter.ts +2 -2
  184. package/extensions/worktree/exit.ts +2 -2
  185. package/extensions/worktree/index.ts +5 -5
  186. package/extensions/worktree/screen.ts +3 -3
  187. package/extensions/worktree/status.ts +2 -2
  188. package/extensions/worktree/tools.ts +113 -116
  189. package/lib/README.md +27 -13
  190. package/lib/board/actions.ts +111 -0
  191. package/lib/board/artifact.ts +149 -0
  192. package/lib/board/card.ts +163 -52
  193. package/lib/board/config.ts +58 -0
  194. package/lib/board/contract.ts +113 -0
  195. package/lib/board/join.ts +38 -0
  196. package/lib/board/lanes.ts +182 -0
  197. package/lib/board/lead.ts +130 -0
  198. package/lib/board/limits.ts +78 -0
  199. package/lib/board/links.ts +31 -10
  200. package/lib/board/page-pointer.ts +1 -1
  201. package/lib/board/read.ts +83 -0
  202. package/lib/board/reason.ts +102 -0
  203. package/lib/board/round-text.ts +68 -6
  204. package/lib/board/round.ts +31 -15
  205. package/lib/board/rules.ts +151 -307
  206. package/lib/board/seat.ts +35 -0
  207. package/lib/board/session.ts +136 -54
  208. package/lib/board/skipped.ts +22 -0
  209. package/lib/board/steps.ts +487 -0
  210. package/lib/board/store.ts +87 -30
  211. package/lib/board/tone.ts +92 -0
  212. package/lib/board/view.ts +70 -58
  213. package/{extensions/watch → lib}/child.ts +40 -21
  214. package/lib/env.ts +65 -7
  215. package/lib/format.ts +13 -0
  216. package/lib/http/index.ts +14 -12
  217. package/lib/http/port.ts +15 -10
  218. package/lib/lazy.ts +45 -0
  219. package/lib/plan-entry.ts +39 -10
  220. package/lib/plans/paths.ts +82 -0
  221. package/lib/policy.ts +9 -6
  222. package/lib/report.ts +3 -2
  223. package/lib/requirements.ts +8 -8
  224. package/lib/session-label.ts +111 -5
  225. package/lib/session-roster.ts +43 -1
  226. package/lib/session-tool.ts +5 -4
  227. package/lib/stray-fields.ts +7 -2
  228. package/lib/tool-names.ts +5 -10
  229. package/lib/tui/README.md +23 -8
  230. package/lib/tui/chrome.ts +16 -15
  231. package/lib/tui/cursor.ts +18 -3
  232. package/lib/tui/glyphs.ts +9 -7
  233. package/lib/tui/tokens.ts +33 -2
  234. package/lib/tui/tool.ts +3 -3
  235. package/lib/web/README.md +21 -0
  236. package/lib/web/components.ts +5 -1
  237. package/lib/web/index.ts +1 -0
  238. package/lib/web/markdown.ts +83 -0
  239. package/lib/web/markup.ts +2 -1
  240. package/lib/web/runtime.ts +9 -10
  241. package/package.json +6 -1
  242. package/skills/init/SKILL.md +5 -5
  243. package/skills/next/SKILL.md +3 -5
  244. package/extensions/board/seat.ts +0 -28
  245. package/extensions/todo/tui.ts +0 -212
  246. package/lib/tui/index.ts +0 -34
  247. package/skills/project-docs/SKILL.md +0 -133
  248. package/skills/project-docs/references/audit.md +0 -131
  249. package/skills/project-docs/references/changelog.md +0 -105
  250. package/skills/project-docs/references/discovery.md +0 -127
  251. package/skills/project-docs/references/readme.md +0 -80
  252. package/skills/project-docs/references/release.md +0 -84
  253. package/skills/project-docs/references/roadmap.md +0 -93
  254. package/skills/project-docs/scripts/state.ts +0 -316
@@ -14,12 +14,16 @@
14
14
 
15
15
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
16
  import type { Static } from "typebox";
17
- import { type Card, 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";
@@ -27,17 +31,17 @@ import { failedResult, textResult } from "../../lib/tool-result.ts";
27
31
  import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
28
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 { joinOnClaim } from "./join.ts";
41
45
  import { collapsedLine, resultInput } from "./result.ts";
42
46
  import { createInput } from "./input.ts";
43
47
  import type { BoardSession } from "../../lib/board/session.ts";
@@ -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. */
@@ -127,14 +131,16 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
127
131
  label: "Board",
128
132
  description:
129
133
  "The shared feature list for this repository: one card per feature, which any session reads and writes. " +
130
- "A card carries the feature's outcome, the session that holds it, path hints, the plan it is executing, " +
131
- `its priority, what it waits for, and its notes. ${verbHelp()} A card's steps do not belong on the ` +
132
- "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`.",
133
138
  promptSnippet: `The shared feature list for this repository: ${BOARD_ACTIONS.join(", ")}.`,
134
139
  promptGuidelines: [
135
- "Claim a card before working on it; release or finish it when you stop, and reclaim a card whose owner is gone.",
136
- "Record the plan file with `board` action `plan` once the plan is approved, then execute it.",
137
- "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.",
138
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.",
139
145
  "A feature's steps belong in `todo`, not on the board: one card is one feature, whatever it takes.",
140
146
  "A `handoff` in your inbox is work you now own: read it with `session inbox`, record the plan, and start it.",
@@ -154,27 +160,35 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
154
160
  `unknown action "${String(args.action)}"; use ${BOARD_ACTIONS.join(", ")}.`,
155
161
  );
156
162
  }
157
- const me = runtime.who(ctx, ctx.cwd);
163
+ const me = await runtime.who(ctx, ctx.cwd);
158
164
  try {
159
- // Before the join and the store: a refused call has done nothing, so it cannot
160
- // 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.
161
167
  const stray = strayField(action, args, ACTION_FIELDS);
162
168
  if (stray !== undefined) return refusal(action, me, stray);
163
169
  const store = await runtime.store(ctx.cwd);
164
170
  const now = runtime.now();
165
171
  switch (action) {
166
172
  case "list":
167
- 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)));
168
177
  case "add":
169
178
  return await add(store, me, args, now);
170
179
  case "claim": {
171
- const claimed = await mutate(store, action, me, now, args, { kind: "claim" });
180
+ const claimed = await mutate(store, action, me, now, args, { kind: "claim" }, CLAIM_NEXT);
172
181
  // Taking a card is what makes this session a teammate. It is the one action here
173
182
  // worth being addressed about, and the board is the user's surface for the team, so
174
183
  // nobody has to know the bus exists. A refused claim wrote nothing and enlists
175
184
  // nobody; a refused join is not a failed claim, so the card stands and the next
176
185
  // claim asks again.
177
- if (!refused(claimed)) await joinOnClaim(runtime.team, team, ctx, me.label);
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
+ }
178
192
  return claimed;
179
193
  }
180
194
  case "release":
@@ -183,29 +197,47 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
183
197
  return await mutate(store, action, me, now, args, { kind: "reclaim" });
184
198
  case "move": {
185
199
  if (args.card === undefined) return refusal(action, me, 'card is required: pass the id, e.g. "4".');
186
- const status = args.status;
187
- if (status === undefined || status === "todo") {
188
- return refusal(
189
- action,
190
- me,
191
- 'move needs a target lane: "doing", "blocked", or "done". Use release to return a card to todo.',
192
- );
200
+ const lane = args.lane;
201
+ if (lane === undefined) {
202
+ return refusal(action, me, `move needs a target lane: ${listOf(movableLanes())}.`);
193
203
  }
194
- // Only the lane that has something to say reads for it: a move to `done`
195
- // records the commit that closed the card, and a board outside a
196
- // repository simply has none to record.
197
- 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;
198
213
  return await mutate(store, action, me, now, args, {
199
214
  kind: "move",
200
- status,
215
+ lane,
201
216
  ...(args.blockedBy === undefined ? {} : { blockedBy: args.blockedBy }),
217
+ ...(args.blocker === undefined ? {} : { blocker: args.blocker }),
202
218
  ...(landed === undefined ? {} : { landed }),
219
+ ...(proved === undefined ? {} : { artifact: proved }),
203
220
  });
204
221
  }
205
222
  case "note":
206
223
  return await mutate(store, action, me, now, args, { kind: "note", text: args.text });
207
- case "plan":
208
- 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 });
209
241
  case "edit": {
210
242
  if (
211
243
  args.title === undefined &&
@@ -259,6 +291,42 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
259
291
  });
260
292
  }
261
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
+
262
330
  /** `list`: the board, one card, the lead's rollup, or the archive. */
263
331
  async function read(
264
332
  store: BoardStore,
@@ -266,6 +334,7 @@ async function read(
266
334
  me: BoardDetails["me"],
267
335
  args: BoardArgs,
268
336
  now: number,
337
+ roster: () => Promise<readonly RosterSession[] | undefined>,
269
338
  ): Promise<ReturnType<typeof textResult<BoardDetails>>> {
270
339
  if (args.archived === true) return readArchive(store, action, me, args, now);
271
340
  const cards = await store.list();
@@ -275,6 +344,8 @@ async function read(
275
344
  // A directory listing, not a read: a listing says what it left aside without opening one.
276
345
  const archived = await store.archivedCount();
277
346
  if (args.card !== undefined) {
347
+ const ignored = cardFiltersRefusal(args);
348
+ if (ignored !== undefined) return refusal(action, me, ignored);
278
349
  const id = normalizeCardId(args.card);
279
350
  if (id === undefined) return refusal(action, me, `"${args.card}" is not a card id.`);
280
351
  const card = cards.find((entry) => entry.id === id);
@@ -296,7 +367,7 @@ async function read(
296
367
  }
297
368
  const listed = cards.filter(
298
369
  (card) =>
299
- (args.status === undefined || card.status === args.status) &&
370
+ (args.lane === undefined || card.lane === args.lane) &&
300
371
  (args.query === undefined || matchesQuery(card, args.query)) &&
301
372
  // The same rule a claim is checked against, so "mine" cannot list a card this
302
373
  // session could not release.
@@ -306,18 +377,23 @@ async function read(
306
377
  // repository's own board is mostly finished cards — so a listing that names no lane
307
378
  // leaves them out and says how many. A named lane, `card`, and `round` are unchanged;
308
379
  // the rollup counts the whole board, which is what makes it a rollup.
309
- const hiddenDone = args.status === undefined ? listed.filter((card) => card.status === "done").length : 0;
310
- 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");
311
382
  if (args.round !== true) {
312
- 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
+ });
313
389
  }
314
390
  const round = buildRound(cards, now);
315
391
  // One line under the rollup, because a reader who cannot see a card file should know why,
316
392
  // and one for the archive, because a rollup that silently left it out would be a board
317
393
  // smaller than the one on disk.
318
- const notes = [formatSkipped(skipped), archiveNote(archived)].filter((line) => line !== "");
319
- const text = [formatRound(cards, round), ...notes].join("\n");
320
- 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 } : {}) });
321
397
  }
322
398
 
323
399
  /**
@@ -334,7 +410,7 @@ async function readArchive(
334
410
  args: BoardArgs,
335
411
  now: number,
336
412
  ): Promise<ReturnType<typeof textResult<BoardDetails>>> {
337
- const wrong = (["status", "round"] as const).filter((field) => args[field] !== undefined);
413
+ const wrong = (["lane", "round"] as const).filter((field) => args[field] !== undefined);
338
414
  if (wrong.length > 0) {
339
415
  return refusal(
340
416
  action,
@@ -344,6 +420,9 @@ async function readArchive(
344
420
  }
345
421
  const pile = await store.archived();
346
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);
347
426
  const id = normalizeCardId(args.card);
348
427
  if (id === undefined) return refusal(action, me, `"${args.card}" is not a card id.`);
349
428
  const card = pile.cards.find((entry) => entry.id === id);
@@ -360,7 +439,12 @@ async function readArchive(
360
439
  const shown = pile.cards.filter(
361
440
  (card) => (args.query === undefined || matchesQuery(card, args.query)) && (args.mine !== true || holds(card, me)),
362
441
  );
363
- 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
+ });
364
448
  }
365
449
 
366
450
  /** `add`: the only action that creates a card. */
@@ -384,13 +468,16 @@ async function mutate(
384
468
  now: number,
385
469
  args: BoardArgs,
386
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,
387
473
  ): Promise<ReturnType<typeof textResult<BoardDetails>>> {
388
474
  if (args.card === undefined) return refusal(action, me, 'card is required: pass the id, e.g. "4".');
389
475
  const id = normalizeCardId(args.card);
390
476
  if (id === undefined) return refusal(action, me, `"${args.card}" is not a card id.`);
391
477
  const result = await store.mutate(id, op, me, now);
392
478
  if (!result.ok) return refusal(action, me, result.error, result.card);
393
- 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 });
394
481
  }
395
482
 
396
483
  /**
@@ -415,7 +502,7 @@ async function archiveCards(
415
502
  if (!result.changed) return textResult(`${cardRef(id)} is already archived.`, { action: "archive", me });
416
503
  return textResult(formatArchived([id]), { action: "archive", me, card: result.card, archived: [id] });
417
504
  }
418
- const finished = (await store.list()).filter((card) => card.status === "done");
505
+ const finished = (await store.list()).filter((card) => card.lane === "done");
419
506
  const moved: string[] = [];
420
507
  const failed: Array<{ card: string; error: string }> = [];
421
508
  for (const card of finished) {
@@ -10,53 +10,47 @@
10
10
  import type { Card } from "../../lib/board/card.ts";
11
11
  import type { Actor } from "../../lib/board/rules.ts";
12
12
  import type { RoundSummary } from "../../lib/board/round.ts";
13
+ import { BOARD_ACTIONS, type BoardAction } from "../../lib/board/actions.ts";
13
14
 
14
15
  /** The tool's name. One extension registers it, so it stays here. */
15
16
  export const BOARD_TOOL = "board";
16
17
 
17
- /** The actions the tool accepts, in the order the description lists them. */
18
- export const BOARD_ACTIONS = [
19
- "list",
20
- "add",
21
- "claim",
22
- "release",
23
- "reclaim",
24
- "move",
25
- "note",
26
- "plan",
27
- "edit",
28
- "archive",
29
- "drop",
30
- ] as const;
31
-
32
- /** One action. */
33
- export type BoardAction = (typeof BOARD_ACTIONS)[number];
18
+ // The actions are the board's, not the tool's: plan mode's read-only policy and the todo
19
+ // nudge classify a call by the same table, and nothing under `lib/` may import this
20
+ // file. Re-exported here so the schema, the description, and the suites keep one import
21
+ // for the tool's own vocabulary.
22
+ export { BOARD_ACTIONS, type BoardAction };
34
23
 
35
24
  /**
36
- * What each verb does, in one sentence.
25
+ * What each action does, in one sentence.
37
26
  *
38
27
  * The tool description is built from this table and the parameter's `action`
39
- * description points at that sentence, so the verbs are stated once. A new action is
28
+ * description points at that sentence, so the actions are stated once. A new action is
40
29
  * a row here and an entry in {@link BOARD_ACTIONS}; `test` holds the two together.
41
30
  */
42
31
  export const BOARD_ACTION_DOCS: Record<BoardAction, string> = {
43
32
  list:
44
- "shows the board — `status` filters one lane, `mine` only what this session holds, " +
33
+ "shows the board — `lane` filters one lane, `mine` only what this session holds, " +
45
34
  "`query` matches a title, a holder, a path hint, the plan, or a note's text, `card` reads that one card " +
46
- "instead of the board, and `round` adds the lead's rollup of owners, unassigned, unplanned, blocked, and " +
47
- "idle, which always covers the whole board — and a listing that names no lane leaves out the `done` cards, " +
35
+ "instead of the board, which refuses the fields that narrow a listing beside it, and `round` adds the lead's rollup of owners, unassigned, unplanned, awaiting approval, " +
36
+ "blocked, idle, and done, which always covers the whole board — and a listing that names no lane leaves out the `done` cards, " +
48
37
  "saying how many it left out",
49
- add: "creates a `todo` card",
50
- claim: "takes one for this session",
51
- release: "gives it back",
52
- reclaim: "takes it from whoever holds it",
53
- move: "changes lane",
38
+ add: "creates a `backlog` card",
39
+ claim: "pulls one into `plan` for this session, when `plan` has room",
40
+ release: "gives it back to `backlog`",
41
+ reclaim: "takes it from whoever holds it, back to `backlog`",
42
+ move: "changes lane, through the one transition the board allows from where the card is; a card enters `execute` or `done` only while the plan it recorded is still in the store",
54
43
  note: "appends to its history",
55
- plan: "records the plan file being executed",
44
+ contract:
45
+ "records the plan file and the three lines a card promises — `goal`, `acceptance`, `scopeOut` — and drops any approval, because the wording changed; `path` is required and must be the plan `plan` action `write` saved in this repository's plan store",
46
+ approve:
47
+ "approves the contract: the lead (`board.lead`, `lead` by default, the name the page serves as), who does not hold the card, says the wording is right, which is what `plan` to `execute` requires, and `text` records what the signer saw",
48
+ review:
49
+ "records a spot-check of the finished work: the lead, who does not hold the card, in `execute`, which is what `done` requires, and `text` records what the signer saw",
56
50
  edit: "fixes what the card says — title, path hints, priority, and what it waits for",
57
51
  archive:
58
52
  "sets a finished card aside, out of the board and still readable, or every finished card when no `card` is named",
59
- drop: "deletes a `todo`, a `done`, or one you hold",
53
+ drop: "deletes a `backlog` or a `done` card, or one you hold",
60
54
  };
61
55
 
62
56
  /**
@@ -71,14 +65,16 @@ export const BOARD_ACTION_DOCS: Record<BoardAction, string> = {
71
65
  * a new field cannot be published without an action that acts on it.
72
66
  */
73
67
  export const ACTION_FIELDS: Record<BoardAction, readonly string[]> = {
74
- list: ["card", "status", "mine", "query", "round", "archived"],
68
+ list: ["card", "lane", "mine", "query", "round", "archived"],
75
69
  add: ["title", "paths", "priority", "note"],
76
70
  claim: ["card"],
77
71
  release: ["card"],
78
72
  reclaim: ["card"],
79
- move: ["card", "status", "blockedBy"],
73
+ move: ["card", "lane", "blockedBy", "blocker"],
80
74
  note: ["card", "text"],
81
- plan: ["card", "path"],
75
+ contract: ["card", "path", "goal", "acceptance", "scopeOut"],
76
+ approve: ["card", "text"],
77
+ review: ["card", "text"],
82
78
  edit: ["card", "title", "paths", "priority", "dependsOn"],
83
79
  archive: ["card"],
84
80
  drop: ["card"],
@@ -89,6 +85,14 @@ export interface BoardDetails {
89
85
  action: BoardAction;
90
86
  /** The serving session: the id Pi knows, and the label a card records. */
91
87
  me: Actor;
88
+ /**
89
+ * Whether the read was narrowed to this session's own cards.
90
+ *
91
+ * Kept because the transcript says so: a listing that answers "what am I holding" draws
92
+ * those rows in the accent, and a listing that answers "what is on the board" does not
93
+ * pretend every row is the reader's.
94
+ */
95
+ mine?: boolean;
92
96
  /** The board a `list` read, in lane order. */
93
97
  cards?: Card[];
94
98
  /** The card an action produced, or the one a refusal left untouched. */