@gavin-hu/my-pi-agent 0.17.0 → 0.19.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 +61 -45
- package/README.zh-CN.md +54 -42
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +344 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +465 -416
- 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 +554 -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} +66 -53
- package/{extensions/memory/README.md → docs/extensions/memory.md} +74 -63
- package/docs/extensions/plan.md +976 -0
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -28
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +285 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +166 -119
- package/docs/extensions/team.md +226 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +60 -38
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +275 -192
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
- package/extensions/board/commands.ts +87 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +72 -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 +312 -0
- package/extensions/board/result.ts +99 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +75 -0
- package/extensions/board/seat.ts +29 -0
- package/extensions/board/snapshot.ts +41 -0
- package/extensions/board/tools.ts +279 -0
- package/extensions/board/transport/router.ts +172 -0
- package/extensions/board/transport/server.ts +149 -0
- package/extensions/board/types.ts +54 -0
- package/extensions/board/view/actions.ts +306 -0
- package/extensions/board/view/client.ts +41 -0
- package/extensions/board/view/keys.ts +121 -0
- package/extensions/board/view/live.ts +397 -0
- package/extensions/board/view/page.ts +158 -0
- package/extensions/board/view/render.ts +496 -0
- package/extensions/board/view/styles.ts +211 -0
- package/extensions/board/view/team.ts +305 -0
- package/extensions/board/writes.ts +99 -0
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +40 -7
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +11 -21
- package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
- 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/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +13 -4
- package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- 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} +54 -82
- package/extensions/file-browse/live/rev.ts +42 -0
- package/extensions/file-browse/{router.ts → transport/router.ts} +44 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/view/chrome.ts +83 -0
- package/extensions/file-browse/{client.ts → view/client.ts} +63 -1
- package/extensions/file-browse/view/diff-html.ts +94 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +32 -158
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{live-client.ts → view/live-client.ts} +47 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
- package/extensions/file-browse/{styles.ts → view/styles.ts} +31 -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/goal/plan-route.ts +8 -2
- package/extensions/job/index.ts +15 -7
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +77 -21
- package/extensions/plan/feedback.ts +409 -63
- package/extensions/plan/index.ts +7 -5
- package/extensions/plan/plan-index.ts +44 -10
- package/extensions/plan/plan-text.ts +51 -24
- package/extensions/plan/plans.ts +162 -57
- package/extensions/plan/policy.ts +20 -12
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +51 -95
- package/extensions/plan/runtime.ts +22 -10
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +63 -95
- package/extensions/plan/tools/enter.ts +8 -3
- package/extensions/plan/tools/exit.ts +13 -19
- package/extensions/plan/tools/write.ts +8 -3
- package/extensions/plan/tools.ts +1 -1
- package/extensions/plan/tui.ts +29 -15
- package/extensions/plan/types.ts +29 -3
- package/extensions/plan/web-chrome.ts +2 -1
- package/extensions/plan/web-client.ts +122 -178
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-page.ts +96 -17
- package/extensions/plan/web-router.ts +109 -47
- package/extensions/plan/web-share-session.ts +191 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +245 -151
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
- 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 +113 -0
- package/extensions/session-bus/format.ts +101 -0
- package/extensions/session-bus/index.ts +75 -0
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +86 -0
- package/extensions/session-bus/overlap.ts +36 -0
- package/extensions/session-bus/protocol.ts +104 -0
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +208 -0
- package/extensions/session-bus/rows.ts +171 -0
- package/extensions/session-bus/runtime.ts +466 -0
- package/extensions/session-bus/store.ts +365 -0
- package/extensions/session-bus/tools.ts +192 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +112 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +105 -0
- package/extensions/team/index.ts +33 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +246 -0
- package/extensions/team/types.ts +48 -0
- package/extensions/watch/index.ts +15 -9
- 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 +76 -92
- 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} +66 -51
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +40 -25
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- 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} +119 -56
- package/extensions/wechat/{presence.ts → peer/presence.ts} +11 -11
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +13 -5
- 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} +15 -7
- 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} +104 -40
- 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/git.ts +5 -0
- package/extensions/worktree/guard.ts +3 -5
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/status.ts +1 -3
- package/lib/README.md +12 -6
- package/lib/board/card.ts +290 -0
- package/lib/board/links.ts +117 -0
- package/lib/board/page-pointer.ts +256 -0
- package/lib/board/round-text.ts +72 -0
- package/lib/board/round.ts +89 -0
- package/lib/board/rules.ts +304 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +310 -0
- package/lib/board/view.ts +150 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +13 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/router.ts +40 -13
- package/lib/http/serve.ts +234 -0
- package/lib/http/server.ts +29 -13
- package/lib/http/sink.ts +110 -0
- package/lib/http/token.ts +38 -0
- package/lib/interaction.ts +10 -0
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/policy.ts +12 -2
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +52 -33
- package/lib/requirements.ts +7 -9
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +130 -0
- package/lib/session-tool.ts +184 -0
- package/lib/tool-names.ts +25 -2
- package/lib/tui/README.md +16 -2
- package/lib/tui/dock-screen.ts +85 -17
- package/lib/tui/glyphs.ts +2 -0
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/web/README.md +5 -2
- package/lib/web/components.ts +6 -1
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +27 -14
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +77 -51
- 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/file-browse/README.md +0 -467
- package/extensions/file-browse/server.ts +0 -134
- package/extensions/plan/README.md +0 -709
- package/extensions/plan/config.ts +0 -27
- package/extensions/wechat/outbound.ts +0 -100
- package/extensions/wechat/qrcode.ts +0 -46
- package/skills/changelog/SKILL.md +0 -148
- /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/{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,87 +7,93 @@ 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
50
|
+
```text
|
|
51
|
+
/worktree # the list screen, or the same facts as text
|
|
52
|
+
```
|
|
20
53
|
|
|
21
|
-
|
|
54
|
+
The model enters with `enter_worktree`; `pi -e . --worktree feature-auth` starts
|
|
55
|
+
the session already isolated.
|
|
22
56
|
|
|
23
|
-
##
|
|
57
|
+
## Tools
|
|
24
58
|
|
|
25
|
-
<img src="../../assets/
|
|
59
|
+
<img src="../../assets/tools/list_worktrees.png" alt="The list_worktrees tool's call and result, drawn by the generic fallback" width="100%">
|
|
26
60
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
the selected one. Outside a terminal — print, RPC, JSON — the same facts print as
|
|
31
|
-
text instead, which is also what the model's `list_worktrees` reads.
|
|
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.
|
|
32
64
|
|
|
33
|
-
|
|
|
65
|
+
| Tool | Usage |
|
|
34
66
|
|---|---|
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
|
|
|
38
|
-
| `
|
|
39
|
-
| `x` | exit the active worktree |
|
|
40
|
-
| `p` | prune eligible worktrees in place |
|
|
41
|
-
| Esc, `q` | close |
|
|
67
|
+
| `enter_worktree` | `name` to create a worktree, or `path` to enter an existing one. Errors if already isolated. |
|
|
68
|
+
| `exit_worktree` | optional `remove` and `keepBranch`. Returns the clean/keep/remove decision and a `handoff` (`branch`, `base`, `ahead`, `merged`). |
|
|
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. |
|
|
42
71
|
|
|
43
|
-
`
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
47
76
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
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, whether that target already
|
|
81
|
+
contains them, and the `git merge --no-ff` and `gh pr create` commands that
|
|
82
|
+
finish the task. The tool never commits, merges, pushes, or opens the PR. A
|
|
83
|
+
branch that still holds work (uncommitted changes, or commits not reachable from
|
|
84
|
+
the recorded base) is never force-deleted, even with `remove: true`.
|
|
73
85
|
|
|
74
|
-
|
|
86
|
+
While isolated, `read`/`write`/`edit`/`bash`/`grep`/`find`/`ls` all resolve
|
|
87
|
+
relative paths inside the worktree, and:
|
|
75
88
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
| `--worktree <name>` | enter at session start |
|
|
89
|
+
- writes/edits outside the worktree are refused, including through a symlink
|
|
90
|
+
that points at the main checkout;
|
|
91
|
+
- `git -C <main>`, `GIT_DIR=`, `GIT_WORK_TREE=`, `--git-dir=`, `--work-tree=`,
|
|
92
|
+
and `cd` outside the worktree are refused in `bash`;
|
|
93
|
+
- remote `git fetch` (the base ref and a PR/MR head) runs with terminal
|
|
94
|
+
prompting disabled, so a credentials prompt fails fast instead of hanging.
|
|
83
95
|
|
|
84
|
-
|
|
85
|
-
asking: a clean worktree this session created is removed, one holding work or
|
|
86
|
-
entered by path asks, and only a worktree with no work loses its branch. There
|
|
87
|
-
are no flags to disagree with either. Declining a confirmation (entering an
|
|
88
|
-
outside path, exiting a worktree with work) is reported as normal feedback, not
|
|
89
|
-
an error. An argument a subcommand does not take prints the usage line instead of
|
|
90
|
-
being ignored.
|
|
96
|
+
### Status text
|
|
91
97
|
|
|
92
98
|
`list_worktrees` (and a headless `/worktree`) reports the active worktree, any
|
|
93
99
|
override that is not re-rooted, what isolation does not cover, and the managed
|
|
@@ -107,19 +113,56 @@ instead, so there is no gap to report. `unverifiable` appears only while
|
|
|
107
113
|
reset at session start. `doctor` reports the `powershell` switch as `degraded`
|
|
108
114
|
when it is off and notes the two structural gaps, which no setting closes.
|
|
109
115
|
|
|
110
|
-
|
|
116
|
+
## Commands and flags
|
|
111
117
|
|
|
112
|
-
|
|
|
118
|
+
| Command / flag | What it does |
|
|
113
119
|
|---|---|
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
120
|
+
| `/worktree` | the list screen: managed worktrees, the active one, and its gaps |
|
|
121
|
+
| `/worktree enter [name]` | enter a worktree (a name may be a PR/MR reference) |
|
|
122
|
+
| `/worktree exit` | exit a worktree |
|
|
123
|
+
| `/worktree prune` | remove clean, unused, old managed worktrees |
|
|
124
|
+
| `--worktree <name>` | enter at session start |
|
|
125
|
+
|
|
126
|
+
Exit asks whether to keep or remove, and `worktree.onExit` answers without
|
|
127
|
+
asking: a clean worktree this session created is removed, one holding work or
|
|
128
|
+
entered by path asks, and only a worktree with no work loses its branch. There
|
|
129
|
+
are no flags to disagree with either. Declining a confirmation (entering an
|
|
130
|
+
outside path, exiting a worktree with work) is reported as normal feedback, not
|
|
131
|
+
an error. An argument a subcommand does not take prints the usage line instead of
|
|
132
|
+
being ignored.
|
|
133
|
+
|
|
134
|
+
### The /worktree screen
|
|
135
|
+
|
|
136
|
+
<img src="../../assets/screens/worktree.png" alt="The /worktree screen: a Worktrees title rule with a `4 worktrees · 1 dirty · 1 ahead` summary, the current dark-mode row carrying its ahead count, a dirty row, a merged row, and a stale-locked row, the focused row's path and base with the Not isolated gap under it, and the key hints" width="100%">
|
|
137
|
+
|
|
138
|
+
`/worktree` on its own opens a list screen: one row per managed worktree with its
|
|
139
|
+
state (current, dirty, ahead, behind, merged, locked, last used), the active
|
|
140
|
+
worktree's base and isolation gaps under the focused row, and a detail pane for
|
|
141
|
+
the selected one. Outside a terminal — print, RPC, JSON — the same facts print as
|
|
142
|
+
text instead, which is also what the model's `list_worktrees` reads.
|
|
143
|
+
|
|
144
|
+
| Key | What it does |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `j` / `k`, arrows | move |
|
|
147
|
+
| `g` / `G` | first / last row |
|
|
148
|
+
| Enter, `l` | open the selected worktree's detail |
|
|
149
|
+
| `e` | enter the selected worktree |
|
|
150
|
+
| `x` | exit the active worktree |
|
|
151
|
+
| `p` | prune eligible worktrees in place |
|
|
152
|
+
| Esc, `q` | close |
|
|
153
|
+
|
|
154
|
+
`e` and `x` close the screen before they act, because an exit asks through the
|
|
155
|
+
normal prompt and a prompt cannot open inside a mounted screen. `p` never asks,
|
|
156
|
+
so it prunes without closing and reports the result in the footer. In plan mode
|
|
157
|
+
the screen still opens and every verb refuses with a notice.
|
|
118
158
|
|
|
119
159
|
## Configuration
|
|
120
160
|
|
|
121
|
-
The `worktree` section of the shared `bytefly.json
|
|
122
|
-
|
|
161
|
+
The `worktree` section of the shared `bytefly.json`; the project document is read
|
|
162
|
+
from the repository root rather than the active working directory, and the
|
|
163
|
+
project wins. Resolution rules are in
|
|
164
|
+
[`../configuration.md#worktree--isolated-worktrees`](../configuration.md#worktree--isolated-worktrees),
|
|
165
|
+
which also lists every key with its default and accepted values.
|
|
123
166
|
|
|
124
167
|
```jsonc
|
|
125
168
|
// bytefly.json → "worktree"
|
|
@@ -0,0 +1,87 @@
|
|
|
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(page.status(), joined), "info");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (word === "stop") {
|
|
42
|
+
const status = page.status();
|
|
43
|
+
// A viewer's page is somebody else's to stop, and saying whose is the answer.
|
|
44
|
+
if (status.attached === true) {
|
|
45
|
+
ctx.ui.notify(
|
|
46
|
+
`board: nothing to stop here — ${status.seat} serves this repository on :${status.port}`,
|
|
47
|
+
"info",
|
|
48
|
+
);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
ctx.ui.notify((await page.stop(ctx)) ? "board: stopped" : "board: not running", "info");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (word !== "" && word !== "team") {
|
|
55
|
+
ctx.ui.notify(
|
|
56
|
+
`board: "${word}" is not a word this command knows — try /board, /board team, /board status, /board stop`,
|
|
57
|
+
"warning",
|
|
58
|
+
);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const started = await page.start(ctx, word === "team" ? "team" : "board");
|
|
62
|
+
// A failed start has already said why. Otherwise the join happens here, because
|
|
63
|
+
// opening the page is the lead's one action: the seat when this session serves it,
|
|
64
|
+
// this session's own label when it watches a page somebody else serves.
|
|
65
|
+
if (started.kind === "failed") return;
|
|
66
|
+
joined = await team.join(ctx, joinLabel(started.kind === "served"));
|
|
67
|
+
ctx.ui.notify(
|
|
68
|
+
started.kind === "attached"
|
|
69
|
+
? `board: ${started.seat} is serving this repository on :${started.port} — viewing${teamLine(joined)}`
|
|
70
|
+
: `board: ${started.seat} serving ${started.where} on :${started.port}${teamLine(joined)}`,
|
|
71
|
+
"info",
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One line for `/board status`: the page this session serves, watches, or does not have. */
|
|
78
|
+
export function statusLine(status: BoardPageStatus, team?: JoinResult): string {
|
|
79
|
+
if (status.running) {
|
|
80
|
+
const watching = plural(status.watchers ?? 0, "tab");
|
|
81
|
+
return `board: ${status.seat} serving on :${status.port} · ${watching} watching · ${status.where}${teamLine(team)}`;
|
|
82
|
+
}
|
|
83
|
+
if (status.attached === true) {
|
|
84
|
+
return `board: watching ${status.seat} on :${status.port} — nothing to stop here${teamLine(team)}`;
|
|
85
|
+
}
|
|
86
|
+
return "board: not running — /board opens it";
|
|
87
|
+
}
|
|
@@ -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,72 @@
|
|
|
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 } from "../../lib/board/links.ts";
|
|
16
|
+
import { plural } from "../../lib/board/round-text.ts";
|
|
17
|
+
|
|
18
|
+
/** One card: the headline, then the state a reader needs in order to act on it. */
|
|
19
|
+
export function formatCard(card: Card, board?: BoardIndex): string {
|
|
20
|
+
return [`${cardRef(card.id)} ${card.status} · ${card.title}`, ...detailLines(card, board)].join("\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A board, in lane order. */
|
|
24
|
+
export function formatCards(cards: readonly Card[]): string {
|
|
25
|
+
if (cards.length === 0) return "The board is empty.";
|
|
26
|
+
return cards.map((card) => listLine(card)).join("\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The dropped id, which is the only thing left of the card. */
|
|
30
|
+
export function formatDropped(id: string): string {
|
|
31
|
+
return `${cardRef(id)} dropped, with its notes.`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A one-line listing: the id, the lane, the title, and who holds it. */
|
|
35
|
+
function listLine(card: Card): string {
|
|
36
|
+
const holder = card.owner === undefined ? "unassigned" : card.owner.label;
|
|
37
|
+
const urgency = priorityOf(card) === DEFAULT_PRIORITY ? "" : ` · ${priorityOf(card)}`;
|
|
38
|
+
const waits = dependsOf(card).length === 0 ? "" : ` · waits on ${dependsOf(card).map(cardRef).join(", ")}`;
|
|
39
|
+
return `${cardRef(card.id)} ${card.status} · ${card.title} · ${holder}${urgency}${waits}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The state lines under a card's headline. */
|
|
43
|
+
function detailLines(card: Card, board?: BoardIndex): string[] {
|
|
44
|
+
const lines: string[] = [];
|
|
45
|
+
if (card.owner !== undefined) lines.push(` owner: ${card.owner.label}`);
|
|
46
|
+
if (priorityOf(card) !== DEFAULT_PRIORITY) lines.push(` priority: ${priorityOf(card)}`);
|
|
47
|
+
if (card.plan !== undefined) lines.push(` plan: ${card.plan}`);
|
|
48
|
+
if (card.paths.length > 0) lines.push(` paths: ${card.paths.join(", ")}`);
|
|
49
|
+
if (dependsOf(card).length > 0) lines.push(` waits on: ${waitingOf(card, board)}`);
|
|
50
|
+
if (card.notes.length > 0) {
|
|
51
|
+
const last = card.notes.at(-1);
|
|
52
|
+
if (last) lines.push(` last note (${last.by}): ${last.text}`);
|
|
53
|
+
}
|
|
54
|
+
const dropped = card.notesDropped > 0 ? ` (${card.notesDropped} earlier dropped)` : "";
|
|
55
|
+
lines.push(` ${plural(card.notes.length, "note")}${dropped}`);
|
|
56
|
+
return lines;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The cards a card waits on, each with its lane when the board is there to say it.
|
|
61
|
+
*
|
|
62
|
+
* The lane is what turns a list of ids into a decision: `#2 (done)` is a card about to
|
|
63
|
+
* start, and `#2 (doing)` is one to leave alone.
|
|
64
|
+
*/
|
|
65
|
+
function waitingOf(card: Card, board: BoardIndex | undefined): string {
|
|
66
|
+
return dependsOf(card)
|
|
67
|
+
.map((id) => {
|
|
68
|
+
const blocker = board?.get(id);
|
|
69
|
+
return blocker === undefined ? cardRef(id) : `${cardRef(id)} (${blocker.status})`;
|
|
70
|
+
})
|
|
71
|
+
.join(", ");
|
|
72
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A card body as both surfaces read it: the tool's arguments and the page's JSON.
|
|
3
|
+
*
|
|
4
|
+
* `add` is reachable two ways, and the fields it takes are the same four — title,
|
|
5
|
+
* path hints, priority, and an optional first note. The normalization, the order the
|
|
6
|
+
* refusals come in, and the shape handed to the store were written twice, which is how
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
type FieldError,
|
|
16
|
+
normalizeNote,
|
|
17
|
+
normalizePathHints,
|
|
18
|
+
normalizePriority,
|
|
19
|
+
normalizeTitle,
|
|
20
|
+
} from "../../lib/board/card.ts";
|
|
21
|
+
import type { CreateInput } from "../../lib/board/store.ts";
|
|
22
|
+
|
|
23
|
+
/** The first note, or nothing when the body carries none. */
|
|
24
|
+
function noteOf(raw: unknown): { text?: string } | FieldError {
|
|
25
|
+
if (raw === undefined) return {};
|
|
26
|
+
const parsed = normalizeNote(raw);
|
|
27
|
+
if ("error" in parsed) return parsed;
|
|
28
|
+
return { text: parsed.text };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The card `add` creates, or the first field that cannot be one.
|
|
33
|
+
*
|
|
34
|
+
* The store takes an already-normalized body, so this is the boundary in front of it:
|
|
35
|
+
* an absent field is its default, and a bad one is refused rather than clamped.
|
|
36
|
+
*/
|
|
37
|
+
export function createInput(body: Record<string, unknown>): { input: CreateInput } | FieldError {
|
|
38
|
+
const title = normalizeTitle(body.title);
|
|
39
|
+
if ("error" in title) return title;
|
|
40
|
+
const paths = normalizePathHints(body.paths);
|
|
41
|
+
if ("error" in paths) return paths;
|
|
42
|
+
const priority = normalizePriority(body.priority);
|
|
43
|
+
if ("error" in priority) return priority;
|
|
44
|
+
const note = noteOf(body.note);
|
|
45
|
+
if ("error" in note) return note;
|
|
46
|
+
return {
|
|
47
|
+
input: {
|
|
48
|
+
title: title.title,
|
|
49
|
+
paths: paths.paths,
|
|
50
|
+
priority: priority.priority,
|
|
51
|
+
...(note.text === undefined ? {} : { note: note.text }),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board's side of the team: the one call that puts this session on it.
|
|
3
|
+
*
|
|
4
|
+
* The user's surface is `/board` and `team`; the session bus is the transport under
|
|
5
|
+
* them. So the board joins on the user's behalf — when the page opens, and again the
|
|
6
|
+
* first time a card is worked — and never asks anybody to run a bus action.
|
|
7
|
+
*
|
|
8
|
+
* The seam exists for the same reason `team`'s dispatch does: a board suite drives a
|
|
9
|
+
* recording join instead of a bus, and the shipped implementation is one in-process
|
|
10
|
+
* request through `lib/session-tool.ts` (a command context cannot call a tool).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { JoinResult } from "../../lib/session-tool.ts";
|
|
14
|
+
import { DEFAULT_SEAT } from "./seat.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The join itself is `lib/session-tool.ts`'s, because `team` asks for it too; this
|
|
18
|
+
* module only holds what is the board's — the label it lands on, and how the outcome
|
|
19
|
+
* reads on a `/board` line.
|
|
20
|
+
*/
|
|
21
|
+
export type { TeamJoin } from "../../lib/session-tool.ts";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The label `/board` lands on: the page's seat when this session serves it, and
|
|
25
|
+
* nothing when it watches somebody else's page, so a viewer keeps its own name.
|
|
26
|
+
*/
|
|
27
|
+
export function joinLabel(served: boolean): string | undefined {
|
|
28
|
+
return served ? DEFAULT_SEAT : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* What the join did, as a clause for a `/board` line.
|
|
33
|
+
*
|
|
34
|
+
* A board with no bus still serves, so the failure is a clause and not an error: the
|
|
35
|
+
* page works, and the line says there is no team to hand work over.
|
|
36
|
+
*/
|
|
37
|
+
export function teamLine(result: JoinResult | undefined): string {
|
|
38
|
+
if (result === undefined) return "";
|
|
39
|
+
if (!result.ok) return ` · no team (${result.error})`;
|
|
40
|
+
const state = result.watching.armed ? "listening" : "not listening";
|
|
41
|
+
return ` · on the team as ${result.label}, ${state}`;
|
|
42
|
+
}
|