@gavin-hu/my-pi-agent 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -52
- package/README.zh-CN.md +56 -46
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +507 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +561 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +298 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +254 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
- package/extensions/board/commands.ts +105 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +170 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +388 -0
- package/extensions/board/result.ts +108 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +90 -0
- package/extensions/board/seat.ts +28 -0
- package/extensions/board/snapshot.ts +42 -0
- package/extensions/board/tools.ts +444 -0
- package/extensions/board/transport/router.ts +91 -0
- package/extensions/board/transport/server.ts +138 -0
- package/extensions/board/types.ts +121 -0
- package/extensions/board/view/client.ts +51 -0
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +125 -0
- package/extensions/board/view/live.ts +231 -0
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +118 -0
- package/extensions/board/view/render.ts +519 -0
- package/extensions/board/view/styles.ts +228 -0
- package/extensions/board/view/team.ts +327 -0
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +67 -7
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +75 -43
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/check/commands.ts +2 -1
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +12 -0
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/job/runtime.ts +4 -4
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +185 -52
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +23 -5
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +27 -34
- package/extensions/plan/policy.ts +30 -17
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +52 -11
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +20 -95
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +15 -8
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -8
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +3 -58
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +11 -5
- package/extensions/session-bus/runtime.ts +220 -267
- package/extensions/session-bus/store.ts +55 -7
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +5 -5
- package/extensions/session-bus/types.ts +23 -14
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +145 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +119 -0
- package/extensions/team/index.ts +34 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +267 -0
- package/extensions/team/types.ts +65 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +106 -251
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +18 -290
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +11 -8
- package/lib/board/card.ts +479 -0
- package/lib/board/links.ts +141 -0
- package/lib/board/page-pointer.ts +310 -0
- package/lib/board/round-text.ts +81 -0
- package/lib/board/round.ts +94 -0
- package/lib/board/rules.ts +437 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +497 -0
- package/lib/board/view.ts +234 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +35 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +237 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +135 -0
- package/lib/http/token.ts +38 -0
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +25 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +201 -0
- package/lib/session-tool.ts +184 -0
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +38 -2
- package/lib/tui/glyphs.ts +3 -2
- package/lib/web/README.md +57 -4
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/components.ts +5 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/files.ts +0 -564
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/job/process.ts +0 -35
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/team` — the lead's round, printed.
|
|
3
|
+
*
|
|
4
|
+
* The Team tab is the same round with a mouse; a lead reading a TUI should not have to
|
|
5
|
+
* open a browser to see what needs a decision, so the command prints the rollup the
|
|
6
|
+
* tool returns, the work a dead owner left behind, and one line saying who is live. It
|
|
7
|
+
* reads the cards and the bus's records and writes nothing: acting on the round is
|
|
8
|
+
* `team`'s tool (`assign`, `reclaim`) or `board`'s.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { type Card, cardRef } from "../../lib/board/card.ts";
|
|
13
|
+
import { buildRound } from "../../lib/board/round.ts";
|
|
14
|
+
import { formatRound, plural } from "../../lib/board/round-text.ts";
|
|
15
|
+
import type { BoardSession } from "../../lib/board/session.ts";
|
|
16
|
+
import { createTeamJoin, type JoinResult, type TeamJoin } from "../../lib/session-tool.ts";
|
|
17
|
+
import { readRoster, type RosterSession, rosterStateOf } from "../../lib/session-roster.ts";
|
|
18
|
+
import { formatJoined, formatNotJoined } from "./format.ts";
|
|
19
|
+
|
|
20
|
+
/** The command's name, which is also the word a user types. */
|
|
21
|
+
export const TEAM_COMMAND = "team";
|
|
22
|
+
|
|
23
|
+
/** The words the command accepts besides the round itself. */
|
|
24
|
+
export const TEAM_WORDS = ["join"] as const;
|
|
25
|
+
|
|
26
|
+
/** How many lines a round may print before it says how many it left out. */
|
|
27
|
+
export const MAX_ROUND_LINES = 10;
|
|
28
|
+
|
|
29
|
+
export function registerCommands(pi: ExtensionAPI, session: BoardSession, deps: { team?: TeamJoin } = {}): void {
|
|
30
|
+
const team = deps.team ?? createTeamJoin(pi);
|
|
31
|
+
pi.registerCommand(TEAM_COMMAND, {
|
|
32
|
+
description: "The lead's round: who holds what, and what needs a decision; `/team join [<label>]` joins the team",
|
|
33
|
+
getArgumentCompletions: (prefix: string) => {
|
|
34
|
+
const text = prefix.trimStart();
|
|
35
|
+
if (text.includes(" ")) return null;
|
|
36
|
+
const items = TEAM_WORDS.filter((word) => word.startsWith(text));
|
|
37
|
+
return items.length > 0 ? items.map((value) => ({ value, label: value })) : null;
|
|
38
|
+
},
|
|
39
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
40
|
+
const [word = "", ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
41
|
+
if (word === "join") {
|
|
42
|
+
const label = rest.join(" ").trim();
|
|
43
|
+
const joined = await team.join(ctx, label === "" ? undefined : label);
|
|
44
|
+
ctx.ui.notify(joinedLine(joined), joined.ok ? "info" : "warning");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (word !== "") {
|
|
48
|
+
ctx.ui.notify(
|
|
49
|
+
`team: "${word}" is not a word this command knows — try /team, or /team join [<label>]`,
|
|
50
|
+
"warning",
|
|
51
|
+
);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const store = await session.store(ctx.cwd);
|
|
55
|
+
const cards = await store.list();
|
|
56
|
+
// The roster is read as data, so a repository with no bus prints the round and
|
|
57
|
+
// says nothing about who is here rather than failing.
|
|
58
|
+
const roster = await readRoster(await session.busDir(ctx.cwd));
|
|
59
|
+
ctx.ui.notify(roundText(cards, session.now(), roster), "info");
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `/team join`'s line: who this session is on the team, or why it is on none. */
|
|
65
|
+
export function joinedLine(result: JoinResult): string {
|
|
66
|
+
return result.ok ? formatJoined(result.label, result.watching) : formatNotJoined(result.error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The round as the lines a lead reads: the rollup, then who is live.
|
|
71
|
+
*
|
|
72
|
+
* Capped, because a notify is a message and not a screen: past its limit the round says
|
|
73
|
+
* how many lines it left out instead of pushing the rest of the session up the screen.
|
|
74
|
+
*/
|
|
75
|
+
export function roundText(cards: readonly Card[], now: number, roster?: readonly RosterSession[]): string {
|
|
76
|
+
if (cards.length === 0) return "team: nothing to report — the board is empty";
|
|
77
|
+
const round = formatRound(cards, buildRound(cards, now)).split("\n");
|
|
78
|
+
const shown = round.slice(0, MAX_ROUND_LINES);
|
|
79
|
+
if (round.length > MAX_ROUND_LINES) shown.push(`… ${plural(round.length - MAX_ROUND_LINES, "line")} more`);
|
|
80
|
+
for (const line of goneLines(cards, roster, now)) shown.push(line);
|
|
81
|
+
const who = presenceLine(roster, now);
|
|
82
|
+
if (who !== undefined) shown.push(who);
|
|
83
|
+
return shown.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The work a dead owner left behind, as the line that says who takes it.
|
|
88
|
+
*
|
|
89
|
+
* One line per gone owner that still holds cards, before the presence line: `gone: b` at
|
|
90
|
+
* the end of that line says who died, and this one says what to do about it. A card that
|
|
91
|
+
* waits on another is left to the board, whose rules would refuse the claim anyway.
|
|
92
|
+
*/
|
|
93
|
+
function goneLines(cards: readonly Card[], roster: readonly RosterSession[] | undefined, now: number): string[] {
|
|
94
|
+
if (roster === undefined) return [];
|
|
95
|
+
const gone = new Map<string, string>();
|
|
96
|
+
for (const session of roster) {
|
|
97
|
+
if (rosterStateOf(session, now) === "gone") gone.set(session.label.toLowerCase(), session.label);
|
|
98
|
+
}
|
|
99
|
+
if (gone.size === 0) return [];
|
|
100
|
+
const byOwner = new Map<string, string[]>();
|
|
101
|
+
for (const card of cards) {
|
|
102
|
+
if (card.owner === undefined || card.status === "done") continue;
|
|
103
|
+
const label = gone.get(card.owner.label.toLowerCase());
|
|
104
|
+
if (label === undefined) continue;
|
|
105
|
+
byOwner.set(label, [...(byOwner.get(label) ?? []), cardRef(card.id)]);
|
|
106
|
+
}
|
|
107
|
+
return [...byOwner.entries()].map(([label, refs]) => `reclaim ${refs.join(", ")} — ${label} is not running`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* `live: a, b · not listening: b · quiet: c · gone: d`, or nothing when nobody is here.
|
|
112
|
+
*
|
|
113
|
+
* Four facts, and the middle one is the one a lead can act on: a live session that is
|
|
114
|
+
* not listening reads mail at its next turn rather than being woken for it. It is only
|
|
115
|
+
* named when its record says so — a session that never wrote the field is not accused of
|
|
116
|
+
* ignoring anything. `gone` is the loudest and comes last, because it is the one a lead
|
|
117
|
+
* answers by reclaiming rather than by waiting.
|
|
118
|
+
*/
|
|
119
|
+
function presenceLine(roster: readonly RosterSession[] | undefined, now: number): string | undefined {
|
|
120
|
+
if (roster === undefined || roster.length === 0) return undefined;
|
|
121
|
+
const live: string[] = [];
|
|
122
|
+
const quiet: string[] = [];
|
|
123
|
+
const dead: string[] = [];
|
|
124
|
+
const silent: string[] = [];
|
|
125
|
+
for (const session of roster) {
|
|
126
|
+
const state = rosterStateOf(session, now);
|
|
127
|
+
if (state === "gone") {
|
|
128
|
+
dead.push(session.label);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (state !== "live") {
|
|
132
|
+
quiet.push(session.label);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
live.push(session.label);
|
|
136
|
+
if (session.watching?.armed === false) silent.push(session.label);
|
|
137
|
+
}
|
|
138
|
+
const parts = [
|
|
139
|
+
live.length > 0 ? `live: ${live.join(", ")}` : "",
|
|
140
|
+
silent.length > 0 ? `not listening: ${silent.join(", ")}` : "",
|
|
141
|
+
quiet.length > 0 ? `quiet: ${quiet.join(", ")}` : "",
|
|
142
|
+
dead.length > 0 ? `gone: ${dead.join(", ")}` : "",
|
|
143
|
+
];
|
|
144
|
+
return parts.filter((part) => part !== "").join(" · ");
|
|
145
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The handoff: what `team assign` tells the peer, and how it tells it.
|
|
3
|
+
*
|
|
4
|
+
* The peer is a session that is already running, so this is a message and not a
|
|
5
|
+
* process: `session send` writes it into the recipient's inbox, and its armed watch is
|
|
6
|
+
* what raises the turn. Nothing here starts anything, and a send that fails leaves the
|
|
7
|
+
* card exactly as it was — the order is deliberate, because a card assigned without a
|
|
8
|
+
* message is a card nobody knows about.
|
|
9
|
+
*
|
|
10
|
+
* The transport is a seam (`Dispatch`), so the tool's ordering is testable without a
|
|
11
|
+
* bus, and the one production implementation is `lib/session-tool.ts`'s.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { Card } from "../../lib/board/card.ts";
|
|
16
|
+
import { cardRef, type CardOwner } from "../../lib/board/card.ts";
|
|
17
|
+
import { listPeers, MAX_MESSAGE_TEXT, type Peer, send as sendTo } from "../../lib/session-tool.ts";
|
|
18
|
+
|
|
19
|
+
/** How a handoff reaches a peer, and what the bus can say about the peers there are. */
|
|
20
|
+
export interface Dispatch {
|
|
21
|
+
/** Send the text as `kind`, and report the labels a message file was written for. Throws on failure. */
|
|
22
|
+
send(input: { kind: "handoff" | "event"; to: string; text: string }): Promise<{ delivered: string[] }>;
|
|
23
|
+
/**
|
|
24
|
+
* Every session the bus lists, when it can say.
|
|
25
|
+
*
|
|
26
|
+
* Its record is what makes an assignment survive a rename — `holds` compares ids
|
|
27
|
+
* before labels — and what makes the report able to name a peer that cannot be
|
|
28
|
+
* woken. It never throws: a resolve that fails must not undo a handoff that landed.
|
|
29
|
+
*/
|
|
30
|
+
peers(): Promise<Peer[] | undefined>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What the peer reads.
|
|
35
|
+
*
|
|
36
|
+
* It names the card, its outcome, and the paths, and says what to do next: record the
|
|
37
|
+
* plan, work, note, finish. It does **not** ask the peer to claim the card — `assign`
|
|
38
|
+
* has already recorded the owner, and a claim by the holder is a no-op. The claim is
|
|
39
|
+
* only for the failure path where the send landed and the assignment did not.
|
|
40
|
+
*/
|
|
41
|
+
export function handoffText(card: Card, to: string, note?: string): string {
|
|
42
|
+
const lines = [
|
|
43
|
+
`Handoff: ${cardRef(card.id)} "${card.title}" is yours, ${to}.`,
|
|
44
|
+
card.paths.length > 0 ? `Paths: ${card.paths.join(", ")}` : undefined,
|
|
45
|
+
card.plan === undefined ? undefined : `Plan: ${card.plan}`,
|
|
46
|
+
note === undefined ? undefined : `Note: ${note}`,
|
|
47
|
+
`It is already assigned to you — record the plan with board action "plan" if it has none, then note each phase and move it to done or blocked.`,
|
|
48
|
+
].filter((line): line is string => line !== undefined);
|
|
49
|
+
return withinLimit(lines.join("\n"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What every live session reads when the lead offers a card instead of assigning it.
|
|
54
|
+
*
|
|
55
|
+
* An offer names no owner: the first peer to claim the card takes it, so the text says
|
|
56
|
+
* what the card is and that claiming is the next step, and it never implies the work is
|
|
57
|
+
* already somebody's.
|
|
58
|
+
*/
|
|
59
|
+
export function offerText(card: Card, note?: string): string {
|
|
60
|
+
const lines = [
|
|
61
|
+
`Offer: ${cardRef(card.id)} "${card.title}" is unclaimed.`,
|
|
62
|
+
card.paths.length > 0 ? `Paths: ${card.paths.join(", ")}` : undefined,
|
|
63
|
+
card.plan === undefined ? undefined : `Plan: ${card.plan}`,
|
|
64
|
+
note === undefined ? undefined : `Note: ${note}`,
|
|
65
|
+
`First to claim it takes it: run board action "claim" with card "${card.id}", then note each phase and move it to done or blocked.`,
|
|
66
|
+
].filter((line): line is string => line !== undefined);
|
|
67
|
+
return withinLimit(lines.join("\n"));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Trim to the bus's text limit, which is also this message's limit.
|
|
72
|
+
*
|
|
73
|
+
* Not `lib/tui/columns.ts`'s `clip`: that one measures terminal columns, and this one
|
|
74
|
+
* counts characters, because the bus's limit is characters. The number arrives from
|
|
75
|
+
* `lib/session-tool.ts` — one copy for the two extensions that send through it.
|
|
76
|
+
*/
|
|
77
|
+
function withinLimit(text: string): string {
|
|
78
|
+
return text.length > MAX_MESSAGE_TEXT ? `${text.slice(0, MAX_MESSAGE_TEXT - 1)}…` : text;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The production dispatch: `lib/session-tool.ts`, with a failure turned into a throw. */
|
|
82
|
+
export function sessionDispatch(ctx: ExtensionToolContext): Dispatch {
|
|
83
|
+
return {
|
|
84
|
+
async send(input) {
|
|
85
|
+
const result = await sendTo(ctx, input);
|
|
86
|
+
// `assign` and `offer` branch on a failed send, so this seam reports it the way
|
|
87
|
+
// they read it: the bus's sentence, as the reason the card was not handed over.
|
|
88
|
+
if (!result.ok) throw new Error(result.error);
|
|
89
|
+
return { delivered: result.delivered };
|
|
90
|
+
},
|
|
91
|
+
peers: () => listPeers(ctx),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The peer `to` names: an id exactly, else a label ignoring case.
|
|
97
|
+
*
|
|
98
|
+
* The send has already proved exactly one session answers to it, so the first match is
|
|
99
|
+
* the recipient; a miss means the bus could not say, which is why the caller falls back
|
|
100
|
+
* to the label alone.
|
|
101
|
+
*/
|
|
102
|
+
export function peerFor(peers: readonly Peer[] | undefined, to: string): Peer | undefined {
|
|
103
|
+
if (peers === undefined) return undefined;
|
|
104
|
+
const target = to.trim();
|
|
105
|
+
const byId = peers.find((peer) => peer.id === target);
|
|
106
|
+
if (byId) return byId;
|
|
107
|
+
const wanted = target.toLowerCase();
|
|
108
|
+
return peers.find((peer) => peer.label.toLowerCase() === wanted);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The labels a message reached that cannot be woken: their record says they are not
|
|
113
|
+
* listening.
|
|
114
|
+
*
|
|
115
|
+
* A label the bus could not answer for is left out — "the record does not say" is not
|
|
116
|
+
* "nobody will read it" — and so is a peer the bus lists as listening.
|
|
117
|
+
*/
|
|
118
|
+
export function notListening(peers: readonly Peer[] | undefined, labels: readonly string[]): string[] {
|
|
119
|
+
if (peers === undefined) return [];
|
|
120
|
+
return labels.filter((label) => {
|
|
121
|
+
const peer = peerFor(peers, label);
|
|
122
|
+
return peer !== undefined && peer.listening === false;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The peer a handoff names, as the card records it when the bus cannot say who that
|
|
128
|
+
* is: the label a message file was written for, else the one that was asked for.
|
|
129
|
+
*/
|
|
130
|
+
export function ownerFor(to: string, delivered: readonly string[]): CardOwner {
|
|
131
|
+
const label = delivered[0] ?? to;
|
|
132
|
+
return { label };
|
|
133
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model-facing text for one `team` call.
|
|
3
|
+
*
|
|
4
|
+
* A handoff is stated as what happened and what did not: `handed off` names the peer
|
|
5
|
+
* and the card, a failed send says the card is untouched, and an assignment that lost
|
|
6
|
+
* a race after the send says the peer should claim it. Nothing here claims the peer
|
|
7
|
+
* read the message — `delivered` means a message file was written for it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type Card, cardRef } from "../../lib/board/card.ts";
|
|
11
|
+
|
|
12
|
+
/** `/team join` landed: the label peers address, and whether mail will reach this session. */
|
|
13
|
+
export function formatJoined(label: string, watching: { armed: boolean }): string {
|
|
14
|
+
const state = watching.armed ? "listening for handoffs" : "not listening";
|
|
15
|
+
return `team: on the team as ${label}, ${state}.`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** `/team join` did not land, so nothing about this session changed. */
|
|
19
|
+
export function formatNotJoined(reason: string): string {
|
|
20
|
+
return `team: not on the team — ${reason}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A card that was handed over, and the peer that got the message.
|
|
25
|
+
*
|
|
26
|
+
* `notListening` is the one fact the send cannot tell you and the record can: a peer
|
|
27
|
+
* that is not listening reads the handoff at its next turn rather than being woken for
|
|
28
|
+
* it. Saying so is the difference between "delivered" and "delivered and read".
|
|
29
|
+
*/
|
|
30
|
+
export function formatHandoff(
|
|
31
|
+
card: Card,
|
|
32
|
+
to: string,
|
|
33
|
+
delivered: readonly string[],
|
|
34
|
+
notListening: readonly string[] = [],
|
|
35
|
+
): string {
|
|
36
|
+
const who = delivered.length > 0 ? delivered.join(", ") : to;
|
|
37
|
+
return [
|
|
38
|
+
`${cardRef(card.id)} handed off to ${who}.`,
|
|
39
|
+
` ${card.title}`,
|
|
40
|
+
card.paths.length > 0 ? ` paths: ${card.paths.join(", ")}` : undefined,
|
|
41
|
+
" The card is already owned by the peer; the handoff is in its inbox.",
|
|
42
|
+
notListeningLine(notListening),
|
|
43
|
+
]
|
|
44
|
+
.filter((line): line is string => line !== undefined)
|
|
45
|
+
.join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The line that names a peer which will not be woken, or nothing when every peer is
|
|
50
|
+
* listening.
|
|
51
|
+
*
|
|
52
|
+
* One sentence for both verbs: what the mail does instead, and no pronoun, because the
|
|
53
|
+
* list can hold one peer or three.
|
|
54
|
+
*/
|
|
55
|
+
export function notListeningLine(labels: readonly string[]): string | undefined {
|
|
56
|
+
if (labels.length === 0) return undefined;
|
|
57
|
+
return ` Not listening: ${labels.join(", ")} — it reads this at its next turn.`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The send that never landed: the card was not touched. */
|
|
61
|
+
export function formatHandoffFailure(card: Card, to: string, reason: string): string {
|
|
62
|
+
return `${cardRef(card.id)} was not handed off to ${to}: ${reason}\n The card is unchanged.`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A card offered to the team: who heard, and that nobody holds it yet.
|
|
67
|
+
*
|
|
68
|
+
* `offer` writes nothing, so the sentence has to say the ownership question is still
|
|
69
|
+
* open — a reader who took "offered" for "assigned" would stop looking at the card.
|
|
70
|
+
*/
|
|
71
|
+
export function formatOffer(card: Card, delivered: readonly string[], notListening: readonly string[] = []): string {
|
|
72
|
+
const who = delivered.length > 0 ? delivered.join(", ") : "nobody";
|
|
73
|
+
return [
|
|
74
|
+
`${cardRef(card.id)} offered to ${who}.`,
|
|
75
|
+
` ${card.title}`,
|
|
76
|
+
card.paths.length > 0 ? ` paths: ${card.paths.join(", ")}` : undefined,
|
|
77
|
+
" The card is unowned until one of them claims it.",
|
|
78
|
+
notListeningLine(notListening),
|
|
79
|
+
]
|
|
80
|
+
.filter((line): line is string => line !== undefined)
|
|
81
|
+
.join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The broadcast that never landed: no card was touched, and nobody heard. */
|
|
85
|
+
export function formatOfferFailure(card: Card, reason: string): string {
|
|
86
|
+
return `${cardRef(card.id)} was not offered: ${reason}\n The card is unchanged.`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The broadcast landed and recording the offer then lost a race.
|
|
91
|
+
*
|
|
92
|
+
* The peers have an event for a card somebody else has since claimed, moved, or taken
|
|
93
|
+
* over, so the correction says the offer stands as a message and the card is what the
|
|
94
|
+
* board now says — not that nothing happened.
|
|
95
|
+
*/
|
|
96
|
+
export function formatRacedOffer(card: Card, reason: string): string {
|
|
97
|
+
return (
|
|
98
|
+
`${cardRef(card.id)} was offered, but the card changed before the offer could be recorded: ${reason}\n` +
|
|
99
|
+
" The peers have the message; the card is as the board shows it now."
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The send landed and the assignment then lost a race.
|
|
105
|
+
*
|
|
106
|
+
* The peer has a message telling it the card is its, so the correction names the
|
|
107
|
+
* claim rather than pretending nothing happened.
|
|
108
|
+
*/
|
|
109
|
+
export function formatRacedAssignment(card: Card, to: string, reason: string): string {
|
|
110
|
+
return (
|
|
111
|
+
`${cardRef(card.id)} was handed off to ${to}, but the card changed before it could be assigned: ${reason}\n` +
|
|
112
|
+
` ${to} should run \`board\` action \`claim\` with card "${card.id}".`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A card taken back to To do. */
|
|
117
|
+
export function formatReclaimed(card: Card, previous: string): string {
|
|
118
|
+
return `${cardRef(card.id)} reclaimed from ${previous} and back in To do; its notes are kept.\n ${card.title}`;
|
|
119
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* team — the lead's round over the shared board.
|
|
3
|
+
*
|
|
4
|
+
* `board` owns the cards and never touches the session bus; `team` owns the two
|
|
5
|
+
* actions that need it. `assign` hands a card to a session that is already running
|
|
6
|
+
* and sends it a handoff — dispatch cannot start a peer's turn, and the page and the
|
|
7
|
+
* docs say so — and `reclaim` takes a card back when its owner is gone or has gone quiet.
|
|
8
|
+
* Its one
|
|
9
|
+
* command, `/team`, prints the round the board's Team tab draws, without a browser.
|
|
10
|
+
*
|
|
11
|
+
* Either extension works without the other: `board` is the work list any session
|
|
12
|
+
* uses, and `team` is the lead's. The factory only registers; the store is opened on
|
|
13
|
+
* first use.
|
|
14
|
+
*
|
|
15
|
+
* Load with: pi --extension ./extensions/team --extension ./extensions/session-bus
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { type BoardSession, openBoardSession } from "../../lib/board/session.ts";
|
|
20
|
+
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
21
|
+
import { registerCommands } from "./commands.ts";
|
|
22
|
+
import { registerTools } from "./tools.ts";
|
|
23
|
+
|
|
24
|
+
export interface TeamDeps {
|
|
25
|
+
/** Test seam: a session with a temp agent directory and a fixed clock. */
|
|
26
|
+
session?: BoardSession;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function team(pi: ExtensionAPI, deps: TeamDeps = {}): void {
|
|
30
|
+
if (!isExtensionEnabled("team")) return;
|
|
31
|
+
const session = deps.session ?? openBoardSession(pi);
|
|
32
|
+
registerTools(pi, session);
|
|
33
|
+
registerCommands(pi, session);
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `team` tool's parameters: two actions over one card.
|
|
3
|
+
*
|
|
4
|
+
* `assign` names the peer a handoff goes to, and `reclaim` takes a card back. Both
|
|
5
|
+
* take the card the way `board` does — `4` or `#4` — because a lead reads ids off
|
|
6
|
+
* the same board.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Type } from "typebox";
|
|
10
|
+
import { TEAM_ACTIONS } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
export const TeamParams = Type.Object({
|
|
13
|
+
action: Type.Union(
|
|
14
|
+
TEAM_ACTIONS.map((action) => Type.Literal(action)),
|
|
15
|
+
{
|
|
16
|
+
description:
|
|
17
|
+
"`assign` hands a card to a running session and sends it a handoff; `offer` broadcasts an unclaimed card to " +
|
|
18
|
+
"every other live session and writes nothing, so the first to claim it takes it; `reclaim` takes a card back " +
|
|
19
|
+
"to To do. A lead's three actions — use `board` to create, claim, move, note, and finish the cards themselves.",
|
|
20
|
+
},
|
|
21
|
+
),
|
|
22
|
+
card: Type.String({ description: 'The card to act on, e.g. "4" or "#4".' }),
|
|
23
|
+
to: Type.Optional(
|
|
24
|
+
Type.String({
|
|
25
|
+
description: "`assign`: the peer to hand the card to, as `/team` lists them — a teammate's label or id.",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
note: Type.Optional(Type.String({ description: "`assign` or `offer`: a line to include, e.g. what to do first." })),
|
|
29
|
+
});
|