@gavin-hu/my-pi-agent 0.19.0 → 0.20.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 (158) hide show
  1. package/README.md +17 -11
  2. package/README.zh-CN.md +13 -8
  3. package/docs/extensions/README.md +1 -1
  4. package/docs/extensions/board.md +286 -123
  5. package/docs/extensions/bytefly.md +381 -223
  6. package/docs/extensions/file-browse.md +14 -7
  7. package/docs/extensions/job.md +1 -2
  8. package/docs/extensions/plan.md +56 -11
  9. package/docs/extensions/session-bus.md +23 -10
  10. package/docs/extensions/team.md +45 -17
  11. package/docs/extensions/wechat.md +2 -1
  12. package/docs/extensions/worktree.md +30 -15
  13. package/extensions/board/commands.ts +23 -5
  14. package/extensions/board/format.ts +107 -9
  15. package/extensions/board/join.ts +1 -1
  16. package/extensions/board/page.ts +170 -94
  17. package/extensions/board/result.ts +9 -0
  18. package/extensions/board/schema.ts +22 -7
  19. package/extensions/board/seat.ts +9 -10
  20. package/extensions/board/snapshot.ts +1 -0
  21. package/extensions/board/tools.ts +186 -21
  22. package/extensions/board/transport/router.ts +27 -108
  23. package/extensions/board/transport/server.ts +18 -29
  24. package/extensions/board/types.ts +67 -0
  25. package/extensions/board/view/client.ts +23 -13
  26. package/extensions/board/view/dialog.ts +43 -0
  27. package/extensions/board/view/keys.ts +28 -24
  28. package/extensions/board/view/live.ts +5 -171
  29. package/extensions/board/view/names.ts +91 -0
  30. package/extensions/board/view/page.ts +48 -88
  31. package/extensions/board/view/render.ts +213 -190
  32. package/extensions/board/view/styles.ts +65 -48
  33. package/extensions/board/view/team.ts +63 -41
  34. package/extensions/bytefly/footer/budget.ts +6 -5
  35. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  36. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  37. package/extensions/bytefly/footer/config.ts +19 -8
  38. package/extensions/bytefly/footer/price/price.ts +30 -8
  39. package/extensions/bytefly/footer/register.ts +5 -5
  40. package/extensions/bytefly/footer/render/format.ts +18 -0
  41. package/extensions/bytefly/footer/render/lines.ts +23 -7
  42. package/extensions/bytefly/footer/render/segments.ts +117 -31
  43. package/extensions/bytefly/footer/types.ts +29 -5
  44. package/extensions/bytefly/footer/unpriced.ts +42 -17
  45. package/extensions/bytefly/panel/commands.ts +28 -1
  46. package/extensions/bytefly/panel/filter.ts +68 -0
  47. package/extensions/bytefly/panel/format.ts +16 -15
  48. package/extensions/bytefly/panel/join.ts +56 -13
  49. package/extensions/bytefly/panel/prices.ts +43 -0
  50. package/extensions/bytefly/panel/settings.ts +72 -23
  51. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  52. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  53. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  54. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  55. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  56. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  57. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  58. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  59. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  60. package/extensions/bytefly/panel/tui.ts +306 -259
  61. package/extensions/check/commands.ts +2 -1
  62. package/extensions/file-browse/files/classify.ts +136 -0
  63. package/extensions/file-browse/files/files.ts +8 -547
  64. package/extensions/file-browse/files/index.ts +90 -0
  65. package/extensions/file-browse/files/list.ts +261 -0
  66. package/extensions/file-browse/files/tree.ts +101 -0
  67. package/extensions/file-browse/transport/router.ts +0 -2
  68. package/extensions/file-browse/view/emit.ts +51 -0
  69. package/extensions/file-browse/view/html.ts +6 -7
  70. package/extensions/file-browse/view/markdown.ts +11 -13
  71. package/extensions/file-browse/view/raw-html.ts +18 -13
  72. package/extensions/job/runtime.ts +4 -4
  73. package/extensions/plan/feedback.ts +40 -16
  74. package/extensions/plan/format.ts +10 -3
  75. package/extensions/plan/index.ts +17 -1
  76. package/extensions/plan/plan-files.ts +88 -0
  77. package/extensions/plan/plans.ts +19 -23
  78. package/extensions/plan/policy.ts +10 -5
  79. package/extensions/plan/runtime.ts +31 -2
  80. package/extensions/plan/todos.ts +51 -3
  81. package/extensions/plan/tools/exit.ts +10 -1
  82. package/extensions/plan/tools/todo-seed.ts +4 -2
  83. package/extensions/plan/tools/write.ts +12 -1
  84. package/extensions/plan/types.ts +15 -0
  85. package/extensions/plan/web-blocks.ts +89 -0
  86. package/extensions/plan/web-chrome.ts +3 -5
  87. package/extensions/plan/web-client.ts +179 -330
  88. package/extensions/plan/web-collab.ts +3 -3
  89. package/extensions/plan/web-comments.ts +18 -8
  90. package/extensions/plan/web-composer.ts +80 -0
  91. package/extensions/plan/web-draft.ts +40 -0
  92. package/extensions/plan/web-keys.ts +134 -0
  93. package/extensions/plan/web-outline.ts +192 -0
  94. package/extensions/plan/web-page.ts +4 -1
  95. package/extensions/plan/web-plan.ts +150 -0
  96. package/extensions/plan/web-router.ts +0 -1
  97. package/extensions/plan/web-settle.ts +194 -0
  98. package/extensions/rewind/commands/review.ts +2 -1
  99. package/extensions/session-bus/rows.ts +7 -3
  100. package/extensions/session-bus/runtime.ts +100 -38
  101. package/extensions/session-bus/store.ts +14 -2
  102. package/extensions/session-bus/tui.ts +1 -1
  103. package/extensions/session-bus/types.ts +10 -1
  104. package/extensions/subagent/commands.ts +2 -1
  105. package/extensions/team/commands.ts +41 -8
  106. package/extensions/team/format.ts +14 -0
  107. package/extensions/team/index.ts +2 -1
  108. package/extensions/team/tools.ts +30 -9
  109. package/extensions/team/types.ts +17 -0
  110. package/extensions/todo/index.ts +4 -1
  111. package/extensions/todo/nudge.ts +8 -2
  112. package/extensions/wechat/bridge.ts +43 -172
  113. package/extensions/wechat/intake.ts +245 -0
  114. package/extensions/worktree/commands.ts +24 -17
  115. package/extensions/worktree/exit.ts +12 -6
  116. package/extensions/worktree/git.ts +22 -14
  117. package/extensions/worktree/guard.ts +15 -285
  118. package/extensions/worktree/handoff.ts +10 -2
  119. package/extensions/worktree/index.ts +2 -1
  120. package/extensions/worktree/lifecycle.ts +2 -0
  121. package/extensions/worktree/registry.ts +20 -11
  122. package/extensions/worktree/root-tools.ts +13 -3
  123. package/extensions/worktree/runtime.ts +8 -2
  124. package/extensions/worktree/screen.ts +15 -8
  125. package/extensions/worktree/shell.ts +303 -0
  126. package/extensions/worktree/state.ts +1 -1
  127. package/extensions/worktree/status.ts +42 -9
  128. package/extensions/worktree/tools.ts +19 -13
  129. package/lib/README.md +6 -5
  130. package/lib/board/card.ts +192 -3
  131. package/lib/board/links.ts +26 -2
  132. package/lib/board/page-pointer.ts +86 -32
  133. package/lib/board/round-text.ts +11 -2
  134. package/lib/board/round.ts +25 -20
  135. package/lib/board/rules.ts +144 -11
  136. package/lib/board/store.ts +239 -52
  137. package/lib/board/view.ts +94 -10
  138. package/lib/format.ts +22 -0
  139. package/lib/http/serve.ts +3 -0
  140. package/lib/http/sink.ts +34 -9
  141. package/lib/plan-entry.ts +11 -0
  142. package/lib/policy.ts +14 -1
  143. package/lib/session-roster.ts +74 -3
  144. package/lib/spawn.ts +37 -3
  145. package/lib/stray-fields.ts +45 -0
  146. package/lib/tool-names.ts +13 -0
  147. package/lib/tui/glyphs.ts +2 -3
  148. package/lib/web/README.md +52 -2
  149. package/lib/web/client-runtime.ts +123 -0
  150. package/lib/web/index.ts +4 -2
  151. package/lib/web/markup.ts +88 -1
  152. package/lib/web/names.ts +42 -0
  153. package/package.json +1 -1
  154. package/extensions/board/view/actions.ts +0 -306
  155. package/extensions/board/writes.ts +0 -99
  156. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  157. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  158. package/extensions/job/process.ts +0 -35
