@gavin-hu/my-pi-agent 0.18.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.
- package/README.md +66 -52
- package/README.zh-CN.md +56 -46
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +507 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +561 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +298 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +254 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
- package/extensions/board/commands.ts +105 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +170 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +388 -0
- package/extensions/board/result.ts +108 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +90 -0
- package/extensions/board/seat.ts +28 -0
- package/extensions/board/snapshot.ts +42 -0
- package/extensions/board/tools.ts +444 -0
- package/extensions/board/transport/router.ts +91 -0
- package/extensions/board/transport/server.ts +138 -0
- package/extensions/board/types.ts +121 -0
- package/extensions/board/view/client.ts +51 -0
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +125 -0
- package/extensions/board/view/live.ts +231 -0
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +118 -0
- package/extensions/board/view/render.ts +519 -0
- package/extensions/board/view/styles.ts +228 -0
- package/extensions/board/view/team.ts +327 -0
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +67 -7
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +75 -43
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/check/commands.ts +2 -1
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +12 -0
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/job/runtime.ts +4 -4
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +185 -52
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +23 -5
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +27 -34
- package/extensions/plan/policy.ts +30 -17
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +52 -11
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +20 -95
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +15 -8
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -8
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +3 -58
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +11 -5
- package/extensions/session-bus/runtime.ts +220 -267
- package/extensions/session-bus/store.ts +55 -7
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +5 -5
- package/extensions/session-bus/types.ts +23 -14
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +145 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +119 -0
- package/extensions/team/index.ts +34 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +267 -0
- package/extensions/team/types.ts +65 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +106 -251
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +18 -290
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +11 -8
- package/lib/board/card.ts +479 -0
- package/lib/board/links.ts +141 -0
- package/lib/board/page-pointer.ts +310 -0
- package/lib/board/round-text.ts +81 -0
- package/lib/board/round.ts +94 -0
- package/lib/board/rules.ts +437 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +497 -0
- package/lib/board/view.ts +234 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +35 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +237 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +135 -0
- package/lib/http/token.ts +38 -0
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +25 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +201 -0
- package/lib/session-tool.ts +184 -0
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +38 -2
- package/lib/tui/glyphs.ts +3 -2
- package/lib/web/README.md +57 -4
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/components.ts +5 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/files.ts +0 -564
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/job/process.ts +0 -35
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -7,73 +7,124 @@ exit, with cleanup that refuses to throw work away. The model keeps calling
|
|
|
7
7
|
root changes. It is one of two sibling git extensions in this package
|
|
8
8
|
(`rewind` is the other).
|
|
9
9
|
|
|
10
|
+
## Capabilities
|
|
11
|
+
|
|
12
|
+
Status records what a reader can rely on: **Shipped** is in a tagged release, **Unreleased** is complete in the current tree but not yet tagged, and **Partial** has one released part and one unreleased part. Rows are ordered by user value, highest first.
|
|
13
|
+
|
|
14
|
+
### Isolating work
|
|
15
|
+
|
|
16
|
+
| Capability | How | What you get | Status |
|
|
17
|
+
|---|---|---|---|
|
|
18
|
+
| Isolated checkout | `enter_worktree`, `/worktree enter`, `--worktree` | A new checkout at `<repo>/.pi/worktrees/<name>` on branch `worktree-<name>`, cut from `origin/main`. | Shipped |
|
|
19
|
+
| Root-following built-ins | `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls` | The built-in tools keep taking ordinary relative paths, resolved inside the worktree. | Shipped |
|
|
20
|
+
| Escape refusal | `guard.blockFileEscapes`, `guard.blockGitRedirects`, `guard.blockSymlinkEscapes` | Writes, `cd`, and `git` redirects that reach the main checkout are refused, symlink escapes included. | Shipped |
|
|
21
|
+
| Plan-mode refusal | `/worktree`, `enter_worktree` | In plan mode the screen opens read-only and every verb refuses until the plan is approved. | Shipped |
|
|
22
|
+
| PowerShell refusal | `powershell` while isolated | The unparseable shell is refused instead of becoming an unguarded escape. | Shipped |
|
|
23
|
+
| Seeded working copy | `.worktreeinclude`, `include` | Gitignored files such as `.env` are copied into each new worktree. | Shipped |
|
|
24
|
+
| Ignore bootstrap | `enter_worktree` | The first worktree offers to add `.pi/worktrees` to `.gitignore`, so the checkout does not pollute status. | Shipped |
|
|
25
|
+
| PR/MR checkout | `enter_worktree` `name`, `--worktree` | A pull or merge request's head is fetched and checked out as `pr-<n>`. | Shipped |
|
|
26
|
+
| Child sessions | `PI_WORKTREE_ROOT`, `PI_WORKTREE_BRANCH`, `PI_WORKTREE_MAIN` | A `pi` started from a tool binds to the same worktree, read-only for lifecycle. | Shipped |
|
|
27
|
+
|
|
28
|
+
### Returning and managing
|
|
29
|
+
|
|
30
|
+
| Capability | How | What you get | Status |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| Return with a handoff | `exit_worktree`, `/worktree exit` | Back in the main checkout, with the branch, its commits, and the merge or PR command that finishes them. | Shipped |
|
|
33
|
+
| Work that survives | `exit_worktree`, `worktree.onExit` | A worktree holding changes or commits is asked about or kept, never force-deleted. | Shipped |
|
|
34
|
+
| Status inventory | `list_worktrees`, `/worktree` | The active root, the gaps isolation does not cover, and every managed worktree with its state. | Shipped |
|
|
35
|
+
| Worktree screen | `/worktree` | One list screen with the focused worktree's path, base, and gaps, and the verbs to act on it. | Shipped |
|
|
36
|
+
| Cleanup | `prune_worktrees`, `/worktree prune` | Clean, unused, old worktrees are removed; every other one is kept with a reason. | Shipped |
|
|
37
|
+
| Concurrent-session safety | `enter_worktree`, `prune_worktrees` | A worktree in use is locked, so another session's sweep cannot remove it. | Shipped |
|
|
38
|
+
| Session restore | `/resume`, `--continue` | An active worktree is restored from the branch, or reported gone. | Shipped |
|
|
39
|
+
| Extension signal | `worktree:changed` | Another extension follows the active root without polling. | Shipped |
|
|
40
|
+
| Doctor finding | `run_doctor` | A degraded `guard.blockPowershell` setting is named in the health report instead of only at use. | Shipped |
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
10
44
|
```bash
|
|
11
45
|
pi --extension ./extensions/worktree # load just this extension
|
|
12
46
|
pi -e . # load the whole @gavin-hu/my-pi-agent package
|
|
13
47
|
pi install ./ # install the package
|
|
14
|
-
pi -e . --worktree feature-auth # load the package and enter a worktree at startup
|
|
15
48
|
```
|
|
16
49
|
|
|
17
|
-
|
|
50
|
+
```text
|
|
51
|
+
/worktree # the list screen, or the same facts as text
|
|
52
|
+
```
|
|
18
53
|
|
|
19
|
-
|
|
54
|
+
The model enters with `enter_worktree`; `pi -e . --worktree feature-auth` starts
|
|
55
|
+
the session already isolated.
|
|
56
|
+
|
|
57
|
+
## Tools
|
|
20
58
|
|
|
21
59
|
<img src="../../assets/tools/list_worktrees.png" alt="The list_worktrees tool's call and result, drawn by the generic fallback" width="100%">
|
|
22
60
|
|
|
23
|
-
|
|
61
|
+
`enter_worktree` creates or enters a checkout, `exit_worktree` returns to the
|
|
62
|
+
main checkout, and `prune_worktrees` and `list_worktrees` manage and report. All
|
|
63
|
+
four are `executionMode: "sequential"`, and only `list_worktrees` is read-only.
|
|
24
64
|
|
|
25
|
-
|
|
65
|
+
| Tool | Usage |
|
|
66
|
+
|---|---|
|
|
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`, `commits`, `merged`). |
|
|
69
|
+
| `prune_worktrees` | no parameters. Removes eligible managed worktrees and reports the rest. |
|
|
70
|
+
| `list_worktrees` | no parameters. Read-only status: active root, inactive overrides, isolation gaps, managed worktrees. |
|
|
26
71
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
72
|
+
`enter_worktree` creates `<repo>/.pi/worktrees/<name>` on branch
|
|
73
|
+
`worktree-<name>` cut from `origin/main` (which can lag the local `main`), or
|
|
74
|
+
enters an existing worktree by `path`, and rebinds the session's working root to
|
|
75
|
+
it.
|
|
76
|
+
|
|
77
|
+
`exit_worktree` returns to the main checkout. A clean worktree is removed
|
|
78
|
+
automatically; one with uncommitted changes or new commits prompts (or is kept
|
|
79
|
+
in non-interactive mode). Its result ends with a **handoff**: the branch, how
|
|
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`.
|
|
88
|
+
|
|
89
|
+
While isolated, `read`/`write`/`edit`/`bash`/`grep`/`find`/`ls` all resolve
|
|
90
|
+
relative paths inside the worktree, and:
|
|
91
|
+
|
|
92
|
+
- writes/edits outside the worktree are refused, including through a symlink
|
|
93
|
+
that points at the main checkout;
|
|
94
|
+
- `git -C <main>`, `GIT_DIR=`, `GIT_WORK_TREE=`, `--git-dir=`, `--work-tree=`,
|
|
95
|
+
and `cd` outside the worktree are refused in `bash`;
|
|
96
|
+
- remote `git fetch` (the base ref and a PR/MR head) runs with terminal
|
|
97
|
+
prompting disabled, so a credentials prompt fails fast instead of hanging.
|
|
98
|
+
|
|
99
|
+
### Status text
|
|
32
100
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
| `g` / `G` | first / last row |
|
|
37
|
-
| Enter, `l` | open the selected worktree's detail |
|
|
38
|
-
| `e` | enter the selected worktree |
|
|
39
|
-
| `x` | exit the active worktree |
|
|
40
|
-
| `p` | prune eligible worktrees in place |
|
|
41
|
-
| Esc, `q` | close |
|
|
101
|
+
`list_worktrees` (and a headless `/worktree`) reports the active worktree, any
|
|
102
|
+
override that is not re-rooted, what isolation does not cover, and the managed
|
|
103
|
+
worktrees:
|
|
42
104
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
105
|
+
```text
|
|
106
|
+
Not isolated:
|
|
107
|
+
! shell runs in the main checkout; Pi has no mutable session cwd
|
|
108
|
+
powershell allowed by worktree.guard.blockPowershell: false; not re-rooted or guarded
|
|
109
|
+
unverifiable $(...), backticks, eval, and sh -c pass through (blockUnparsableCommands is off); 2 ran this session
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`powershell` appears only on Windows, with that tool active, and only when
|
|
113
|
+
`guard.blockPowershell` lets it through — by default the guard refuses the call
|
|
114
|
+
instead, so there is no gap to report. `unverifiable` appears only while
|
|
115
|
+
`guard.blockUnparsableCommands` is off; its trailing count is this session's,
|
|
116
|
+
reset at session start. `doctor` reports the `powershell` switch as `degraded`
|
|
117
|
+
when it is off and notes the two structural gaps, which no setting closes.
|
|
47
118
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
to it.
|
|
54
|
-
- `exit_worktree` returns to the main checkout. A clean worktree is removed
|
|
55
|
-
automatically; one with uncommitted changes or new commits prompts (or is
|
|
56
|
-
kept in non-interactive mode). Its result ends with a **handoff**: the branch,
|
|
57
|
-
how many commits it holds beyond the merge target, whether that target already
|
|
58
|
-
contains them, and the `git merge --no-ff` and `gh pr create` commands that
|
|
59
|
-
finish the task. The tool never commits, merges, pushes, or opens the PR. A
|
|
60
|
-
branch that still holds work (uncommitted
|
|
61
|
-
changes, or commits not reachable from the recorded base) is never
|
|
62
|
-
force-deleted, even with `remove: true`.
|
|
63
|
-
- While isolated, `read`/`write`/`edit`/`bash`/`grep`/`find`/`ls` all resolve
|
|
64
|
-
relative paths inside the worktree, and:
|
|
65
|
-
- writes/edits outside the worktree are refused, including through a symlink
|
|
66
|
-
that points at the main checkout;
|
|
67
|
-
- `git -C <main>`, `GIT_DIR=`, `GIT_WORK_TREE=`, `--git-dir=`, `--work-tree=`,
|
|
68
|
-
and `cd` outside the worktree are refused in `bash`;
|
|
69
|
-
- network `git fetch` runs with terminal prompting disabled, so a credentials
|
|
70
|
-
prompt fails fast instead of hanging.
|
|
71
|
-
- State is stored as a custom session entry, so `/resume` (and `--continue`)
|
|
72
|
-
restores an active worktree, or reports when it is gone.
|
|
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.
|
|
73
124
|
|
|
74
125
|
## Commands and flags
|
|
75
126
|
|
|
76
|
-
| | |
|
|
127
|
+
| Command / flag | What it does |
|
|
77
128
|
|---|---|
|
|
78
129
|
| `/worktree` | the list screen: managed worktrees, the active one, and its gaps |
|
|
79
130
|
| `/worktree enter [name]` | enter a worktree (a name may be a PR/MR reference) |
|
|
@@ -89,37 +140,42 @@ outside path, exiting a worktree with work) is reported as normal feedback, not
|
|
|
89
140
|
an error. An argument a subcommand does not take prints the usage line instead of
|
|
90
141
|
being ignored.
|
|
91
142
|
|
|
92
|
-
|
|
93
|
-
override that is not re-rooted, what isolation does not cover, and the managed
|
|
94
|
-
worktrees:
|
|
95
|
-
|
|
96
|
-
```text
|
|
97
|
-
Not isolated:
|
|
98
|
-
! shell runs in the main checkout; Pi has no mutable session cwd
|
|
99
|
-
powershell allowed by worktree.guard.blockPowershell: false; not re-rooted or guarded
|
|
100
|
-
unverifiable $(...), backticks, eval, and sh -c pass through (blockUnparsableCommands is off); 2 ran this session
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
`powershell` appears only on Windows, with that tool active, and only when
|
|
104
|
-
`guard.blockPowershell` lets it through — by default the guard refuses the call
|
|
105
|
-
instead, so there is no gap to report. `unverifiable` appears only while
|
|
106
|
-
`guard.blockUnparsableCommands` is off; its trailing count is this session's,
|
|
107
|
-
reset at session start. `doctor` reports the `powershell` switch as `degraded`
|
|
108
|
-
when it is off and notes the two structural gaps, which no setting closes.
|
|
143
|
+
### The /worktree screen
|
|
109
144
|
|
|
110
|
-
The
|
|
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%">
|
|
111
146
|
|
|
112
|
-
|
|
147
|
+
`/worktree` on its own opens a list screen: one row per managed worktree with its
|
|
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.
|
|
156
|
+
|
|
157
|
+
| Key | What it does |
|
|
113
158
|
|---|---|
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
159
|
+
| `j` / `k`, arrows | move |
|
|
160
|
+
| `g` / `G` | first / last row |
|
|
161
|
+
| Enter, `l` | open the selected worktree's detail |
|
|
162
|
+
| `e` | enter the selected worktree |
|
|
163
|
+
| `x` | exit the active worktree |
|
|
164
|
+
| `p` | prune eligible worktrees in place |
|
|
165
|
+
| Esc, `q` | close |
|
|
166
|
+
|
|
167
|
+
`e` and `x` close the screen before they act, because an exit asks through the
|
|
168
|
+
normal prompt and a prompt cannot open inside a mounted screen. `p` never asks,
|
|
169
|
+
so it prunes without closing and reports the result in the footer. In plan mode
|
|
170
|
+
the screen still opens and every verb refuses with a notice.
|
|
118
171
|
|
|
119
172
|
## Configuration
|
|
120
173
|
|
|
121
|
-
The `worktree` section of the shared `bytefly.json
|
|
122
|
-
|
|
174
|
+
The `worktree` section of the shared `bytefly.json`; the project document is read
|
|
175
|
+
from the repository root rather than the active working directory, and the
|
|
176
|
+
project wins. Resolution rules are in
|
|
177
|
+
[`../configuration.md#worktree--isolated-worktrees`](../configuration.md#worktree--isolated-worktrees),
|
|
178
|
+
which also lists every key with its default and accepted values.
|
|
123
179
|
|
|
124
180
|
```jsonc
|
|
125
181
|
// bytefly.json → "worktree"
|
|
@@ -255,10 +311,11 @@ use, falling back to directory mtime for a worktree with no record.
|
|
|
255
311
|
writes are guarded (relative paths are not isolated), and `list_worktrees`
|
|
256
312
|
reports it. `skipOverrides` is read at load time, from the process working
|
|
257
313
|
directory.
|
|
258
|
-
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
one
|
|
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.
|
|
262
319
|
- The guard is best-effort at the shell level: command substitution, `eval`, and
|
|
263
320
|
`sh -c` are only covered when `blockUnparsableCommands` is enabled, and real
|
|
264
321
|
isolation of arbitrary shell writes needs an OS sandbox. `/worktree` names the
|
|
@@ -287,7 +344,7 @@ use, falling back to directory mtime for a worktree with no record.
|
|
|
287
344
|
| Contract | Detail |
|
|
288
345
|
|---|---|
|
|
289
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. |
|
|
290
|
-
| 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). |
|
|
291
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`). |
|
|
292
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. |
|
|
293
350
|
| Flags | `--worktree` via `registerFlag`. |
|
|
@@ -364,6 +421,7 @@ one read and two renderings.
|
|
|
364
421
|
| `root-tools.ts` | Load-time built-in overrides bound to the active root |
|
|
365
422
|
| `git.ts` | Git plumbing (shared runner + read helpers) |
|
|
366
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 |
|
|
367
425
|
| `gaps.ts` | The isolation gaps a session carries, and how they read (pure) |
|
|
368
426
|
| `include.ts` | `.worktreeinclude` matcher and copy |
|
|
369
427
|
| `config.ts` / `state.ts` / `status.ts` | Config, session entry shape, status text and the shared snapshot |
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/board` — the page's one entry point.
|
|
3
|
+
*
|
|
4
|
+
* Three words, and everything else opens the page: `team` opens it on the Team tab,
|
|
5
|
+
* `status` prints one line, `stop` closes it. The command is a thin parser over the
|
|
6
|
+
* lifecycle in `page.ts`, so the idle grace and `session_shutdown` close the same
|
|
7
|
+
* server this opens.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { plural } from "../../lib/board/round-text.ts";
|
|
12
|
+
import type { JoinResult } from "../../lib/session-tool.ts";
|
|
13
|
+
import { createTeamJoin, type TeamJoin } from "../../lib/session-tool.ts";
|
|
14
|
+
import { joinLabel, teamLine } from "./join.ts";
|
|
15
|
+
import type { BoardPage, BoardPageStatus } from "./page.ts";
|
|
16
|
+
|
|
17
|
+
/** The command's name, which is also the word a user types. */
|
|
18
|
+
export const BOARD_COMMAND = "board";
|
|
19
|
+
|
|
20
|
+
/** The words the command accepts, in the order a user is likely to want them. */
|
|
21
|
+
export const BOARD_WORDS = ["status", "stop", "team"] as const;
|
|
22
|
+
|
|
23
|
+
export function registerCommands(pi: ExtensionAPI, page: BoardPage, deps: { team?: TeamJoin } = {}): void {
|
|
24
|
+
const team = deps.team ?? createTeamJoin(pi);
|
|
25
|
+
/** What the last `/board` did about the team, for the status line. */
|
|
26
|
+
let joined: JoinResult | undefined;
|
|
27
|
+
pi.registerCommand(BOARD_COMMAND, {
|
|
28
|
+
description: "Open the shared board in a local browser: /board [team | status | stop]",
|
|
29
|
+
getArgumentCompletions: (prefix: string) => {
|
|
30
|
+
const text = prefix.trimStart();
|
|
31
|
+
if (text.includes(" ")) return null;
|
|
32
|
+
const items = BOARD_WORDS.filter((word) => word.startsWith(text));
|
|
33
|
+
return items.length > 0 ? items.map((value) => ({ value, label: value })) : null;
|
|
34
|
+
},
|
|
35
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
36
|
+
const word = args.trim().toLowerCase();
|
|
37
|
+
if (word === "status") {
|
|
38
|
+
ctx.ui.notify(statusLine(await page.status(), joined), "info");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (word === "stop") {
|
|
42
|
+
const status = await page.status();
|
|
43
|
+
// A note nobody answers is not a page to leave for the next session to trip over:
|
|
44
|
+
// it is dropped here, and the line says whose it was.
|
|
45
|
+
if (status.stale === true) {
|
|
46
|
+
const dropped = await page.dropStale(ctx);
|
|
47
|
+
ctx.ui.notify(
|
|
48
|
+
dropped
|
|
49
|
+
? `board: dropped ${status.seat}'s note for :${status.port} — it answered nobody`
|
|
50
|
+
: `board: nothing to stop here — ${status.seat} is answering on :${status.port}`,
|
|
51
|
+
"info",
|
|
52
|
+
);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// A page another session serves is that session's to stop, and saying whose it is is the answer.
|
|
56
|
+
if (status.attached === true) {
|
|
57
|
+
ctx.ui.notify(
|
|
58
|
+
`board: nothing to stop here — ${status.seat} serves this repository on :${status.port}`,
|
|
59
|
+
"info",
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
ctx.ui.notify((await page.stop(ctx)) ? "board: stopped" : "board: not running", "info");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (word !== "" && word !== "team") {
|
|
67
|
+
ctx.ui.notify(
|
|
68
|
+
`board: "${word}" is not a word this command knows — try /board, /board team, /board status, /board stop`,
|
|
69
|
+
"warning",
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const started = await page.start(ctx, word === "team" ? "team" : "board");
|
|
74
|
+
// A failed start has already said why. Otherwise the join happens here, because
|
|
75
|
+
// opening the page is the lead's one action: the seat when this session serves it,
|
|
76
|
+
// this session's own label when it watches a page somebody else serves.
|
|
77
|
+
if (started.kind === "failed") return;
|
|
78
|
+
joined = await team.join(ctx, joinLabel(started.kind === "served"));
|
|
79
|
+
// The address is on the line because the browser is not the only way in: a machine
|
|
80
|
+
// with no default browser prints it here, and a tab closed by mistake is one
|
|
81
|
+
// command from being opened again.
|
|
82
|
+
ctx.ui.notify(
|
|
83
|
+
started.kind === "attached"
|
|
84
|
+
? `board: ${started.seat} is serving this repository on :${started.port} — viewing · open ${started.url}${teamLine(joined)}`
|
|
85
|
+
: `board: ${started.seat} serving ${started.where} on :${started.port} · open ${started.url}${teamLine(joined)}`,
|
|
86
|
+
"info",
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** One line for `/board status`: the page this session serves, watches, or does not have. */
|
|
93
|
+
export function statusLine(status: BoardPageStatus, team?: JoinResult): string {
|
|
94
|
+
if (status.running) {
|
|
95
|
+
const watching = plural(status.watchers ?? 0, "tab");
|
|
96
|
+
return `board: ${status.seat} serving on :${status.port} · ${watching} watching · ${status.where}${teamLine(team)}`;
|
|
97
|
+
}
|
|
98
|
+
if (status.stale === true) {
|
|
99
|
+
return `board: watching ${status.seat} on :${status.port} — that note answers nobody; /board stop drops it${teamLine(team)}`;
|
|
100
|
+
}
|
|
101
|
+
if (status.attached === true) {
|
|
102
|
+
return `board: watching ${status.seat} on :${status.port} — nothing to stop here${teamLine(team)}`;
|
|
103
|
+
}
|
|
104
|
+
return "board: not running — /board opens it";
|
|
105
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board's one setting: the port its page is pinned to.
|
|
3
|
+
*
|
|
4
|
+
* The `board` section of the shared `bytefly.json` (global then project, project
|
|
5
|
+
* wins). `0` — the default — takes one free port for the session and remembers it,
|
|
6
|
+
* which is what a session that never bookmarks its URL wants; a positive value pins
|
|
7
|
+
* the page, so a bookmarked URL survives a restart. A pinned port that is taken
|
|
8
|
+
* fails the start rather than moving the page out from under the bookmark.
|
|
9
|
+
*
|
|
10
|
+
* Pin it in the project document when more than one repository runs a board: a
|
|
11
|
+
* global value is one port for every board on the machine.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { clampInteger, loadConfigSection } from "../../lib/config.ts";
|
|
15
|
+
|
|
16
|
+
/** The `board` section. */
|
|
17
|
+
export interface BoardConfig {
|
|
18
|
+
/** TCP port on `127.0.0.1`; `0` picks one for the session, a positive value pins it. */
|
|
19
|
+
port: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_BOARD_CONFIG: BoardConfig = { port: 0 };
|
|
23
|
+
|
|
24
|
+
/** Validate and clamp a raw config object over the defaults. */
|
|
25
|
+
export function normalizeBoardConfig(
|
|
26
|
+
raw: Record<string, unknown> | undefined,
|
|
27
|
+
base: BoardConfig = DEFAULT_BOARD_CONFIG,
|
|
28
|
+
): BoardConfig {
|
|
29
|
+
if (!raw) return base;
|
|
30
|
+
return { port: clampInteger(raw.port, base.port, 0, 65_535) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Load the effective config for a working directory. */
|
|
34
|
+
export function loadBoardConfig(cwd: string): BoardConfig {
|
|
35
|
+
return loadConfigSection(cwd, "board", DEFAULT_BOARD_CONFIG, normalizeBoardConfig);
|
|
36
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model-facing text for one card or one listing.
|
|
3
|
+
*
|
|
4
|
+
* Text is the model's channel, so it carries the facts a next decision needs — the
|
|
5
|
+
* id, the lane, who holds it, whether it is planned, how urgent it is, and what it
|
|
6
|
+
* waits for — and the renderer carries the styling. Ages and sizes stay out: a note is
|
|
7
|
+
* read for what it says, and the page is where "3m ago" belongs.
|
|
8
|
+
*
|
|
9
|
+
* `board` is optional because a single card's answer sometimes comes without the rest
|
|
10
|
+
* of the board: the ids are still named, and only the lane of each dependency is lost.
|
|
11
|
+
* The rollup's text is `lib/board/round-text.ts`, because `team` prints it too.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { type Card, cardRef, DEFAULT_PRIORITY, dependsOf, priorityOf } from "../../lib/board/card.ts";
|
|
15
|
+
import { type BoardIndex, lanesText, openDependentsOf } from "../../lib/board/links.ts";
|
|
16
|
+
import type { Skip } from "../../lib/board/store.ts";
|
|
17
|
+
import { formatAge } from "../../lib/format.ts";
|
|
18
|
+
import { plural } from "../../lib/board/round-text.ts";
|
|
19
|
+
|
|
20
|
+
/** One card: the headline, then the state a reader needs in order to act on it. */
|
|
21
|
+
export function formatCard(card: Card, board?: BoardIndex, now?: number): string {
|
|
22
|
+
return [`${cardRef(card.id)} ${card.status} · ${card.title}`, ...detailLines(card, board, now)].join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What a listing cannot show, and therefore has to say.
|
|
27
|
+
*
|
|
28
|
+
* A footnote is the difference between a listing and the board: an omission nobody
|
|
29
|
+
* accounted for reads as a smaller board — or as history that was deleted.
|
|
30
|
+
*/
|
|
31
|
+
export interface ListingNotes {
|
|
32
|
+
/** Finished cards the board holds that a lane-less listing left out. */
|
|
33
|
+
hiddenDone: number;
|
|
34
|
+
/** Cards set aside in the archive, which no board read opens. */
|
|
35
|
+
archived: number;
|
|
36
|
+
/** Card files this build could not read. */
|
|
37
|
+
skipped: readonly Skip[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A board's listing: the cards, then the footnotes a reader cannot see.
|
|
42
|
+
*
|
|
43
|
+
* One body, because the footnotes belong to the listing: a card file this build cannot
|
|
44
|
+
* read, the finished cards a plain listing left out, and the cards an `archive` set aside.
|
|
45
|
+
*/
|
|
46
|
+
export function formatBoard(shown: readonly Card[], notes: ListingNotes): string {
|
|
47
|
+
const lines =
|
|
48
|
+
shown.length === 0 ? [notes.hiddenDone === 0 ? "The board is empty." : "No open cards."] : shown.map(listLine);
|
|
49
|
+
// A finished card is history rather than work, so a listing that names no lane leaves
|
|
50
|
+
// them out; the count is what keeps the omission from reading as a smaller board.
|
|
51
|
+
if (notes.hiddenDone > 0) {
|
|
52
|
+
const them = notes.hiddenDone === 1 ? "it" : "them";
|
|
53
|
+
lines.push(`${plural(notes.hiddenDone, "done card")} hidden; pass status: "done" to read ${them}.`);
|
|
54
|
+
}
|
|
55
|
+
for (const note of [archiveNote(notes.archived), formatSkipped(notes.skipped)]) {
|
|
56
|
+
if (note !== "") lines.push(note);
|
|
57
|
+
}
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The archive's listing: what is set aside, and the files there this build refused.
|
|
63
|
+
*
|
|
64
|
+
* The archive is read on request only, so this has no lane footnote of its own — every
|
|
65
|
+
* card in the pile is finished, and a `done` lane would be a second way to say it.
|
|
66
|
+
*/
|
|
67
|
+
export function formatArchive(shown: readonly Card[], skipped: readonly Skip[]): string {
|
|
68
|
+
const lines = shown.length === 0 ? ["The archive is empty."] : shown.map(listLine);
|
|
69
|
+
const unreadable = formatSkipped(skipped);
|
|
70
|
+
if (unreadable !== "") lines.push(unreadable);
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The one sentence a board listing adds so its archive is not mistaken for deleted history.
|
|
76
|
+
*
|
|
77
|
+
* Empty when nothing is set aside, so the ordinary board says nothing new.
|
|
78
|
+
*/
|
|
79
|
+
export function archiveNote(count: number): string {
|
|
80
|
+
if (count === 0) return "";
|
|
81
|
+
return `${plural(count, "card")} ${count === 1 ? "is" : "are"} set aside in the archive; pass archived: true to read ${count === 1 ? "it" : "them"}.`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The files the board could not read, or nothing when it read everything.
|
|
86
|
+
*
|
|
87
|
+
* A skipped file is not an absent one: a board written by a newer build would
|
|
88
|
+
* otherwise read as "nobody has planned anything" while the work sits on disk.
|
|
89
|
+
*/
|
|
90
|
+
export function formatSkipped(skipped: readonly Skip[]): string {
|
|
91
|
+
if (skipped.length === 0) return "";
|
|
92
|
+
const files = skipped.map((entry) => `${entry.file} (${entry.why})`).join(", ");
|
|
93
|
+
return `${plural(skipped.length, "file")} this build cannot read: ${files}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The dropped id, which is the only thing left of the card. */
|
|
97
|
+
export function formatDropped(id: string): string {
|
|
98
|
+
return `${cardRef(id)} dropped, with its notes.`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The ids an archive set aside, and how to read them again.
|
|
103
|
+
*
|
|
104
|
+
* A move has to name the way back or it reads like a deletion: the card is out of the
|
|
105
|
+
* board and its notes, plan, and landed commit are still in its file.
|
|
106
|
+
*/
|
|
107
|
+
export function formatArchived(ids: readonly string[]): string {
|
|
108
|
+
const theirs = ids.length === 1 ? "its" : "their";
|
|
109
|
+
return `${plural(ids.length, "card")} archived: ${ids.map(cardRef).join(", ")} — out of the board, ${theirs} notes and plan still readable with archived: true.`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A one-line listing: the id, the lane, the title, and who holds it. */
|
|
113
|
+
function listLine(card: Card): string {
|
|
114
|
+
const holder = card.owner === undefined ? "unassigned" : card.owner.label;
|
|
115
|
+
const urgency = priorityOf(card) === DEFAULT_PRIORITY ? "" : ` · ${priorityOf(card)}`;
|
|
116
|
+
const waits = dependsOf(card).length === 0 ? "" : ` · waits on ${dependsOf(card).map(cardRef).join(", ")}`;
|
|
117
|
+
// The two lanes that carry a fact of their own say it here: what a block waits on, and
|
|
118
|
+
// what closed a finished card. Both are one short phrase, so a listing stays one line.
|
|
119
|
+
const blocked = card.blockedBy === undefined ? "" : ` · blocked: ${card.blockedBy}`;
|
|
120
|
+
const landed = card.landed === undefined ? "" : ` · landed ${card.landed.commit}`;
|
|
121
|
+
return `${cardRef(card.id)} ${card.status} · ${card.title} · ${holder}${urgency}${waits}${blocked}${landed}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The state lines under a card's headline. */
|
|
125
|
+
function detailLines(card: Card, board?: BoardIndex, now?: number): string[] {
|
|
126
|
+
const lines: string[] = [];
|
|
127
|
+
if (card.owner !== undefined) lines.push(` owner: ${card.owner.label}`);
|
|
128
|
+
// An offer's age is the fact a reader acts on, which is the one place an age belongs in
|
|
129
|
+
// this text: the value is whether anybody has had time to answer.
|
|
130
|
+
if (card.offeredAt !== undefined && now !== undefined) {
|
|
131
|
+
lines.push(` offered: ${formatAge(now, card.offeredAt)} ago, and nobody took it`);
|
|
132
|
+
}
|
|
133
|
+
if (card.blockedBy !== undefined) lines.push(` blocked: ${card.blockedBy}`);
|
|
134
|
+
if (card.landed !== undefined) lines.push(` landed: ${card.landed.commit}`);
|
|
135
|
+
if (priorityOf(card) !== DEFAULT_PRIORITY) lines.push(` priority: ${priorityOf(card)}`);
|
|
136
|
+
if (card.plan !== undefined) lines.push(` plan: ${card.plan}`);
|
|
137
|
+
if (card.paths.length > 0) lines.push(` paths: ${card.paths.join(", ")}`);
|
|
138
|
+
if (dependsOf(card).length > 0) lines.push(` waits on: ${waitingOf(card, board)}`);
|
|
139
|
+
// The mirror question, for the same board: what leaving this card undone holds up. Only
|
|
140
|
+
// a card with the board in hand can answer it, which is why a bare card read omits it.
|
|
141
|
+
if (board !== undefined) {
|
|
142
|
+
const dependents = openDependentsOf(card.id, board);
|
|
143
|
+
if (dependents.length > 0) {
|
|
144
|
+
const which = dependents.length === 1 ? "card" : "cards";
|
|
145
|
+
lines.push(` waited on by ${dependents.length} ${which}: ${lanesText(dependents)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (card.notes.length > 0) {
|
|
149
|
+
const last = card.notes.at(-1);
|
|
150
|
+
if (last) lines.push(` last note (${last.by}): ${last.text}`);
|
|
151
|
+
}
|
|
152
|
+
const dropped = card.notesDropped > 0 ? ` (${card.notesDropped} earlier dropped)` : "";
|
|
153
|
+
lines.push(` ${plural(card.notes.length, "note")}${dropped}`);
|
|
154
|
+
return lines;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The cards a card waits on, each with its lane when the board is there to say it.
|
|
159
|
+
*
|
|
160
|
+
* The lane is what turns a list of ids into a decision: `#2 (done)` is a card about to
|
|
161
|
+
* start, and `#2 (doing)` is one to leave alone.
|
|
162
|
+
*/
|
|
163
|
+
function waitingOf(card: Card, board: BoardIndex | undefined): string {
|
|
164
|
+
return dependsOf(card)
|
|
165
|
+
.map((id) => {
|
|
166
|
+
const blocker = board?.get(id);
|
|
167
|
+
return blocker === undefined ? cardRef(id) : `${cardRef(id)} (${blocker.status})`;
|
|
168
|
+
})
|
|
169
|
+
.join(", ");
|
|
170
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* board — one shared feature board per repository.
|
|
3
|
+
*
|
|
4
|
+
* Every Pi session on a repository reads and writes the same cards, and `/board`
|
|
5
|
+
* serves them as a page in the browser that follows the store live. A card is one
|
|
6
|
+
* *feature*, not one step: the steps of the work live in the owner's plan and in
|
|
7
|
+
* `todo`, and the board answers who is doing what and what needs a decision.
|
|
8
|
+
*
|
|
9
|
+
* The factory only registers. The store is opened on first use by the session, the
|
|
10
|
+
* page starts when `/board` asks for it, and `session_shutdown` closes it — so a
|
|
11
|
+
* session that never touches the board writes nothing and binds nothing.
|
|
12
|
+
*
|
|
13
|
+
* Load with: pi --extension ./extensions/board
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
18
|
+
import { registerCommands } from "./commands.ts";
|
|
19
|
+
import { type BoardPage, createBoardPage } from "./page.ts";
|
|
20
|
+
import { type BoardSession, openBoardSession } from "../../lib/board/session.ts";
|
|
21
|
+
import { registerTools } from "./tools.ts";
|
|
22
|
+
|
|
23
|
+
export interface BoardDeps {
|
|
24
|
+
/** Test seam: a session with a temp agent directory and a fixed clock. */
|
|
25
|
+
session?: BoardSession;
|
|
26
|
+
/** Test seam: the page lifecycle, so a test drives it without a socket. */
|
|
27
|
+
page?: BoardPage;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function board(pi: ExtensionAPI, deps: BoardDeps = {}): void {
|
|
31
|
+
if (!isExtensionEnabled("board")) return;
|
|
32
|
+
const session = deps.session ?? openBoardSession(pi);
|
|
33
|
+
const page = deps.page ?? createBoardPage(session);
|
|
34
|
+
registerTools(pi, session);
|
|
35
|
+
registerCommands(pi, page);
|
|
36
|
+
|
|
37
|
+
pi.on("session_shutdown", async () => {
|
|
38
|
+
await page.closeAll();
|
|
39
|
+
});
|
|
40
|
+
}
|