@gavin-hu/my-pi-agent 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -30
- package/README.zh-CN.md +40 -25
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -23
- package/docs/extensions/board.md +397 -168
- package/docs/extensions/bytefly.md +234 -197
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +57 -63
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +197 -140
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +145 -0
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +83 -44
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +79 -25
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +17 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +5 -20
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +65 -93
- package/extensions/board/result.ts +30 -39
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +49 -11
- package/extensions/board/snapshot.ts +8 -6
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +154 -55
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +36 -42
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +35 -16
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +90 -28
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +38 -13
- package/extensions/board/view/render.ts +219 -277
- package/extensions/board/view/styles.ts +130 -32
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +7 -8
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +10 -7
- package/extensions/bytefly/footer/render/segments.ts +41 -38
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/render.ts +10 -1
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +89 -79
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -4
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/html.ts +73 -40
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +13 -14
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +9 -9
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +21 -9
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +58 -51
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +61 -41
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +102 -2
- package/extensions/plan/web-collab.ts +4 -4
- package/extensions/plan/web-comments.ts +39 -8
- package/extensions/plan/web-composer.ts +14 -3
- package/extensions/plan/web-keys.ts +149 -25
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +10 -68
- package/extensions/plan/web-outline.ts +22 -2
- package/extensions/plan/web-page.ts +56 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +23 -13
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web-settle.ts +14 -7
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +67 -131
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +39 -0
- package/extensions/serve/index.ts +85 -0
- package/extensions/serve/registry.ts +260 -0
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +17 -9
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -5
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +28 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +30 -5
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +199 -51
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +119 -36
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +81 -63
- package/lib/bus-request.ts +83 -0
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +17 -11
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +30 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +46 -21
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +10 -10
- package/lib/session-label.ts +129 -7
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +39 -22
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +29 -11
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +11 -11
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +33 -4
- package/lib/web/README.md +21 -0
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +20 -2
- package/lib/web/runtime.ts +9 -10
- package/package.json +8 -2
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/config.ts +0 -36
- package/extensions/board/seat.ts +0 -28
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -9,35 +9,33 @@ root changes. It is one of two sibling git extensions in this package
|
|
|
9
9
|
|
|
10
10
|
## Capabilities
|
|
11
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
12
|
### Isolating work
|
|
15
13
|
|
|
16
|
-
| Capability | How | What you get |
|
|
17
|
-
|
|
18
|
-
| Isolated checkout | `
|
|
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. |
|
|
20
|
-
| Escape refusal | `guard.blockFileEscapes`, `guard.blockGitRedirects`, `guard.blockSymlinkEscapes` | Writes, `cd`, and `git` redirects that reach the main checkout are refused, symlink escapes included. |
|
|
21
|
-
| Plan-mode refusal | `/worktree`, `
|
|
22
|
-
| PowerShell refusal | `powershell` while isolated | The unparseable shell is refused instead of becoming an unguarded escape. |
|
|
23
|
-
| Seeded working copy | `.worktreeinclude`, `include` | Gitignored files such as `.env` are copied into each new worktree. |
|
|
24
|
-
| Ignore bootstrap | `
|
|
25
|
-
| PR/MR checkout | `
|
|
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. |
|
|
14
|
+
| Capability | How | What you get |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Isolated checkout | `worktree` action `enter`, `/worktree enter`, `--worktree` | A new checkout at `<repo>/.pi/worktrees/<name>` on branch `worktree-<name>`, cut from `origin/main`. |
|
|
17
|
+
| Root-following built-ins | `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls` | The built-in tools keep taking ordinary relative paths, resolved inside the worktree. |
|
|
18
|
+
| Escape refusal | `guard.blockFileEscapes`, `guard.blockGitRedirects`, `guard.blockSymlinkEscapes` | Writes, `cd`, and `git` redirects that reach the main checkout are refused, symlink escapes included. |
|
|
19
|
+
| Plan-mode refusal | `/worktree`, `worktree` action `enter` | In plan mode the screen opens read-only and every verb refuses until the plan is approved. |
|
|
20
|
+
| PowerShell refusal | `powershell` while isolated | The unparseable shell is refused instead of becoming an unguarded escape. |
|
|
21
|
+
| Seeded working copy | `.worktreeinclude`, `include` | Gitignored files such as `.env` are copied into each new worktree. |
|
|
22
|
+
| Ignore bootstrap | `worktree` action `enter` | The first worktree offers to add `.pi/worktrees` to `.gitignore`, so the checkout does not pollute status. |
|
|
23
|
+
| PR/MR checkout | `worktree` action `enter` with `name`, `--worktree` | A pull or merge request's head is fetched and checked out as `pr-<n>`. |
|
|
24
|
+
| 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. |
|
|
27
25
|
|
|
28
26
|
### Returning and managing
|
|
29
27
|
|
|
30
|
-
| Capability | How | What you get |
|
|
31
|
-
|
|
32
|
-
| Return with a handoff | `
|
|
33
|
-
| Work that survives | `
|
|
34
|
-
| Status inventory | `
|
|
35
|
-
| Worktree screen | `/worktree` | One list screen with the focused worktree's path, base, and gaps, and the verbs to act on it. |
|
|
36
|
-
| Cleanup | `
|
|
37
|
-
| Concurrent-session safety | `
|
|
38
|
-
| Session restore | `/resume`, `--continue` | An active worktree is restored from the branch, or reported gone. |
|
|
39
|
-
| Extension signal | `worktree:changed` | Another extension follows the active root without polling. |
|
|
40
|
-
| Doctor finding | `run_doctor` | A degraded `guard.blockPowershell` setting is named in the health report instead of only at use. |
|
|
28
|
+
| Capability | How | What you get |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| Return with a handoff | `worktree` action `exit`, `/worktree exit` | Back in the main checkout, with the branch, its commits, and the merge or PR command that finishes them. |
|
|
31
|
+
| Work that survives | `worktree` action `exit`, `worktree.onExit` | A worktree holding changes or commits is asked about or kept, never force-deleted. |
|
|
32
|
+
| Status inventory | `worktree` action `list`, `/worktree` | The active root, the gaps isolation does not cover, and every managed worktree with its state. |
|
|
33
|
+
| Worktree screen | `/worktree` | One list screen with the focused worktree's path, base, and gaps, and the verbs to act on it. |
|
|
34
|
+
| Cleanup | `worktree` action `prune`, `/worktree prune` | Clean, unused, old worktrees are removed; every other one is kept with a reason. |
|
|
35
|
+
| Concurrent-session safety | `worktree` actions `enter` and `prune` | A worktree in use is locked, so another session's sweep cannot remove it. |
|
|
36
|
+
| Session restore | `/resume`, `--continue` | An active worktree is restored from the branch, or reported gone. |
|
|
37
|
+
| Extension signal | `worktree:changed` | Another extension follows the active root without polling. |
|
|
38
|
+
| Doctor finding | `run_doctor` | A degraded `guard.blockPowershell` setting is named in the health report instead of only at use. |
|
|
41
39
|
|
|
42
40
|
## Quickstart
|
|
43
41
|
|
|
@@ -51,30 +49,32 @@ pi install ./ # install the package
|
|
|
51
49
|
/worktree # the list screen, or the same facts as text
|
|
52
50
|
```
|
|
53
51
|
|
|
54
|
-
The model enters with `
|
|
52
|
+
The model enters with `worktree` action `enter`; `pi -e . --worktree feature-auth` starts
|
|
55
53
|
the session already isolated.
|
|
56
54
|
|
|
57
55
|
## Tools
|
|
58
56
|
|
|
59
|
-
<img src="../../assets/tools/
|
|
57
|
+
<img src="../../assets/tools/worktree-list.png" alt="The worktree tool's list action: the call line and the indented status it reports" width="100%">
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
four
|
|
59
|
+
One tool with four actions — `enter`, `exit`, `list`, `prune` — because they are
|
|
60
|
+
one lifecycle. It is `executionMode: "sequential"` and takes the strictest
|
|
61
|
+
annotation of the four, `destructiveHint: true` with `openWorldHint: true`,
|
|
62
|
+
because `enter` writes into the checkout the session is bound to; `list` writes
|
|
63
|
+
nothing, and the fold cannot say that with one hint.
|
|
64
64
|
|
|
65
|
-
|
|
|
65
|
+
| Action | Usage |
|
|
66
66
|
|---|---|
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
67
|
+
| `enter` | `name` to create a worktree, or `path` to enter an existing one. Errors if already isolated. |
|
|
68
|
+
| `exit` | optional `remove` and `keepBranch`. Returns the clean/keep/remove decision and a `handoff` (`branch`, `base`, `ahead`, `commits`, `merged`). |
|
|
69
|
+
| `list` | no fields. Read-only status: active root, inactive overrides, isolation gaps, managed worktrees. |
|
|
70
|
+
| `prune` | no fields. Removes eligible managed worktrees and reports the rest. |
|
|
71
71
|
|
|
72
|
-
`
|
|
72
|
+
`enter` creates `<repo>/.pi/worktrees/<name>` on branch
|
|
73
73
|
`worktree-<name>` cut from `origin/main` (which can lag the local `main`), or
|
|
74
74
|
enters an existing worktree by `path`, and rebinds the session's working root to
|
|
75
75
|
it.
|
|
76
76
|
|
|
77
|
-
`
|
|
77
|
+
`exit` 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
80
|
many commits it holds beyond the merge target, how many it made since it was
|
|
@@ -98,7 +98,7 @@ relative paths inside the worktree, and:
|
|
|
98
98
|
|
|
99
99
|
### Status text
|
|
100
100
|
|
|
101
|
-
`
|
|
101
|
+
`list` (and a headless `/worktree`) reports the active worktree, any
|
|
102
102
|
override that is not re-rooted, what isolation does not cover, and the managed
|
|
103
103
|
worktrees:
|
|
104
104
|
|
|
@@ -151,7 +151,7 @@ pane for the selected one. A worktree that has not committed since it was create
|
|
|
151
151
|
says `no commits` instead of `merged`: it is an ancestor of its base, which git
|
|
152
152
|
alone cannot tell from a branch that landed. When work did land, the row says how
|
|
153
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 `
|
|
154
|
+
facts print as text instead, which is also what the model's `list` action
|
|
155
155
|
reads.
|
|
156
156
|
|
|
157
157
|
| Key | What it does |
|
|
@@ -204,10 +204,10 @@ which also lists every key with its default and accepted values.
|
|
|
204
204
|
tool names: Pi refuses to load two extensions that register the same tool, so
|
|
205
205
|
exclude the name here to load alongside it. It is read when the extension
|
|
206
206
|
loads, from the process working directory; reload after changing it. `/worktree`
|
|
207
|
-
and `
|
|
207
|
+
and `worktree` action `list` name any overridden tool that is not re-rooted.
|
|
208
208
|
|
|
209
209
|
`.gitignore` must cover the managed directory, or worktrees show up as untracked
|
|
210
|
-
files in the main checkout. `
|
|
210
|
+
files in the main checkout. `enter` offers once per session to append
|
|
211
211
|
`.pi/worktrees/`, and skips the offer when `git check-ignore` already reports the
|
|
212
212
|
directory as ignored, so a wider rule is enough and no literal line is needed:
|
|
213
213
|
|
|
@@ -266,7 +266,7 @@ config/secrets.json
|
|
|
266
266
|
|
|
267
267
|
## Pull / merge requests
|
|
268
268
|
|
|
269
|
-
`
|
|
269
|
+
`enter` accepts a PR/MR reference in `name`, as does the startup flag:
|
|
270
270
|
|
|
271
271
|
```
|
|
272
272
|
pi --worktree '#1234'
|
|
@@ -282,8 +282,8 @@ While isolated, the extension exports `PI_WORKTREE_ROOT`, `PI_WORKTREE_BRANCH`,
|
|
|
282
282
|
and `PI_WORKTREE_MAIN` (the main checkout) into the process environment. A child
|
|
283
283
|
`pi` process — a subagent spawned by an extension, or any `pi` you start from a
|
|
284
284
|
tool — inherits it and binds to the same worktree instead of the main checkout.
|
|
285
|
-
The child is read-only with respect to lifecycle: `
|
|
286
|
-
|
|
285
|
+
The child is read-only with respect to lifecycle: `enter` and `exit`
|
|
286
|
+
refuse, and the parent owns cleanup.
|
|
287
287
|
|
|
288
288
|
Extensions can also follow changes on the `pi.events` channel
|
|
289
289
|
`worktree:changed`, which receives `{ active, path, branch, borrowed }`.
|
|
@@ -292,7 +292,7 @@ Extensions can also follow changes on the `pi.events` channel
|
|
|
292
292
|
|
|
293
293
|
Entering a managed worktree takes a `git worktree lock` (reason
|
|
294
294
|
`pi:<pid>:<session>`), so a concurrent sweep cannot remove it; exiting and
|
|
295
|
-
shutdown release it. `/worktree prune` (or
|
|
295
|
+
shutdown release it. `/worktree prune` (or `worktree` action `prune`) removes
|
|
296
296
|
managed worktrees that are clean, have no new commits, are not the current or a
|
|
297
297
|
live-locked one, and are older than `pruneAfterDays`. It releases locks whose
|
|
298
298
|
owning process is gone first, and any failure keeps the worktree with a reason.
|
|
@@ -308,7 +308,7 @@ use, falling back to directory mtime for a worktree with no record.
|
|
|
308
308
|
extension owns `bash` (or another overridden name), set
|
|
309
309
|
`skipOverrides: ["bash"]` so this extension does not register it; that tool
|
|
310
310
|
then runs in the main checkout with **no re-rooting**, so only its absolute
|
|
311
|
-
writes are guarded (relative paths are not isolated), and `
|
|
311
|
+
writes are guarded (relative paths are not isolated), and `list`
|
|
312
312
|
reports it. `skipOverrides` is read at load time, from the process working
|
|
313
313
|
directory.
|
|
314
314
|
- The commit count behind `no commits` comes from the commit a worktree was
|
|
@@ -323,11 +323,15 @@ use, falling back to directory mtime for a worktree with no record.
|
|
|
323
323
|
- Only the model's `bash` tool is re-rooted and guarded. A shell command the
|
|
324
324
|
user runs directly (`!` in the TUI / the `user_bash` event) is not re-rooted
|
|
325
325
|
and still runs in the main checkout; Pi has no mutable session cwd to change
|
|
326
|
-
that. `/worktree` and `
|
|
326
|
+
that. `/worktree` and `list` report the active root, and list the
|
|
327
327
|
shell under `Not isolated:` on every status read.
|
|
328
328
|
- Windows `powershell` is refused while isolated, or — with
|
|
329
329
|
`guard.blockPowershell: false` — allowed and unguarded. There is no third
|
|
330
330
|
state: a parsed PowerShell command that keeps the tool.
|
|
331
|
+
- **`list` is not available while planning.** One annotation set cannot be both
|
|
332
|
+
read-only and destructive, so plan mode hides the whole tool and a planning
|
|
333
|
+
session reads the inventory with `/worktree` instead. A per-tool filter is
|
|
334
|
+
all-or-nothing for the same reason, so disabling `worktree` loses every verb.
|
|
331
335
|
|
|
332
336
|
## Non-goals
|
|
333
337
|
|
|
@@ -344,7 +348,7 @@ use, falling back to directory mtime for a worktree with no record.
|
|
|
344
348
|
| Contract | Detail |
|
|
345
349
|
|---|---|
|
|
346
350
|
| 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. |
|
|
347
|
-
| Tools | `
|
|
351
|
+
| Tools | one `worktree` tool via `registerTool`, with four actions (`enter`, `exit`, `list`, `prune`), `executionMode: "sequential"`, and one annotation set for all four: `destructiveHint: true`, `openWorldHint: true` — the strictest, because `enter` writes into the checkout the session is bound to. The block is bytefly's generic fallback, as the four tools' was: the verb reads as the subject and its fields as the aside (`worktree enter (name feat)`, `worktree list`). No `outputSchema`; structured state is returned in tool-result `details` discriminated by `action` — `exit` carries `{ action, state, removed, handoff }`, where `handoff` is `{ branch, base, ahead, commits, merged }` (`commits` absent when the worktree has no recorded base commit). |
|
|
348
352
|
| 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`). |
|
|
349
353
|
| 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. |
|
|
350
354
|
| Flags | `--worktree` via `registerFlag`. |
|
|
@@ -389,7 +393,7 @@ one read and two renderings.
|
|
|
389
393
|
`--continue` restore the binding or report accurately when the directory is
|
|
390
394
|
gone; the registry is reconciled against `git worktree list` on every
|
|
391
395
|
management read and is never the source of truth.
|
|
392
|
-
- **Never force out work.** `
|
|
396
|
+
- **Never force out work.** `exit` never force-deletes a branch that
|
|
393
397
|
still holds uncommitted changes or commits not reachable from the recorded
|
|
394
398
|
base, even with `remove: true`.
|
|
395
399
|
- **A handoff is read, not performed.** The exit reports the branch's state and
|
|
@@ -416,7 +420,7 @@ one read and two renderings.
|
|
|
416
420
|
| `errors.ts` | Cancellation error that distinguishes a user decline from a failure |
|
|
417
421
|
| `enter.ts` / `exit.ts` / `prune.ts` | The lifecycle phases |
|
|
418
422
|
| `handoff.ts` | The `Handoff:` block an exit leaves: the branch's lead, whether the base contains it, and the merge or PR command. |
|
|
419
|
-
| `tools.ts` / `commands.ts` |
|
|
423
|
+
| `tools.ts` / `commands.ts` | The one model tool and its four actions / slash commands |
|
|
420
424
|
| `screen.ts` | The `/worktree` list screen: rows, keys, detail pane, and verbs |
|
|
421
425
|
| `root-tools.ts` | Load-time built-in overrides bound to the active root |
|
|
422
426
|
| `git.ts` | Git plumbing (shared runner + read helpers) |
|
|
@@ -436,7 +440,9 @@ bun run test:watch
|
|
|
436
440
|
```
|
|
437
441
|
|
|
438
442
|
The suite covers the pure logic (`include.ts` gitignore matcher, `guard.ts` path
|
|
439
|
-
and command checks, `parsePrReference`),
|
|
443
|
+
and command checks, `parsePrReference`), the folded tool's own surface
|
|
444
|
+
(`tools.test.ts`: the four actions in one schema and its annotations), drives the
|
|
445
|
+
`/worktree` screen against a
|
|
440
446
|
snapshot (`ui.test.ts`: rows, marks, verbs, and width bounds), and drives the
|
|
441
447
|
extension end-to-end against real temporary git repositories through a mocked
|
|
442
448
|
`pi` API: enter/exit, isolation guards, borrowed worktrees, prune, and
|
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `/board` — the page's one entry point.
|
|
2
|
+
* `/board` — the page's one entry point, and the person's door to the two signs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `team` opens the page on the Team tab, `status` prints one line, `stop` closes it, and
|
|
5
|
+
* `approve <id>` / `review <id>` write the two signs as the person at the keyboard, which is what
|
|
6
|
+
* a session that is not the board's lead cannot do. Everything else opens the page. The command
|
|
7
|
+
* is a thin parser — the lifecycle is `page.ts`'s, the transitions are `lib/board/steps.ts`'s —
|
|
8
|
+
* so the idle grace and `session_shutdown` close the same server this opens.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { cardRef, normalizeCardId } from "../../lib/board/card.ts";
|
|
13
|
+
import { PERSON_LABEL, personActor, type Sign, signOf } from "../../lib/board/lead.ts";
|
|
11
14
|
import { plural } from "../../lib/board/round-text.ts";
|
|
15
|
+
import { type BoardSession } from "../../lib/board/session.ts";
|
|
12
16
|
import type { JoinResult } from "../../lib/session-tool.ts";
|
|
13
17
|
import { createTeamJoin, type TeamJoin } from "../../lib/session-tool.ts";
|
|
14
|
-
import {
|
|
18
|
+
import { teamLine } from "./join.ts";
|
|
15
19
|
import type { BoardPage, BoardPageStatus } from "./page.ts";
|
|
16
20
|
|
|
17
21
|
/** The command's name, which is also the word a user types. */
|
|
18
22
|
export const BOARD_COMMAND = "board";
|
|
19
23
|
|
|
20
24
|
/** The words the command accepts, in the order a user is likely to want them. */
|
|
21
|
-
|
|
25
|
+
const BOARD_WORDS = ["approve", "review", "status", "stop", "team"] as const;
|
|
22
26
|
|
|
23
|
-
export function registerCommands(
|
|
27
|
+
export function registerCommands(
|
|
28
|
+
pi: ExtensionAPI,
|
|
29
|
+
session: BoardSession,
|
|
30
|
+
page: BoardPage,
|
|
31
|
+
deps: { team?: TeamJoin } = {},
|
|
32
|
+
): void {
|
|
24
33
|
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
34
|
pi.registerCommand(BOARD_COMMAND, {
|
|
28
|
-
description:
|
|
35
|
+
description:
|
|
36
|
+
"Open the shared board in a local browser, or sign its two gates as the person: /board [team | status | stop | approve <id> | review <id>]",
|
|
29
37
|
getArgumentCompletions: (prefix: string) => {
|
|
30
38
|
const text = prefix.trimStart();
|
|
31
39
|
if (text.includes(" ")) return null;
|
|
@@ -33,9 +41,14 @@ export function registerCommands(pi: ExtensionAPI, page: BoardPage, deps: { team
|
|
|
33
41
|
return items.length > 0 ? items.map((value) => ({ value, label: value })) : null;
|
|
34
42
|
},
|
|
35
43
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
36
|
-
const word = args.trim().toLowerCase();
|
|
44
|
+
const [word = "", rawCard] = args.trim().toLowerCase().split(/\s+/);
|
|
45
|
+
const sign = signOf(word);
|
|
46
|
+
if (sign !== undefined) {
|
|
47
|
+
ctx.ui.notify(await signLine(session, sign, rawCard, ctx), "info");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
37
50
|
if (word === "status") {
|
|
38
|
-
ctx.ui.notify(statusLine(await page.status(),
|
|
51
|
+
ctx.ui.notify(statusLine(await page.status(), session.team.last()), "info");
|
|
39
52
|
return;
|
|
40
53
|
}
|
|
41
54
|
if (word === "stop") {
|
|
@@ -60,40 +73,79 @@ export function registerCommands(pi: ExtensionAPI, page: BoardPage, deps: { team
|
|
|
60
73
|
);
|
|
61
74
|
return;
|
|
62
75
|
}
|
|
63
|
-
|
|
76
|
+
const stopped = await page.stop();
|
|
77
|
+
// The word is this session saying it serves nothing now: the page clears the seat too
|
|
78
|
+
// when it closes itself (the idle grace), and both writes are the same fact.
|
|
79
|
+
session.serve(undefined);
|
|
80
|
+
ctx.ui.notify(stopped ? "board: stopped" : "board: not running", "info");
|
|
64
81
|
return;
|
|
65
82
|
}
|
|
66
83
|
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
|
-
);
|
|
84
|
+
ctx.ui.notify(`board: "${word}" is not a word this command knows — try ${wordHelp()}`, "warning");
|
|
71
85
|
return;
|
|
72
86
|
}
|
|
73
87
|
const started = await page.start(ctx, word === "team" ? "team" : "board");
|
|
74
|
-
// A failed start has already said why. Otherwise the
|
|
75
|
-
//
|
|
76
|
-
//
|
|
88
|
+
// A failed start has already said why. Otherwise the seat is set from the page's own
|
|
89
|
+
// answer — the page a session serves is a role, and every name this session writes and
|
|
90
|
+
// is addressed by follows from it — and the join happens here, because opening the page
|
|
91
|
+
// is the lead's one action.
|
|
77
92
|
if (started.kind === "failed") return;
|
|
78
|
-
|
|
93
|
+
session.serve(started.kind === "served" ? started.seat : undefined);
|
|
94
|
+
const joined = await team.join(ctx, { label: (await session.who(ctx, ctx.cwd)).label });
|
|
95
|
+
session.team.record(joined);
|
|
79
96
|
// The address is on the line because the browser is not the only way in: a machine
|
|
80
97
|
// with no default browser prints it here, and a tab closed by mistake is one
|
|
81
98
|
// command from being opened again.
|
|
82
99
|
ctx.ui.notify(
|
|
83
100
|
started.kind === "attached"
|
|
84
101
|
? `board: ${started.seat} is serving this repository on :${started.port} — viewing · open ${started.url}${teamLine(joined)}`
|
|
85
|
-
: `board: ${started.seat} serving ${started.
|
|
102
|
+
: `board: ${started.seat} serving ${started.repo} on :${started.port} · open ${started.url}${teamLine(joined)}`,
|
|
86
103
|
"info",
|
|
87
104
|
);
|
|
88
105
|
},
|
|
89
106
|
});
|
|
90
107
|
}
|
|
91
108
|
|
|
109
|
+
/** The words, as the warning that names them: a sign takes a card, and nothing else does. */
|
|
110
|
+
function wordHelp(): string {
|
|
111
|
+
return BOARD_WORDS.map((entry) => (signOf(entry) === undefined ? `/board ${entry}` : `/board ${entry} <id>`)).join(
|
|
112
|
+
", ",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Sign one of the card's two gates as the person at the keyboard.
|
|
118
|
+
*
|
|
119
|
+
* The write is the same `mutate` the tool uses, so the rules are the state machine's: an
|
|
120
|
+
* `approve` outside `plan`, a `review` outside `execute`, an unknown id, and a card already
|
|
121
|
+
* signed are each refused with the sentence the tool would give. The one thing that differs
|
|
122
|
+
* is the actor — the person, whose id no session can have — which is what makes the sign a
|
|
123
|
+
* sign even when the session that typed it holds the card.
|
|
124
|
+
*/
|
|
125
|
+
async function signLine(
|
|
126
|
+
session: BoardSession,
|
|
127
|
+
sign: Sign,
|
|
128
|
+
rawCard: string | undefined,
|
|
129
|
+
ctx: ExtensionCommandContext,
|
|
130
|
+
): Promise<string> {
|
|
131
|
+
const id = rawCard === undefined ? undefined : normalizeCardId(rawCard);
|
|
132
|
+
if (id === undefined) {
|
|
133
|
+
return `board: ${sign} needs a card id — try /board ${sign} 4`;
|
|
134
|
+
}
|
|
135
|
+
const store = await session.store(ctx.cwd);
|
|
136
|
+
const result = await store.mutate(id, { kind: sign }, personActor(), session.now());
|
|
137
|
+
if (!result.ok) return `board: ${result.error}`;
|
|
138
|
+
const done = sign === "approve" ? "approved" : "reviewed";
|
|
139
|
+
return result.changed
|
|
140
|
+
? `board: ${cardRef(id)} ${done} by ${PERSON_LABEL}`
|
|
141
|
+
: `board: ${cardRef(id)} is already ${done} by ${PERSON_LABEL}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
92
144
|
/** One line for `/board status`: the page this session serves, watches, or does not have. */
|
|
93
145
|
export function statusLine(status: BoardPageStatus, team?: JoinResult): string {
|
|
94
146
|
if (status.running) {
|
|
95
147
|
const watching = plural(status.watchers ?? 0, "tab");
|
|
96
|
-
return `board: ${status.seat} serving on :${status.port} · ${watching} watching · ${status.where}${teamLine(team)}`;
|
|
148
|
+
return `board: ${status.seat} serving ${status.repo} on :${status.port} · ${watching} watching · ${status.where}${teamLine(team)}`;
|
|
97
149
|
}
|
|
98
150
|
if (status.stale === true) {
|
|
99
151
|
return `board: watching ${status.seat} on :${status.port} — that note answers nobody; /board stop drops it${teamLine(team)}`;
|
|
@@ -101,5 +153,7 @@ export function statusLine(status: BoardPageStatus, team?: JoinResult): string {
|
|
|
101
153
|
if (status.attached === true) {
|
|
102
154
|
return `board: watching ${status.seat} on :${status.port} — nothing to stop here${teamLine(team)}`;
|
|
103
155
|
}
|
|
104
|
-
|
|
156
|
+
// The team outlives the page: a session that claimed a card is on it with no page open, so
|
|
157
|
+
// the line says which team this session is on whichever door put it there.
|
|
158
|
+
return `board: not running — /board opens it${teamLine(team)}`;
|
|
105
159
|
}
|
|
@@ -11,15 +11,26 @@
|
|
|
11
11
|
* The rollup's text is `lib/board/round-text.ts`, because `team` prints it too.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { type Card, cardRef, DEFAULT_PRIORITY, dependsOf, priorityOf } from "../../lib/board/card.ts";
|
|
14
|
+
import { type Card, type CardBlock, cardRef, DEFAULT_PRIORITY, dependsOf, priorityOf } from "../../lib/board/card.ts";
|
|
15
15
|
import { type BoardIndex, lanesText, openDependentsOf } from "../../lib/board/links.ts";
|
|
16
|
+
import { skippedText } from "../../lib/board/skipped.ts";
|
|
16
17
|
import type { Skip } from "../../lib/board/store.ts";
|
|
17
|
-
import { formatAge } from "../../lib/format.ts";
|
|
18
|
+
import { formatAge, shortenPath } from "../../lib/format.ts";
|
|
18
19
|
import { plural } from "../../lib/board/round-text.ts";
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* What a claim adds to the card: the next step, in the order the board enforces.
|
|
23
|
+
*
|
|
24
|
+
* A claim is the one call that commits a session to a card, and the board will not record
|
|
25
|
+
* that card's plan until one exists — so the call that opens the work is the one place the
|
|
26
|
+
* order has to be said. It is a line of the transcript, not a field: nothing reads it back.
|
|
27
|
+
*/
|
|
28
|
+
export const CLAIM_NEXT =
|
|
29
|
+
" next: design it in plan mode, `plan` action `write`, then `contract` with the path it returned";
|
|
30
|
+
|
|
20
31
|
/** One card: the headline, then the state a reader needs in order to act on it. */
|
|
21
32
|
export function formatCard(card: Card, board?: BoardIndex, now?: number): string {
|
|
22
|
-
return [`${cardRef(card.id)} ${card.
|
|
33
|
+
return [`${cardRef(card.id)} ${card.lane} · ${card.title}`, ...detailLines(card, board, now)].join("\n");
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
/**
|
|
@@ -50,9 +61,9 @@ export function formatBoard(shown: readonly Card[], notes: ListingNotes): string
|
|
|
50
61
|
// them out; the count is what keeps the omission from reading as a smaller board.
|
|
51
62
|
if (notes.hiddenDone > 0) {
|
|
52
63
|
const them = notes.hiddenDone === 1 ? "it" : "them";
|
|
53
|
-
lines.push(`${plural(notes.hiddenDone, "done card")} hidden; pass
|
|
64
|
+
lines.push(`${plural(notes.hiddenDone, "done card")} hidden; pass lane: "done" to read ${them}.`);
|
|
54
65
|
}
|
|
55
|
-
for (const note of [archiveNote(notes.archived),
|
|
66
|
+
for (const note of [archiveNote(notes.archived), skippedText(notes.skipped)]) {
|
|
56
67
|
if (note !== "") lines.push(note);
|
|
57
68
|
}
|
|
58
69
|
return lines.join("\n");
|
|
@@ -66,7 +77,7 @@ export function formatBoard(shown: readonly Card[], notes: ListingNotes): string
|
|
|
66
77
|
*/
|
|
67
78
|
export function formatArchive(shown: readonly Card[], skipped: readonly Skip[]): string {
|
|
68
79
|
const lines = shown.length === 0 ? ["The archive is empty."] : shown.map(listLine);
|
|
69
|
-
const unreadable =
|
|
80
|
+
const unreadable = skippedText(skipped);
|
|
70
81
|
if (unreadable !== "") lines.push(unreadable);
|
|
71
82
|
return lines.join("\n");
|
|
72
83
|
}
|
|
@@ -81,18 +92,6 @@ export function archiveNote(count: number): string {
|
|
|
81
92
|
return `${plural(count, "card")} ${count === 1 ? "is" : "are"} set aside in the archive; pass archived: true to read ${count === 1 ? "it" : "them"}.`;
|
|
82
93
|
}
|
|
83
94
|
|
|
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
95
|
/** The dropped id, which is the only thing left of the card. */
|
|
97
96
|
export function formatDropped(id: string): string {
|
|
98
97
|
return `${cardRef(id)} dropped, with its notes.`;
|
|
@@ -109,6 +108,12 @@ export function formatArchived(ids: readonly string[]): string {
|
|
|
109
108
|
return `${plural(ids.length, "card")} archived: ${ids.map(cardRef).join(", ")} — out of the board, ${theirs} notes and plan still readable with archived: true.`;
|
|
110
109
|
}
|
|
111
110
|
|
|
111
|
+
/** A block as one line: the sentence, and the kind beside it when one was recorded. */
|
|
112
|
+
function blockText(block: CardBlock): string {
|
|
113
|
+
const note = block.note ?? "nothing recorded";
|
|
114
|
+
return block.kind === undefined ? note : `${note} (${block.kind})`;
|
|
115
|
+
}
|
|
116
|
+
|
|
112
117
|
/** A one-line listing: the id, the lane, the title, and who holds it. */
|
|
113
118
|
function listLine(card: Card): string {
|
|
114
119
|
const holder = card.owner === undefined ? "unassigned" : card.owner.label;
|
|
@@ -116,9 +121,9 @@ function listLine(card: Card): string {
|
|
|
116
121
|
const waits = dependsOf(card).length === 0 ? "" : ` · waits on ${dependsOf(card).map(cardRef).join(", ")}`;
|
|
117
122
|
// The two lanes that carry a fact of their own say it here: what a block waits on, and
|
|
118
123
|
// what closed a finished card. Both are one short phrase, so a listing stays one line.
|
|
119
|
-
const blocked = card.
|
|
124
|
+
const blocked = card.blocked === undefined ? "" : ` · blocked: ${blockText(card.blocked)}`;
|
|
120
125
|
const landed = card.landed === undefined ? "" : ` · landed ${card.landed.commit}`;
|
|
121
|
-
return `${cardRef(card.id)} ${card.
|
|
126
|
+
return `${cardRef(card.id)} ${card.lane} · ${card.title} · ${holder}${urgency}${waits}${blocked}${landed}`;
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
/** The state lines under a card's headline. */
|
|
@@ -130,10 +135,24 @@ function detailLines(card: Card, board?: BoardIndex, now?: number): string[] {
|
|
|
130
135
|
if (card.offeredAt !== undefined && now !== undefined) {
|
|
131
136
|
lines.push(` offered: ${formatAge(now, card.offeredAt)} ago, and nobody took it`);
|
|
132
137
|
}
|
|
133
|
-
if (card.
|
|
138
|
+
if (card.blocked !== undefined) lines.push(` blocked: ${blockText(card.blocked)}`);
|
|
134
139
|
if (card.landed !== undefined) lines.push(` landed: ${card.landed.commit}`);
|
|
135
140
|
if (priorityOf(card) !== DEFAULT_PRIORITY) lines.push(` priority: ${priorityOf(card)}`);
|
|
136
|
-
|
|
141
|
+
// The card stores the path resolved, so the line is shortened the way the `write` action shows
|
|
142
|
+
// it: an absolute home directory is noise in every read of the same card.
|
|
143
|
+
if (card.planPath !== undefined) lines.push(` plan: ${shortenPath(card.planPath)}`);
|
|
144
|
+
// The contract is what a reader is asked to agree to, so the block says it in full and
|
|
145
|
+
// says who signed it: an approval is a fact about that wording, and a review about the
|
|
146
|
+
// work, and neither is inferable from the lane.
|
|
147
|
+
if (card.contract !== undefined) {
|
|
148
|
+
lines.push(` goal: ${card.contract.goal}`);
|
|
149
|
+
lines.push(` acceptance: ${card.contract.acceptance}`);
|
|
150
|
+
lines.push(` scope out: ${card.contract.scopeOut}`);
|
|
151
|
+
const approved = card.contract.approved;
|
|
152
|
+
lines.push(` approved: ${approved === undefined ? "nobody yet" : approved.by}`);
|
|
153
|
+
}
|
|
154
|
+
if (card.review !== undefined) lines.push(` reviewed: ${card.review.by}`);
|
|
155
|
+
|
|
137
156
|
if (card.paths.length > 0) lines.push(` paths: ${card.paths.join(", ")}`);
|
|
138
157
|
if (dependsOf(card).length > 0) lines.push(` waits on: ${waitingOf(card, board)}`);
|
|
139
158
|
// The mirror question, for the same board: what leaving this card undone holds up. Only
|
|
@@ -158,13 +177,13 @@ function detailLines(card: Card, board?: BoardIndex, now?: number): string[] {
|
|
|
158
177
|
* The cards a card waits on, each with its lane when the board is there to say it.
|
|
159
178
|
*
|
|
160
179
|
* The lane is what turns a list of ids into a decision: `#2 (done)` is a card about to
|
|
161
|
-
* start, and `#2 (
|
|
180
|
+
* start, and `#2 (execute)` is one to leave alone.
|
|
162
181
|
*/
|
|
163
182
|
function waitingOf(card: Card, board: BoardIndex | undefined): string {
|
|
164
183
|
return dependsOf(card)
|
|
165
184
|
.map((id) => {
|
|
166
185
|
const blocker = board?.get(id);
|
|
167
|
-
return blocker === undefined ? cardRef(id) : `${cardRef(id)} (${blocker.
|
|
186
|
+
return blocker === undefined ? cardRef(id) : `${cardRef(id)} (${blocker.lane})`;
|
|
168
187
|
})
|
|
169
188
|
.join(", ");
|
|
170
189
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Every Pi session on a repository reads and writes the same cards, and `/board`
|
|
5
5
|
* serves them as a page in the browser that follows the store live. A card is one
|
|
6
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
|
|
7
|
+
* `todo`, and the board answers who is holding what and what needs a decision.
|
|
8
8
|
*
|
|
9
9
|
* The factory only registers. The store is opened on first use by the session, the
|
|
10
10
|
* page starts when `/board` asks for it, and `session_shutdown` closes it — so a
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
18
|
+
import { requestMount } from "../../lib/http/mount.ts";
|
|
18
19
|
import { registerCommands } from "./commands.ts";
|
|
19
|
-
import {
|
|
20
|
+
import { createLazyBoardPage } from "./lazy-page.ts";
|
|
21
|
+
import type { BoardPage } from "./page.ts";
|
|
20
22
|
import { type BoardSession, openBoardSession } from "../../lib/board/session.ts";
|
|
21
23
|
import { registerTools } from "./tools.ts";
|
|
22
24
|
|
|
@@ -30,9 +32,20 @@ export interface BoardDeps {
|
|
|
30
32
|
export default function board(pi: ExtensionAPI, deps: BoardDeps = {}): void {
|
|
31
33
|
if (!isExtensionEnabled("board")) return;
|
|
32
34
|
const session = deps.session ?? openBoardSession(pi);
|
|
33
|
-
|
|
35
|
+
/** The session the page serves as; the mount lands on this session's one server. */
|
|
36
|
+
let sessionId = "";
|
|
37
|
+
const page =
|
|
38
|
+
deps.page ??
|
|
39
|
+
createLazyBoardPage(session, {
|
|
40
|
+
mount: (spec) => requestMount(pi, spec),
|
|
41
|
+
session: () => sessionId,
|
|
42
|
+
});
|
|
34
43
|
registerTools(pi, session);
|
|
35
|
-
registerCommands(pi, page);
|
|
44
|
+
registerCommands(pi, session, page);
|
|
45
|
+
|
|
46
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
47
|
+
sessionId = ctx.sessionManager.getSessionId();
|
|
48
|
+
});
|
|
36
49
|
|
|
37
50
|
pi.on("session_shutdown", async () => {
|
|
38
51
|
await page.closeAll();
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A card body as
|
|
2
|
+
* A card body as `add` reads it: the four fields the tool creates a card from.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* a field added to one surface reaches the store unnormalized from the other. This is
|
|
8
|
-
* that body's one owner.
|
|
9
|
-
*
|
|
10
|
-
* Only the *fields* are shared: a refusal is translated per surface, because the tool
|
|
11
|
-
* answers a model with a failed result and the page answers a browser with a status.
|
|
4
|
+
* The normalization, the order the refusals come in, and the shape handed to the store are
|
|
5
|
+
* one body, so a field added to `add` cannot reach the store unnormalized. This is the
|
|
6
|
+
* boundary in front of `store.create`; a refusal is the first field that cannot be one.
|
|
12
7
|
*/
|
|
13
8
|
|
|
14
9
|
import {
|