@@ -93,7 +93,6 @@ selects the root.
93
93
  | `/events` | Server-Sent Events stream of `status` frames for the open page |
94
94
  | `/app.js` | the first-party client script |
95
95
  | `/theme.js` | the pre-paint theme bootstrap |
96
- | `/favicon.ico` | `204` (the icon is a data URI) |
97
96
 
98
97
  ## Configuration
99
98
 
@@ -171,7 +170,10 @@ global document then project document, project wins — are in
171
170
  ## The page
172
171
 
173
172
  The page is server-rendered HTML with an inline style block and inline SVG
174
- icons, progressively enhanced by one first-party script (`client.ts`). A folder
173
+ icons, progressively enhanced by one first-party script (`client.ts`). Each page
174
+ names its own subject in the tab — `browse · <folder>`, `browse · <rel>` for a file, the
175
+ `<page> · <subject>` shape the kit's `tabTitle` gives every served page — and links the
176
+ brand mark as an SVG data URI, in the appearance the system is in. A folder
175
177
  folds from its twisty, and a `fold all` / `expand all` button beside the filter
176
178
  folds the whole tree at once. The tree scrolls in its own pane with quick open
177
179
  and the filter pinned above it; below 860px it collapses into a `Files`
@@ -342,10 +344,10 @@ old and new sides for `lib/highlight`.
342
344
 
343
345
  While a server is running, the extension publishes a two-tone `◉ <port>` chip
344
346
  (a green circle-dot and a blue port) under the shared `browse` status key. The
345
- [`bytefly`](./bytefly.md) extension routes it to line 1's right zone
346
- beside `⎇ branch` and `⑂ worktree`; it is cleared on stop and on
347
- `session_shutdown`. The chip is session-local: it reflects only this session's
348
- server, with no cross-session sharing.
347
+ [`bytefly`](./bytefly.md) extension routes it to line 1's right zone, after
348
+ `⎇ branch` and `⑂ worktree` and in the same run as the board page's port chip;
349
+ it is cleared on stop and on `session_shutdown`. The chip is session-local: it
350
+ reflects only this session's server, with no cross-session sharing.
349
351
 
350
352
  The page has its own live status, which is what a left-open tab reads. A `◉ live`
351
353
  chip beside `read-only` says whether status is flowing (green), still
@@ -502,12 +504,17 @@ stops at the first difference because the control, not the checker, is the fix.
502
504
  | [`../../lib/escape.ts`](../../lib/escape.ts) | `escapeHtml`, the shared HTML escaping used by every renderer |
503
505
  | [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer for the code view (shared with `plan`) |
504
506
  | [`../../lib/web/index.ts`](../../lib/web/index.ts) | The shared Bytefly web brand system: tokens, base, components, layout, runtime, and markup |
505
- | `files/files.ts` | Classification (incl. Markdown), text reads, and one page-level walk (`directoryView`) that returns a directory's listing and its sidebar tree |
507
+ | `files/files.ts` | The barrel over the four reads: one surface for the callers, four modules behind it |
508
+ | `files/classify.ts` | What a name is: kind, Markdown, image, MIME type, and the text sample |
509
+ | `files/list.ts` | Capped directory listings and one file's bytes, over injected filesystem reads |
510
+ | `files/tree.ts` | The sidebar tree and the page-level walk (`directoryView`) that returns it with the listing |
511
+ | `files/index.ts` | The quick-open file index, walked once per root up to a cap |
506
512
  | `text/diff.ts` | Pure unified-diff and porcelain-status parsing for the diff view |
507
513
  | `text/fuzzy.ts` | Fuzzy path matching and ranking for the quick-open palette |
508
514
  | `view/markdown.ts` | First-party escape-first Markdown-to-HTML renderer (fenced code highlighted; raw HTML through `raw-html.ts`) |
509
515
  | `view/raw-html.ts` | Allowlisted raw-HTML sanitizer: element/attribute policy, suppressed bodies, and cross-block balancing |
510
516
  | `view/urls.ts` | Shared link/image URL policy: scheme allowlist and `/view`/`/raw` rewriting |
517
+ | `view/emit.ts` | The anchor and image elements both renderers emit, so a link written in Markdown and the same one written as HTML are one element |
511
518
  | `git/git.ts` | Read-only branch/dirty provider for the header chip |
512
519
  | `view/icons.ts` | Inline SVG sprite and the per-language mapping |
513
520
  | `view/client.ts` | The `/app.js` source: sort, wrap, copy, hunk navigation, reading progress, and the page keymap |
@@ -365,7 +365,7 @@ rewritten.
365
365
  cached from the stdout stream for `status`/`wait`/completion notes; only the
366
366
  `/jobs` log pane reads the file.
367
367
  - **Pure logic split from IO.** `format.ts` and the reconciliation in
368
- `registry.ts` are pure; `process.ts` and the runtime take injectable
368
+ `registry.ts` are pure; the runtime takes injectable
369
369
  spawn/liveness/kill/clock functions, so the whole suite runs without
370
370
  launching a process.
371
371
  - **The transcript reuses the `/jobs` grammar.** `job list` renders the same
@@ -388,7 +388,6 @@ rewritten.
388
388
  | `schema.ts` | Parameter schema and pure validation (a `JobCall` union). |
389
389
  | `config.ts` | The `job` section of `bytefly.json`: loading and clamping. |
390
390
  | `format.ts` | Pure formatting, duration, and transcript outcome-line helpers; sanitization is `lib/format.ts`'s `sanitizeUntrustedLine`. |
391
- | `process.ts` | The job-only process remainder: the `LivenessFn` seam and the `ps` start-time token. Spawn, tree-kill, and liveness itself come from `lib/spawn.ts`. |
392
391
  | `paths.ts` | Session/marker/registry/status file-name helpers. |
393
392
  | `registry.ts` | Per-session registry load/merge, legacy migration, and pure reconciliation. |
394
393
  | `reservations.ts` | Atomic `O_EXCL` job-id reservation and release. |
@@ -22,7 +22,7 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
22
22
  | One writable artifact | `write_plan` | The plan lands as markdown under the agent directory, one store per repository, shared by its worktrees. | Shipped |
23
23
  | Approval gate | `exit_plan_mode` | The plan is read back and presented; nothing is written until the user approves it. | Shipped |
24
24
  | Session goal at approval | `goal` at approval | A `planning` goal is promoted to the plan's objective, and a session with none gets it, without a second dialog; an active goal is left alone. | Shipped |
25
- | Steps as todos | `exit_plan_mode` | Approval seeds the plan's steps into the todo list, with the verification section as the last item. | Shipped |
25
+ | Steps as todos | `exit_plan_mode` | Approval seeds the plan's steps into the todo list, with the verification section as the last item; approving the same plan again replaces the items its earlier approval seeded. | Shipped |
26
26
  | Plan kept current | `set_plan_steps` | Ticking a step rewrites its checkbox in the file and its todo item in one call. | Shipped |
27
27
  | Stale-step protection | `expected` in `set_plan_steps` | A step whose number moved, is out of range, or no longer matches its text refuses the whole batch instead of ticking the wrong line. | Shipped |
28
28
  | Cancel without a plan | `cancel_plan_mode`, `/plan cancel` | Write access returns, and no todo items or goal are recorded. | Shipped |
@@ -36,7 +36,7 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
36
36
  | Capability | How | What you get | Status |
37
37
  |---|---|---|---|
38
38
  | Review screen | `exit_plan_mode` | A scrollable, themed plan with approve, refine, keep, browser, and exit in the footer. | Shipped |
39
- | Browser page | `/plan review`, `w` on the review screen | The plan opens in a local browser as a reading-first page with live section and step progress. | Shipped |
39
+ | Browser page | `/plan review`, `w` on the review screen | The plan opens in a local browser as a reading-first page with live section and step progress. | Partial |
40
40
  | Approval in the browser | the `/plan review` page's **Approve & run** | The plan is approved without returning to the terminal, and the steps are seeded. | Shipped |
41
41
  | Inline comments | the page's `+ Comment`, **Save feedback** | Comments are saved into the plan file as `> [!REVIEW]` lines and returned to the model. | Shipped |
42
42
  | Live page | the page's `events` stream, the plan-file poll | An edit made elsewhere repaints every open page without a reload. | Shipped |
@@ -140,7 +140,15 @@ the cap and the fix named, and nothing is written.
140
140
 
141
141
  The file is the source of truth. On approval the model follows the plan file's
142
142
  steps, and refinements rewrite the same file (`write_plan` with `plan_path`), so
143
- the reviewed artifact is the executed artifact.
143
+ the reviewed artifact is the executed artifact. The text a refinement replaces is
144
+ kept beside it, as a dotfile named for the plan and the revision
145
+ (`.2026-10-26-1008-add-rate-limiting-r001.md`), up to the newest five: that is
146
+ what makes two revisions diffable, and what lets a comment `carryFeedback` moved
147
+ onto another block be checked against the text the reviewer actually read. A
148
+ revision is a dotfile, so no scan lists it as a plan, no `plan_path` opens it, and
149
+ the review never offers it for approval — and a refinement that changed nothing
150
+ keeps nothing, because the copy would be the plan itself. The write's result names
151
+ the revision it kept.
144
152
 
145
153
  ### `exit_plan_mode`
146
154
 
@@ -171,6 +179,13 @@ the todo list:
171
179
  todos, and its verification bullets are already covered.
172
180
  - Each seeded item is matched back to its step by text, so ticking a step with
173
181
  `set_plan_steps` completes the item too.
182
+ - An approval records the step texts it seeded, per plan path, as a `plan-seeded`
183
+ entry. A plan refined after approval is approved again — `write_plan` needs plan
184
+ mode, so a second round is the way back — and that second approval **replaces the
185
+ leftovers its own earlier approval made**: an untouched item whose step was
186
+ renamed or dropped goes, while one somebody started or finished is kept and
187
+ named, because deleting it would erase work a reader did. `write_plan` says the
188
+ list is stale when it refines a plan that was approved before.
174
189
 
175
190
  **Goal-aware.** While planning, the injected context restates a pursued session
176
191
  goal as one bullet, so the plan is designed against the objective instead of
@@ -265,7 +280,8 @@ its plans hold and how old the oldest one is, both derived from the walk that ju
265
280
  happened. The directory stays the index: no registry file, nothing deletes a plan,
266
281
  and one bounded read per plan.
267
282
 
268
- `/plan review` is the only subcommand that takes an argument. Bare `/plan review`
283
+ `/plan review` is the only subcommand that takes a path; `/plan share` takes the
284
+ one word `stop` after it, and `list` and `cancel` take nothing. Bare `/plan review`
269
285
  reviews the plan this session wrote, or, in a later session, the newest plan file
270
286
  in the plans directory (newest modification time); `/plan review <path>.md` (or a
271
287
  path with a separator) reviews that plan. A line that merely starts with the word
@@ -531,8 +547,12 @@ line range it shows, and the footer lists only the keys that apply.
531
547
  <img src="../../assets/screens/plan-review.png" alt="The browser-based plan review: a sticky header with the plan title, a status chip, and the Approve & run, Save feedback, and Back to Pi actions, a Share action, a side panel with step progress and a section outline, and the plan as one measured column with a comment rail holding the owner's own comment and a teammate's @priya comment" width="100%">
532
548
 
533
549
  Press `w` on the review screen — or run `/plan review [path]` — to open the plan
534
- in the default browser on `127.0.0.1`. The page is a reading-first review
535
- surface: a sticky header carrying the plan's H1 (falling back to the file-name
550
+ in the default browser on `127.0.0.1`. Its tab reads `plan review · <the plan's
551
+ title>` — the page's name as the header's lockup says it, then the subject, the shape
552
+ `lib/web/markup.ts`'s `tabTitle` gives every served page — rendered with the document
553
+ rather than set by a script, so a strip of tabs names the plan before the page has
554
+ finished loading. The page is a
555
+ reading-first review surface: a sticky header carrying the plan's H1 (falling back to the file-name
536
556
  slug), a live status chip (connection, comment count, and unsaved changes), the
537
557
  theme toggle, and the review actions, over a hairline reading-progress bar. A
538
558
  sticky side panel shows the plan's `Steps` section progress and comment count above a
@@ -601,7 +621,9 @@ file and returns them to the model.
601
621
  `DOMParser`, so a code line is never parsed as markup.
602
622
  - **The file is the store.** Each comment is written as
603
623
  `> [!REVIEW] <comment>` on the line after the block it anchors to, tagged with
604
- its author when a teammate wrote it (`> [!REVIEW] @alice: <comment>`). Saving is
624
+ its author when a teammate wrote it (`> [!REVIEW] @alice: <comment>`), and
625
+ marked `> [!REVIEW moved]` when a refinement carried it off the text it was
626
+ written on. Saving is
605
627
  idempotent per writer: that writer's previous markers are replaced and everyone
606
628
  else's are kept, so two people reviewing the same plan cannot erase each other.
607
629
  Stripping is fence-aware: a `> [!REVIEW]` line inside a code block is
@@ -617,7 +639,11 @@ file and returns them to the model.
617
639
  comment whose section is gone too moves to the plan's last block rather than
618
640
  disappearing, and every one of those placements lands beside a block, never after
619
641
  a blank line, so the page keeps the thread under its block. Only a plan with no
620
- block at all leaves a comment in the unattached list.
642
+ block at all leaves a comment in the unattached list. Everything past the first
643
+ step is a guess, so a placement that is not the anchored text itself is marked:
644
+ the card carries a `moved` chip whose tooltip says the block changed and the
645
+ note was placed here, and the marker records it in the file, so the fact
646
+ survives a reload, a hand-off, and a save by its own author.
621
647
  - **Anchored to source.** `blocks.ts` splits the plan (fence-aware) into
622
648
  source-line ranges; a comment is placed after the whole anchored block, so a
623
649
  fenced code block is never split. A save re-checks the file against what the
@@ -705,6 +731,12 @@ both.
705
731
  Git Bash (`bash`), which must be installed for any shell command to run.
706
732
  - The read-only guarantee is a guard rail against accidental writes, not a
707
733
  security boundary (see [Security](#security)).
734
+ - **A carried comment's new home is a guess, and only the page says so.** A note a
735
+ refinement could not pin to surviving text is placed by wording overlap, by the
736
+ gap it left, or at the plan's last block; the `moved` chip is what tells a
737
+ reviewer that. A reader of the plan file alone sees `> [!REVIEW moved]`, and the
738
+ terminal review shows the comment as a quote with no chip, so the marker is the
739
+ fact and the chip is one reader's way of showing it.
708
740
  - The plans directory is the only index. `/plan list` walks it on every open and
709
741
  keeps no registry, so it sees an external edit or a hand-copied plan
710
742
  immediately; it lists a file over the 256 KiB plan limit without reading it,
@@ -897,7 +929,8 @@ both.
897
929
  | `types.ts` | `PlanModeEntry`, `EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `CancelPlanModeDetails`, `SetPlanStepsDetails`. |
898
930
  | `policy.ts` | Plan mode's read-only policy, the shared prompt summary, and the bash-refusal guidance. |
899
931
  | `git-bash.ts` | `checkReadOnlyGit`: the pure allowlist that restricts `bash` to read-only git commands. |
900
- | `plans.ts` | Plan-file store: slug/stamp naming, directory resolution (repository, active worktree, or agent directory), containment, atomic `write`/`setSteps`/`saveFeedback`, and `read`/`latest`. |
932
+ | `plans.ts` | Plan-file store: slug/stamp naming, directory resolution (repository, active worktree, or agent directory), containment, `write`/`setSteps`/`saveFeedback`, and `read`/`latest`. |
933
+ | `plan-files.ts` | How a plan file reaches the disk: `writeAtomic` (a dotfile temp, then a rename, so a crash cannot truncate the plan a review is reading) and `keepRevision`, which keeps the text a refinement replaces beside it, numbered, up to the newest five. |
901
934
  | `todos.ts` | Approval-time todo merge (pure): the last list replayed from the branch, and the merge of the plan's steps into it with the cap respected. |
902
935
  | `steps.ts` | Plan-document structure: the outline, step extraction and the `Steps`-section scope, the plan's own goal (`goalSection`), and the checkbox edit. |
903
936
  | `sections.ts` | The one fence-aware heading scan: `extractSections`, `sectionKeys` (the key that tells two identically titled sections apart), and the `HEADING` / `FENCE` / `normalizeHeadingText` vocabulary `blocks.ts` reads a heading through. |
@@ -920,11 +953,18 @@ both.
920
953
  | `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet, approve confirmation, share and name dialogs), its data island, and inline CSS. |
921
954
  | `web-share.ts` | Pure sharing rules: the reachable LAN addresses from an injected snapshot, one tokenized URL, the link table (mint, find, revoke, list, state) with its cap, and the reasons a share is unavailable. |
922
955
  | `web-share-session.ts` | The shared listener: its bound address, the link table over the session's tokens, `ShareState`, and invite/revoke/stop/close, injected with the request handler, role map, and frame hooks. |
923
- | `web-client.ts` | The `/client.js` source: state and wiring (comment rail, save/approve/back, keyboard nav, the stream's lifetime) over the three fragments below. |
956
+ | `web-keys.ts` | The client's keyboard as typed source (`WEB_KEYS_JS`): `handleKey` — Esc peels the composer before closing, a field and the shortcut sheet own their keys, and `s` is the write the page offers — and `rememberSelection`, which keeps the text a reader selected as the next comment's quote. |
957
+ | `web-settle.ts` | The client's settlement as typed source (`WEB_SETTLE_JS`): `submitAnnotations` — one POST shape whose 409 disables the writes, offers a reload and says the server's own sentence — and the four ways a review ends, of which only the owner's save, approve and close settle it. |
958
+ | `web-plan.ts` | The client's frame as typed source (`WEB_PLAN_JS`, `renderPlanFrame`): it draws every block and its comment rail, then puts the reader back — focus only when it was inside the plan, the active block in place rather than by scrolling. Reads arrive as getters, mutations leave as callbacks, so the bundle keeps every variable. |
959
+ | `web-composer.ts` | The client's composer lifecycle as typed source (`WEB_COMPOSER_JS`): `closeComposer` (over one `ComposerDeps` the client builds where the state is), `reopenComposer` for the draft a reload interrupted, and `commentOnActive`, which returns the index so the client keeps its one `activeIndex`. |
960
+ | `web-draft.ts` | The client's draft guard as typed source (`WEB_DRAFT_JS`): `draftTextarea` answers the open composer's textarea when it holds text, and `guardDraft` refuses the ways off the page, names the reason once, and puts the cursor back in it. |
961
+ | `web-blocks.ts` | The client's block navigation as typed source (`WEB_BLOCKS_JS`): `blockElements`, `setActive` and `moveActive` — which return the index they computed, so the client keeps the one `activeIndex` — and `blockAtLine`, which needs both the rendered block and its island entry. |
962
+ | `web-client.ts` | The `/client.js` source: the composition root, and nothing else by decision — the element handles, the state each rule is handed, one zero-argument adapter per rule, the listeners that attach them, the `EventSource` that keeps a tab's presence known, and the boot order. Every rule it used to hold lives in the `web-*.ts` modules below, where it can be typed and driven. |
924
963
  | `web-comments.ts` | The client's comment half: selection quotes, the composer, authored cards and who may edit them, delete-with-undo. |
925
964
  | `web-collab.ts` | The client's collaboration half: the link list with copy and per-link revoke, the reviewer's name, and the island refetch a stream frame triggers. |
926
965
  | `web-markdown.ts` | The client's document half: `safeHref` and the inline/table/code/list rendering (interpolated into `web-client.ts`). |
927
966
  | `web-chrome.ts` | The client's chrome half: the state chip, the status line, the header, and reading progress (interpolated into `web-client.ts`). |
967
+ | `web-outline.ts` | The client's outline as typed source (`WEB_OUTLINE_JS`): `sectionFor` — the last heading at or before a line — `renderOutline`, which builds the list and hands back the counters it made, `observeSections`, which watches the blocks and returns the one watcher the client keeps, and `renderFacts`, which paints the step progress and counts each comment under its heading. |
928
968
  | `path-guard.ts` | Recursive path-argument detection for the read-only backstop. |
929
969
  | [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform default-browser opener (shared with `file-browse`). |
930
970
  | [`../../lib/plan-entry.ts`](../../lib/plan-entry.ts) | Plan-mode entry shared with `goal`: the read-only summary, the consent sentence, and the `pi.events` request `plan` answers by enabling the mode. |
@@ -943,7 +983,7 @@ both.
943
983
  bun test extensions/plan
944
984
  ```
945
985
 
946
- Eleven co-located suites, declared in `test/suite-manifest.ts`:
986
+ Twelve co-located suites, declared in `test/suite-manifest.ts`:
947
987
 
948
988
  - `plans.test.ts` — the plan-file store: naming, directory resolution, atomic
949
989
  writes, `read`/`latest`/`index`, containment, `setSteps`, `saveFeedback`, and
@@ -958,6 +998,11 @@ Eleven co-located suites, declared in `test/suite-manifest.ts`:
958
998
  - `web.test.ts` — the browser review: the comment codec, the router, the page
959
999
  roles and page shell, the client script, the stream, the plan-file poll, the
960
1000
  share rules, and a real loopback session.
1001
+ - `web-behaviour.test.ts` — the served review, driven in a real browser through
1002
+ the router: the blocks and the plan's name, a block's comment control opening the
1003
+ composer with Escape peeling it before the review, a stored comment in its block's
1004
+ rail and the header's count, and the outline jumping to the section it names. It
1005
+ skips where no browser is installed, naming `PI_CHROME`.
961
1006
  - `web-share.test.ts` — the LAN address selector, `shareUrl`, the link table with
962
1007
  its cap, and the reasons a share is unavailable.
963
1008
  - `commands.test.ts` — `/plan review`, `/plan list`, `/plan share`, and the one
@@ -16,15 +16,15 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
16
16
 
17
17
  | Capability | How | What you get | Status |
18
18
  |---|---|---|---|
19
- | Membership | `session join [label]`, `/team join [<label>]`, the board's `w` | The join: this session is on the bus under a stable label peers address, and listens for mail unless told not to. A label given to `session join` or `/team join` is also Pi's session name, so the footer and `/resume` agree with the record; `w` joins under the name the bus derives and writes nothing to Pi. | Unreleased |
20
- | Peer discovery | `session status` | Every active session with its unread count and claims, plus this session's identity, in one read. | Shipped |
19
+ | Membership | `session join [label]`, `/team join [<label>]`, `/board`, any `board` action that writes, the board's `w` | The join: this session is on the bus under a stable label peers address, and listens for mail unless told not to. A label given to `session join` or `/team join` is also Pi's session name, so the footer and `/resume` agree with the record; `/board` joins as the page's seat (`team-lead`) or, on a page it is only watching, under its own label; the first `board` action that writes joins under the name the session already has; `w` joins under the name the bus derives and writes nothing to Pi. | Shipped |
20
+ | Peer discovery | `session status` | Every active session with its unread count and claims, plus this session's identity, in one read — and a session whose process is gone says so, its paths free from that read on. | Unreleased |
21
21
  | Live board | `/sessions` | Sessions, this session's unread mail, and every claim held on the repository, on one screen. | Shipped |
22
22
  | Board actions | `/sessions` (`w`, `x`) | Start or stop listening from the board, or release an own claim behind a confirmation, without leaving it. | Shipped |
23
23
  | Short messages | `session send` | One message per recipient, addressed by label or id, with a broadcast for events. | Shipped |
24
24
  | Message kinds | `session send` with `kind` | A `message`, `event`, `ask`, or `handoff` keeps its meaning; `ask` does not block the sender, and a `handoff` is named as the work it assigns. | Shipped |
25
25
  | Message paths | `session send` with `paths` | A message can name the repository paths it concerns, echoed in the inbox row. | Shipped |
26
26
  | Inbox on demand | `session inbox` | Up to `limit` messages, oldest first, consumed only when the model asks. | Shipped |
27
- | Listening | `session join` | New mail reported at the next turn; `join({wake: true})` raises one turn once the agent is idle, and `join({listen: false})` stops it. | Unreleased |
27
+ | Listening | `session join` | New mail reported at the next turn; `join({wake: true})` raises one turn once the agent is idle, and `join({listen: false})` stops it. The record carries whether this session listens, so a peer's status read answers it without asking. | Shipped |
28
28
  | Path claims | `session claim`, `session release` | A file or directory prefix held by one session at a time. | Shipped |
29
29
 
30
30
  ## Quickstart
@@ -55,12 +55,12 @@ collapsed line that expands to the board's rows.
55
55
 
56
56
  | Action | Inputs | Result |
57
57
  |---|---|---|
58
- | `status` | — | Active sessions with unread counts, claims, and this session's identity. Prunes stale records. A read: it never adds this session to the bus. |
58
+ | `status` | — | Active sessions with unread counts, their state, claims, and this session's identity. A session whose process is gone is listed as `gone` until its heartbeat ages out, and its claims are not listed at all. Prunes stale records. A read: it never adds this session to the bus. |
59
59
  | `send` | `to`, `text`, optional `kind`, optional `paths` | Writes one message per recipient. `to: "*"` is only valid with `kind: "event"`. |
60
60
  | `inbox` | optional `limit` | Returns and deletes up to `limit` messages, oldest first (default 20, max 50). |
61
61
  | `claim` | `paths` | Claims repository-relative files or directory prefixes; returns conflicts with their owner. |
62
62
  | `release` | `paths` | Removes claims owned by this session; reports paths owned by someone else. |
63
- | `join` | optional `label`, `wake`, `listen` | Joins the bus, names this session on it and in Pi when a label is given — the same path and uniqueness rule as `/team join`, so the footer and `/resume` agree with the record peers address — and starts listening for mail. `wake: true` raises one turn per burst once the agent is idle; `wake: false` reports at the next turn; `listen: false` joins without listening. A label a live session holds is refused with its owner named, and the refusal leaves both names and the watch alone. The result reports the label the bus settled on and whether mail will reach this session. |
63
+ | `join` | optional `label`, `wake`, `listen` | Joins the bus, names this session on it and in Pi when a label is given — the same path and uniqueness rule as `/team join`, so the footer and `/resume` agree with the record peers address — and starts listening for mail. `wake: true` raises one turn per burst once the agent is idle; `wake: false` reports at the next turn; `listen: false` joins without listening. A label a live session holds is refused, naming the session that holds it — its id, since the label it holds is the one you asked for — and the refusal leaves both names and the watch alone. The result reports the label the bus settled on and whether mail will reach this session. |
64
64
 
65
65
  A claim is all-or-nothing: if one path conflicts, nothing is claimed. A claim on
66
66
  a directory conflicts with claims below it and above it, so `src/api` and
@@ -167,8 +167,11 @@ listening watch is what raises the turn.
167
167
  session to the bus, so an unjoined session reads a board it is not on until it
168
168
  joins or acts.
169
169
  - **Membership is per session and session-only.** It survives neither a resume nor a
170
- new process; unread mail is previewed once more after `/resume`, and the record
171
- goes when the session stops.
170
+ new process: the record and every claim it held go when the session stops, and a
171
+ resumed pane is off the bus until it joins again. What does survive is everything
172
+ stored as a file — unread mail is previewed once more after `/resume`, and the
173
+ label a rejoining session takes is the same one, because it derives from Pi's
174
+ session name (`runtime.test.ts`, *a resume*).
172
175
  - **The board acts on your own state only.** `w` starts or stops listening and `x`
173
176
  releases a claim it holds. Mail is a preview, not a consume: the board never
174
177
  deletes a message, never marks one reported, and never touches a peer's inbox or
@@ -178,8 +181,18 @@ listening watch is what raises the turn.
178
181
  both sides to keep replying; the answer is to stop listening on one of them.
179
182
  - **No broadcast history.** A broadcast reaches the sessions active at send time;
180
183
  a session that joins later does not receive it.
181
- - **No forced claims.** A crashed owner blocks its paths until the session TTL
182
- expires. This is deliberate; stealing a claim is worse than waiting.
184
+ - **No forced claims.** There is no takeover while an owner can still answer: a
185
+ claim is released by its owner, or by the next bus call once the owner's process
186
+ is gone. A *hung* owner still blocks its paths until the session TTL expires,
187
+ because a running process is not evidence of a working one.
188
+ - **A dead process is named, not waited out.** A record carries its `pid` and, where
189
+ the host can name one, that process's start time. A pid that is no longer running,
190
+ or one whose start time is not the record's (a reused pid), reads `gone` at once:
191
+ its label is free, the paths it held are nobody's, and `session status` and
192
+ `/sessions` count it. It keeps its row until its heartbeat ages out, so a lead can
193
+ see what died and reclaim it. A record with no `pid` — and a platform without
194
+ `ps`, such as Windows — falls back to the heartbeat rather than being called dead
195
+ on no evidence.
183
196
  - **Bounded by TTL.** An idle session with a stopped heartbeat is pruned after
184
197
  ten minutes; its record, its claims, and an inbox directory left empty go with
185
198
  it, and its unread mail expires after 24 hours. A claim belongs to a record
@@ -202,7 +215,7 @@ listening watch is what raises the turn.
202
215
  | Tools | `session` — one action-shaped tool; `readOnlyHint: false`, `destructiveHint: true`, `openWorldHint: false`, `executionMode: "sequential"`. |
203
216
  | Commands | `/sessions` — the board in `tui`, the listing elsewhere; no arguments and no sub-commands. |
204
217
  | UI | `tui` mounts `SessionListComponent` through `withPolledScreen` (rails suppressed, a 1 s read-only poll of `snapshot()` + `peek()`); every other mode prints through `ctx.ui.notify`. No status chip, so listening never changes the footer. |
205
- | Events | `session_start` resolves the repository and arms the mail trigger, without joining; `before_agent_start` reports queued non-wake mail at the next turn, `agent_settled` retries a wake the run suppressed, and `context` prunes stale notices; `session_shutdown` clears the heartbeat, the watch ticker, and the record. The seven hooks are `lib/report.ts`'s `registerPendingChannel`, not a local copy. |
218
+ | Events | `session_start` resolves the repository and arms the mail trigger, without joining; `session_tree` only repaints, since navigating the tree changes no pending record; `agent_start` marks the run busy; `before_agent_start` reports queued non-wake mail at the next turn; `agent_settled` retries a wake the run suppressed; `context` prunes stale notices; `session_shutdown` clears the heartbeat, the watch ticker, and the record. The seven hooks are `lib/report.ts`'s `registerPendingChannel`, not a local copy. |
206
219
  | State | Files under `<agent-dir>/session-bus/<repo-key>/`; nothing in the session branch. Listening is session-only. `/resume` re-registers the same session id. |
207
220
  | Lifecycle | The factory only registers. The runtime starts on `session_start`; heartbeat, watch ticker, and cleanup belong to it. Disabling `session-bus` through `PI_DISABLED_EXTENSIONS` registers nothing. |
208
221
  | Mode | Uses `ctx.mode` to decide whether `/sessions` mounts the board and how a label is derived; nothing joins at start in any mode; `askHuman` is not involved and no status chip is published. |
@@ -2,14 +2,16 @@
2
2
 
3
3
  `board` owns the cards. `team` owns the lead's three actions: `assign` hands a card to a
4
4
  session that is **already running**, `offer` hands one to whoever is free, and `reclaim`
5
- takes one back when its owner has gone quiet. The first two are a write on the board and a
6
- message on the session bus; `reclaim` writes the card alone, so it needs no bus at all. The
7
- report says what the peer will actually do with it.
5
+ takes one back when its owner is gone or has gone quiet. `assign` writes the card and puts
6
+ a handoff on the session bus, `offer` writes only the moment it was made — never an owner,
7
+ because the first peer to claim is what makes an offer into work — and `reclaim` writes the
8
+ card alone, so it needs no bus.
9
+ The report says what the peer will actually do with it.
8
10
 
9
11
  Nothing starts a process. A message lands in the peer's inbox and is reported at its next
10
12
  turn, so `assign` and `offer` are messages, not launches.
11
13
 
12
- <img src="../../assets/screens/team.png" alt="The Team tab as a dashboard: on the left, a Needs a decision queue of rows with a coloured kind edge, the reason on its own line, and a Reclaim or Claim action on the row edge, or no action on a card that waits; on the right, a Teammates list with a state dot, label, you chip, mail badge, a not listening chip on a peer whose watch is off, and a line of what each holds; and one muted Done line under both" width="100%">
14
+ <img src="../../assets/screens/team.png" alt="The Team tab as a dashboard: on the left, a Needs a decision queue of rows with a coloured kind edge, the reason on its own line, and the owner as a chip on the row's right; on the right, a Teammates list with a state dot, label, you chip, mail badge, a not listening chip on a peer whose watch is off, and a line of what each holds; and one muted Done line under both" width="100%">
13
15
 
14
16
  *The Team tab: the cards that need the lead, then who is on the team — including an
15
17
  owner whose label nothing answers to, and a teammate that is not listening.*
@@ -21,11 +23,11 @@ owner whose label nothing answers to, and a teammate that is not listening.*
21
23
  | Join the team | `/board` as the lead, `/team join <label>` as a teammate, or any card you work | The session is on the team under a label peers address, listening, and `/team` and the Team tab list it |
22
24
  | Hand a feature to a running session | `team` action `assign` | The card is `doing`, owned by that session, and a handoff is in its inbox |
23
25
  | Tell the peer what to do first | `team` action `assign` with `note` | The handoff carries the card, its paths, its plan, and that line |
24
- | Hand a feature to whoever is free | `team` action `offer` | Every other live session gets an event naming the card; the card stays unowned until one claims it |
26
+ | Hand a feature to whoever is free | `team` action `offer` | Every other live session gets an event naming the card; the card stays unowned until one claims it, and it records when the offer was made, so a lead can tell an offer nobody took from a card nobody has looked at |
25
27
  | Know whether a message will be woken for | an `assign` or `offer` result, `/team`, and the Team tab | All three name the peers whose record says their next turn is when it lands: the result as `Not listening: <labels>`, `/team` as a lowercase `not listening:` inside its presence line, the Team tab as a chip |
26
28
  | Take a card back | `team` action `reclaim` | The owner clears, the card returns to To do, and its notes stay |
27
29
  | Keep the round to the team | `team` | Only `assign`, `offer`, and `reclaim`: no card is created, moved, planned, or finished here — that is `board`'s surface |
28
- | See the round without a browser | `/team` | `buildRound`'s summary — the one the page's Team tab is served — laid out as text by `formatRound`: what needs a decision, who holds what, then one presence line for who is live, who is not listening, and who has gone quiet. Ten lines at most, then `… N more`; an empty board is one line |
30
+ | See the round without a browser | `/team` | `buildRound`'s summary — the one the page's Team tab is served — laid out as text by `formatRound`: what needs a decision, who holds what, then one line naming the cards a dead owner left behind, and one presence line for who is live, who is not listening, who has gone quiet, and who is gone. The rollup itself is capped at ten lines, then `… N more`, and the gone and presence lines follow the cap rather than counting against it — the cap is a notify's limit, not a screen's. An empty board is one line |
29
31
 
30
32
  ## Quickstart
31
33
 
@@ -66,9 +68,10 @@ who is live but not listening. `/board status` repeats only what the last `/boar
66
68
  session did about the team — a join made by `/team join` or by a card write is not on that
67
69
  line, and a page that is not running prints no team clause at all.
68
70
 
69
- **The seat is not a session.** The page writes as `team-lead` with an id of its own, so a
70
- card it claimed comes back with `reclaim` rather than a `release` from the terminal. The
71
- label is shared with the session that serves the page; the ids are not.
71
+ **The seat is not a session.** The page is a view: it has one name, `team-lead`, and an id
72
+ of its own derived from it, and it writes nothing, so it holds no card for a lead to take
73
+ back. Its cards come from the session that serves it, whose label is the same and whose id
74
+ is not: a card that session claimed is the session's, and `release` returns it.
72
75
 
73
76
  ## Tool
74
77
 
@@ -78,12 +81,26 @@ label is shared with the session that serves the page; the ids are not.
78
81
  | Action | Parameters | Behaviour |
79
82
  |---|---|---|
80
83
  | `assign` | `card`, `to`, `note?` | Checks the card's preconditions, sends the handoff as a `handoff` message, then records the owner |
81
- | `offer` | `card`, `note?` | Checks the preconditions a claim would meet, broadcasts the card as an `event` to every other live session, and writes nothing |
84
+ | `offer` | `card`, `note?` | Checks the preconditions a claim would meet, broadcasts the card as an `event` to every other live session, then records the offer on the card — the moment, and nothing else: status and owner are untouched |
82
85
  | `reclaim` | `card` | Clears the owner and returns the card to To do, keeping its notes and plan |
83
86
 
84
87
  `card` takes `4` or `#4`. `to` is a teammate's label or id, as `/team` lists them — a
85
88
  label nobody answers to cannot be handed a card, and the result says so.
86
89
 
90
+ The parameters are one flat object, so a field is published to every action at once.
91
+ An action reads only the fields in its row above, and a call carrying one that its
92
+ action does not read is **refused**, naming the action that takes it — not ignored.
93
+ `to` is the one that matters: an `offer` that named a peer was broadcast to every live
94
+ session while the lead believed it had named one, and a `reclaim` that named one took
95
+ the card back from its holder anyway.
96
+
97
+ ```
98
+ offer does not act on to: `assign` takes it.
99
+ ```
100
+
101
+ `ACTION_FIELDS` in `types.ts` is the table that answers this, `lib/stray-fields.ts` is
102
+ the sentence, and a test holds every published parameter to an action that acts on it.
103
+
87
104
  ### The order is the point
88
105
 
89
106
  `assign` sends **before** it assigns, and each failure leaves a state the report names:
@@ -94,10 +111,15 @@ label nobody answers to cannot be handed a card, and the result says so.
94
111
  | the send | untouched — `todo`, no owner | `#4 was not handed off to billing-core: …`, plus `The card is unchanged.` |
95
112
  | the assignment, after a successful send | whatever the other writer left | `#4 was handed off to billing-core, but the card changed before it could be assigned: …`, and the peer is told to `board claim` it |
96
113
 
97
- `offer` has no order to get wrong, because it writes nothing: a refused broadcast leaves
98
- the card exactly as the rules just read it, and the first `board claim` is what turns an
99
- offer into work. Two peers claiming at once are settled by the store's compare-and-set —
100
- one claim wins, and the other is refused with the owner named.
114
+ `offer` broadcasts **before** it records, for the same reason: an offer nobody received
115
+ must not leave a record claiming one happened. A refused broadcast leaves the card exactly
116
+ as the rules read it, and the write that follows changes one field — when the offer was
117
+ made. If the card changed in between, the peers already have the message, so the refusal
118
+ says the offer stands as a message and the board is what it now says. The first `board
119
+ claim` is what turns an offer into work, and the claim is what clears the record: every
120
+ answer to an offer — a claim, an assignment, a release — takes it with it, so a record
121
+ here means the offer is still open. Two peers claiming at once are settled by the store's
122
+ compare-and-set; one claim wins, and the other is refused with the owner named.
101
123
 
102
124
  `delivered` in the details means a message file was written for that label — not that the
103
125
  peer read it. `notListening` names the peers it reached whose record says they are not
@@ -118,7 +140,11 @@ read it with `session inbox`, record the plan, and start. The card is already th
118
140
  moves it to `done` or `blocked`.
119
141
 
120
142
  An offer is advice: nothing is theirs until they claim it, and a card nobody claims stays
121
- `todo`, so it keeps showing up as unassigned in the round.
143
+ `todo`, so it keeps showing up as unassigned in the round — with its age, which is the one
144
+ thing an event cannot carry. `board` prints it as `offered 1m ago, and nobody took it`, the
145
+ card face says `offered 1m ago · nobody took it`, and the Team tab's queue says
146
+ `offered 1m ago — nobody took it` in place of "nobody holds it". A lead reading that has a
147
+ decision the plain unassigned row never offered: re-offer, `assign` it, or take it.
122
148
 
123
149
  ## Security
124
150
 
@@ -150,8 +176,10 @@ An offer is advice: nothing is theirs until they claim it, and a card nobody cla
150
176
  every ten seconds and the record is believed for ten minutes, so `quiet:` means the
151
177
  record went stale rather than that the peer died, while `not listening` is the separate
152
178
  `watching` field, written when the session joins or arms and disarms its watch.
153
- - **Plan mode makes this unavailable.** `session` is a mutating tool, so while plan mode is
154
- active the bus call is refused. Assign after the plan is approved.
179
+ - **Plan mode makes this unavailable.** Neither `team` nor `session` is a read-only tool, so
180
+ plan mode refuses both by name and tells the model to exit and get approval; the board
181
+ stays readable, since `list` is the one action it admits. Assign after the plan is
182
+ approved.
155
183
  - **Ownership is advisory.** `assign` does not create a path claim; the peer still claims
156
184
  the paths it edits through `session`.
157
185
 
@@ -594,7 +594,8 @@ status, which is what the old start/stop guidance used to do.
594
594
  | File | Responsibility |
595
595
  |---|---|
596
596
  | `index.ts` | Factory: gate, register the command and tool, wire the lifecycle events, build the bridge, open peer sessions from the host context. |
597
- | `bridge.ts` | The composition root: wires the poll loop, delivery, presence, admission, and inbound modules to the live client/account/cursor/state; owns the connection lifecycle, slash dispatch, the status snapshot, and `sendToOwner`. |
597
+ | `bridge.ts` | The composition root: wires the poll loop, delivery, presence, admission, and inbound modules to the live client/account/cursor/state; owns the connection lifecycle, the status snapshot, and `sendToOwner`. |
598
+ | `intake.ts` | The inbound path: who is refused, what is a replay, what is a command, what answers a pending prompt, and what is queued or turned away; the admission predicates live here too. |
598
599
  | `tools.ts` | The `send_wechat` tool: owner-only proactive text or one image, with the neither-or-both refusal. |
599
600
  | `config.ts` | The `wechat` section of `bytefly.json` loading and normalization. |
600
601
  | `types.ts` | Wire and domain types. |
@@ -65,7 +65,7 @@ four are `executionMode: "sequential"`, and only `list_worktrees` is read-only.
65
65
  | Tool | Usage |
66
66
  |---|---|
67
67
  | `enter_worktree` | `name` to create a worktree, or `path` to enter an existing one. Errors if already isolated. |
68
- | `exit_worktree` | optional `remove` and `keepBranch`. Returns the clean/keep/remove decision and a `handoff` (`branch`, `base`, `ahead`, `merged`). |
68
+ | `exit_worktree` | optional `remove` and `keepBranch`. Returns the clean/keep/remove decision and a `handoff` (`branch`, `base`, `ahead`, `commits`, `merged`). |
69
69
  | `prune_worktrees` | no parameters. Removes eligible managed worktrees and reports the rest. |
70
70
  | `list_worktrees` | no parameters. Read-only status: active root, inactive overrides, isolation gaps, managed worktrees. |
71
71
 
@@ -77,11 +77,14 @@ it.
77
77
  `exit_worktree` returns to the main checkout. A clean worktree is removed
78
78
  automatically; one with uncommitted changes or new commits prompts (or is kept
79
79
  in non-interactive mode). Its result ends with a **handoff**: the branch, how
80
- many commits it holds beyond the merge target, whether that target already
81
- contains them, and the `git merge --no-ff` and `gh pr create` commands that
82
- finish the task. The tool never commits, merges, pushes, or opens the PR. A
83
- branch that still holds work (uncommitted changes, or commits not reachable from
84
- the recorded base) is never force-deleted, even with `remove: true`.
80
+ many commits it holds beyond the merge target, how many it made since it was
81
+ created, whether that target already contains them, and the `git merge --no-ff`
82
+ and `gh pr create` commands that finish the task. A branch with no commits of its
83
+ own says so rather than claiming a merge — a worktree that never committed is an
84
+ ancestor of its base, and "merged" would read as work that landed. The tool
85
+ never commits, merges, pushes, or opens the PR. A branch that still holds work
86
+ (uncommitted changes, or commits not reachable from the recorded base) is never
87
+ force-deleted, even with `remove: true`.
85
88
 
86
89
  While isolated, `read`/`write`/`edit`/`bash`/`grep`/`find`/`ls` all resolve
87
90
  relative paths inside the worktree, and:
@@ -113,6 +116,12 @@ instead, so there is no gap to report. `unverifiable` appears only while
113
116
  reset at session start. `doctor` reports the `powershell` switch as `degraded`
114
117
  when it is off and notes the two structural gaps, which no setting closes.
115
118
 
119
+ Managed worktrees follow the header, one row each, as `path`, branch, and the
120
+ marks in brackets: `current`, `missing`, `dirty (2)`, `ahead 3`, `behind 1`, `no
121
+ commits`, `commits 4 merged`, `locked`, `used 2d ago`. A row with nothing else to
122
+ report says `clean` — the same word the `/worktree` screen prints, because both
123
+ read `statusMarks` rather than each deciding what an empty row means.
124
+
116
125
  ## Commands and flags
117
126
 
118
127
  | Command / flag | What it does |
@@ -136,10 +145,14 @@ being ignored.
136
145
  <img src="../../assets/screens/worktree.png" alt="The /worktree screen: a Worktrees title rule with a `4 worktrees · 1 dirty · 1 ahead` summary, the current dark-mode row carrying its ahead count, a dirty row, a merged row, and a stale-locked row, the focused row's path and base with the Not isolated gap under it, and the key hints" width="100%">
137
146
 
138
147
  `/worktree` on its own opens a list screen: one row per managed worktree with its
139
- state (current, dirty, ahead, behind, merged, locked, last used), the active
140
- worktree's base and isolation gaps under the focused row, and a detail pane for
141
- the selected one. Outside a terminal — print, RPC, JSON — the same facts print as
142
- text instead, which is also what the model's `list_worktrees` reads.
148
+ state (current, dirty, ahead, behind, commits, merged, locked, last used), the
149
+ active worktree's base and isolation gaps under the focused row, and a detail
150
+ pane for the selected one. A worktree that has not committed since it was created
151
+ says `no commits` instead of `merged`: it is an ancestor of its base, which git
152
+ alone cannot tell from a branch that landed. When work did land, the row says how
153
+ much — `commits 2 merged`. Outside a terminal — print, RPC, JSON — the same
154
+ facts print as text instead, which is also what the model's `list_worktrees`
155
+ reads.
143
156
 
144
157
  | Key | What it does |
145
158
  |---|---|
@@ -298,10 +311,11 @@ use, falling back to directory mtime for a worktree with no record.
298
311
  writes are guarded (relative paths are not isolated), and `list_worktrees`
299
312
  reports it. `skipOverrides` is read at load time, from the process working
300
313
  directory.
301
- - `git branch --merged main` calls a worktree branch merged whenever it matches
302
- its base, so a worktree that never committed looks landed while its
303
- uncommitted work is still there. Read `git -C <path> status` before removing
304
- one; `list_worktrees` reports the same clean/merged flags.
314
+ - The commit count behind `no commits` comes from the commit a worktree was
315
+ created at, which the registry records. A worktree this package did not create
316
+ has no fork point on record, so it falls back to what `git branch --merged`
317
+ says: for one of those, a worktree that never committed still reads as merged.
318
+ Read `git -C <path> status` before removing one.
305
319
  - The guard is best-effort at the shell level: command substitution, `eval`, and
306
320
  `sh -c` are only covered when `blockUnparsableCommands` is enabled, and real
307
321
  isolation of arbitrary shell writes needs an OS sandbox. `/worktree` names the
@@ -330,7 +344,7 @@ use, falling back to directory mtime for a worktree with no record.
330
344
  | Contract | Detail |
331
345
  |---|---|
332
346
  | Registration | The default export calls `registerWorktree`: root-tool overrides, the `--worktree` flag, lifecycle listeners, model tools, and the `/worktree` command. Nothing long-lived starts at load. |
333
- | Tools | `enter_worktree`, `exit_worktree`, `prune_worktrees`, `list_worktrees` via `registerTool`. All `executionMode: "sequential"`. Annotations: `enter_worktree` destructive + open-world; `exit_worktree`/`prune_worktrees` destructive; `list_worktrees` read-only. No `outputSchema`; structured state is returned in tool-result `details` — `exit_worktree` carries `{ state, removed, handoff }`, where `handoff` is `{ branch, base, ahead, merged }`. |
347
+ | Tools | `enter_worktree`, `exit_worktree`, `prune_worktrees`, `list_worktrees` via `registerTool`. All `executionMode: "sequential"`. Annotations: `enter_worktree` destructive + open-world; `exit_worktree`/`prune_worktrees` destructive; `list_worktrees` read-only. No `outputSchema`; structured state is returned in tool-result `details` — `exit_worktree` carries `{ state, removed, handoff }`, where `handoff` is `{ branch, base, ahead, commits, merged }` (`commits` absent when the worktree has no recorded base commit). |
334
348
  | Built-in overrides | `read`/`write`/`edit`/`bash`/`grep`/`find`/`ls` are re-registered under the same names with `defaultActive: false`; each spreads the built-in definition and replaces only `execute`. `skipOverrides` is applied at load time. `powershell` is deliberately not overridden: it cannot be re-rooted without a guard, so the `tool_call` handler refuses it (see `guard.blockPowershell`). |
335
349
  | Commands | `/worktree` via `registerCommand`, with argument completions for `enter`, `exit`, and `prune`. The bare command mounts `screen.ts` in a terminal and prints the status text otherwise; `enter`/`exit`/`prune` refuse while plan mode is read-only. |
336
350
  | Flags | `--worktree` via `registerFlag`. |
@@ -407,6 +421,7 @@ one read and two renderings.
407
421
  | `root-tools.ts` | Load-time built-in overrides bound to the active root |
408
422
  | `git.ts` | Git plumbing (shared runner + read helpers) |
409
423
  | `guard.ts` | Path, shell, and git-redirect isolation checks, plus the `powershell` refusal |
424
+ | `shell.ts` | The command-line reader those checks ask: words, segments, and redirection targets |
410
425
  | `gaps.ts` | The isolation gaps a session carries, and how they read (pure) |
411
426
  | `include.ts` | `.worktreeinclude` matcher and copy |
412
427
  | `config.ts` / `state.ts` / `status.ts` | Config, session entry shape, status text and the shared snapshot |