@gavin-hu/my-pi-agent 0.18.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 +52 -44
- package/README.zh-CN.md +46 -41
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +344 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +446 -404
- 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} +61 -50
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +390 -317
- 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 +285 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- 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} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +258 -162
- 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/panel/commands.ts +40 -7
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +3 -20
- 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} +1 -1
- 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} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -15
- 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/{html.ts → view/html.ts} +14 -12
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -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} +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/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +147 -38
- package/extensions/plan/index.ts +6 -4
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +8 -11
- package/extensions/plan/policy.ts +20 -12
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +21 -9
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/tools/exit.ts +11 -95
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +0 -8
- package/extensions/plan/web-client.ts +1 -1
- package/extensions/plan/web-page.ts +0 -7
- package/extensions/plan/web-router.ts +3 -57
- 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} +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 +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 +4 -2
- package/extensions/session-bus/runtime.ts +129 -238
- package/extensions/session-bus/store.ts +42 -6
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +4 -4
- package/extensions/session-bus/types.ts +13 -13
- 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/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} +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/{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/guard.ts +3 -5
- package/lib/README.md +8 -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/serve.ts +234 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +110 -0
- package/lib/http/token.ts +38 -0
- package/lib/policy.ts +12 -2
- package/lib/requirements.ts +6 -7
- 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/glyphs.ts +2 -0
- package/lib/web/README.md +5 -2
- package/lib/web/components.ts +5 -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/file-browse/README.md +0 -506
- package/extensions/file-browse/server.ts +0 -141
- 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,105 @@
|
|
|
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 send landed and the assignment then lost a race.
|
|
91
|
+
*
|
|
92
|
+
* The peer has a message telling it the card is its, so the correction names the
|
|
93
|
+
* claim rather than pretending nothing happened.
|
|
94
|
+
*/
|
|
95
|
+
export function formatRacedAssignment(card: Card, to: string, reason: string): string {
|
|
96
|
+
return (
|
|
97
|
+
`${cardRef(card.id)} was handed off to ${to}, but the card changed before it could be assigned: ${reason}\n` +
|
|
98
|
+
` ${to} should run \`board\` action \`claim\` with card "${card.id}".`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A card taken back to To do. */
|
|
103
|
+
export function formatReclaimed(card: Card, previous: string): string {
|
|
104
|
+
return `${cardRef(card.id)} reclaimed from ${previous} and back in To do; its notes are kept.\n ${card.title}`;
|
|
105
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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 has gone quiet. Its one
|
|
8
|
+
* command, `/team`, prints the round the board's Team tab draws, without a browser.
|
|
9
|
+
*
|
|
10
|
+
* Either extension works without the other: `board` is the work list any session
|
|
11
|
+
* uses, and `team` is the lead's. The factory only registers; the store is opened on
|
|
12
|
+
* first use.
|
|
13
|
+
*
|
|
14
|
+
* Load with: pi --extension ./extensions/team --extension ./extensions/session-bus
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { type BoardSession, openBoardSession } from "../../lib/board/session.ts";
|
|
19
|
+
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
20
|
+
import { registerCommands } from "./commands.ts";
|
|
21
|
+
import { registerTools } from "./tools.ts";
|
|
22
|
+
|
|
23
|
+
export interface TeamDeps {
|
|
24
|
+
/** Test seam: a session with a temp agent directory and a fixed clock. */
|
|
25
|
+
session?: BoardSession;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function team(pi: ExtensionAPI, deps: TeamDeps = {}): void {
|
|
29
|
+
if (!isExtensionEnabled("team")) return;
|
|
30
|
+
const session = deps.session ?? openBoardSession(pi);
|
|
31
|
+
registerTools(pi, session);
|
|
32
|
+
registerCommands(pi, session);
|
|
33
|
+
}
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `team` tool: the lead's round over the same cards `board` owns.
|
|
3
|
+
*
|
|
4
|
+
* Two actions, because those are the two that need the bus. `assign` sends the
|
|
5
|
+
* handoff **first** and sets the owner **second**: a card assigned without a message
|
|
6
|
+
* is a card nobody knows about, while a message for a card that then changed is one
|
|
7
|
+
* the peer can still claim. Either failure is reported with the card as it actually
|
|
8
|
+
* is, so the lead's next call starts from the truth.
|
|
9
|
+
*
|
|
10
|
+
* `reclaim` needs no message — it is the compensation for a dispatch that did not
|
|
11
|
+
* land — and it belongs here rather than in `board` because it only makes sense in
|
|
12
|
+
* the lead's context.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { Static } from "typebox";
|
|
17
|
+
import { type Card, cardRef, normalizeCardId } from "../../lib/board/card.ts";
|
|
18
|
+
import { indexOf } from "../../lib/board/links.ts";
|
|
19
|
+
import { type Actor, applyOp } from "../../lib/board/rules.ts";
|
|
20
|
+
import type { BoardSession } from "../../lib/board/session.ts";
|
|
21
|
+
import type { BoardStore } from "../../lib/board/store.ts";
|
|
22
|
+
import { messageOf } from "../../lib/errors.ts";
|
|
23
|
+
import { failedResult, type ToolFailure, textResult } from "../../lib/tool-result.ts";
|
|
24
|
+
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
25
|
+
import { callLine, detailsOrLine } from "../../lib/tui/tool.ts";
|
|
26
|
+
import { plural } from "../../lib/board/round-text.ts";
|
|
27
|
+
import { type Dispatch, handoffText, notListening, offerText, ownerFor, peerFor, sessionDispatch } from "./dispatch.ts";
|
|
28
|
+
import {
|
|
29
|
+
formatHandoff,
|
|
30
|
+
formatHandoffFailure,
|
|
31
|
+
formatOffer,
|
|
32
|
+
formatOfferFailure,
|
|
33
|
+
formatRacedAssignment,
|
|
34
|
+
formatReclaimed,
|
|
35
|
+
} from "./format.ts";
|
|
36
|
+
import { TeamParams } from "./schema.ts";
|
|
37
|
+
import { normalizeTeamAction, TEAM_ACTIONS, TEAM_TOOL, type TeamAction, type TeamDetails } from "./types.ts";
|
|
38
|
+
|
|
39
|
+
/** The parameters as the schema validates them. */
|
|
40
|
+
type TeamArgs = Static<typeof TeamParams>;
|
|
41
|
+
|
|
42
|
+
/** One refusal: the reason for the model, the state for the renderer. */
|
|
43
|
+
function refusal(action: TeamAction, message: string, card?: Card): ToolFailure<TeamDetails> {
|
|
44
|
+
return failedResult(`Error: ${message}`, { action, error: message, ...(card === undefined ? {} : { card }) });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The call line's subject: the verb, the card, and the peer it names. */
|
|
48
|
+
function callSubject(action: TeamAction, args: Record<string, unknown>): string {
|
|
49
|
+
const card = typeof args.card === "string" && args.card.trim() !== "" ? cardRef(args.card.trim()) : "";
|
|
50
|
+
const to = typeof args.to === "string" && args.to.trim() !== "" ? ` → ${args.to.trim()}` : "";
|
|
51
|
+
return card === "" ? action : `${action} ${card}${action === "assign" ? to : ""}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What the tool needs from the outside, so a suite drives it without a bus. */
|
|
55
|
+
export interface TeamToolsDeps {
|
|
56
|
+
/** Test seam: the handoff transport. */
|
|
57
|
+
dispatch?: (ctx: Parameters<typeof sessionDispatch>[0]) => Dispatch;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function registerTools(pi: ExtensionAPI, session: BoardSession, deps: TeamToolsDeps = {}): void {
|
|
61
|
+
const dispatchFor = deps.dispatch ?? sessionDispatch;
|
|
62
|
+
|
|
63
|
+
pi.registerTool({
|
|
64
|
+
name: TEAM_TOOL,
|
|
65
|
+
label: "Team",
|
|
66
|
+
description:
|
|
67
|
+
"Run a round of work over the shared board: `assign` hands a card to a running session and sends it a handoff " +
|
|
68
|
+
"(`to` is a teammate's label or id, as `/team` lists them); `offer` broadcasts an unclaimed card to every other " +
|
|
69
|
+
"live session and writes nothing, so the first to claim it takes it; `reclaim` takes a card back to To do and " +
|
|
70
|
+
"sends nothing, so it needs no team. Use `board` for everything else. Nothing here starts a peer's turn: a " +
|
|
71
|
+
"message waits in its inbox, and a peer that is listening reports it at its next turn.",
|
|
72
|
+
promptSnippet: "Run a round over the board: assign a card to a running session, offer one, or reclaim one.",
|
|
73
|
+
promptGuidelines: [
|
|
74
|
+
"Open the board with `/board` or work a card first, so this session and its peers are on the team.",
|
|
75
|
+
"Offer a card nobody has taken; assign one you mean a particular peer to hold.",
|
|
76
|
+
"When a result says `Not listening`, say so: the message waits for that peer's next turn.",
|
|
77
|
+
"Reclaim a card whose owner has gone quiet rather than editing someone else's work.",
|
|
78
|
+
],
|
|
79
|
+
parameters: TeamParams,
|
|
80
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
81
|
+
executionMode: "sequential",
|
|
82
|
+
|
|
83
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
84
|
+
const args = params as TeamArgs;
|
|
85
|
+
const action = normalizeTeamAction(args.action);
|
|
86
|
+
if (!action) {
|
|
87
|
+
return refusal("assign", `unknown action "${String(args.action)}"; use ${TEAM_ACTIONS.join(" or ")}.`);
|
|
88
|
+
}
|
|
89
|
+
const id = normalizeCardId(args.card);
|
|
90
|
+
if (id === undefined) return refusal(action, `"${String(args.card)}" is not a card id.`);
|
|
91
|
+
const me = session.who(ctx, ctx.cwd);
|
|
92
|
+
try {
|
|
93
|
+
const store = await session.store(ctx.cwd);
|
|
94
|
+
const now = session.now();
|
|
95
|
+
if (action === "reclaim") return await reclaimCard(store, me, id, now);
|
|
96
|
+
if (action === "offer") return await offerCard(store, id, args.note, now, dispatchFor(ctx));
|
|
97
|
+
const to = typeof args.to === "string" ? args.to.trim() : "";
|
|
98
|
+
if (to === "") return refusal("assign", 'assign needs a peer: pass `to`, e.g. "billing-core".');
|
|
99
|
+
return await assignCard(store, me, id, to, args.note, now, dispatchFor(ctx));
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return refusal(action, messageOf(error));
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
renderCall(args, theme, context) {
|
|
106
|
+
const action = normalizeTeamAction(args.action) ?? "assign";
|
|
107
|
+
return callLine(theme, context.lastComponent, TEAM_TOOL, callSubject(action, args as Record<string, unknown>));
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
renderResult(result, _options, theme, context) {
|
|
111
|
+
const text = reuseText(context?.lastComponent);
|
|
112
|
+
const details = toolDetails<TeamDetails>(result.details);
|
|
113
|
+
const outcome = detailsOrLine(theme, result, false, details);
|
|
114
|
+
text.setText("line" in outcome ? outcome.line : summaryOf(outcome.details));
|
|
115
|
+
return text;
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `assign`: check the preconditions, send the handoff, then set the owner.
|
|
122
|
+
*
|
|
123
|
+
* The preconditions are checked with the same `applyOp` the store will use, so an
|
|
124
|
+
* assignment the rules would refuse never sends a message at all.
|
|
125
|
+
*/
|
|
126
|
+
async function assignCard(
|
|
127
|
+
store: BoardStore,
|
|
128
|
+
me: Actor,
|
|
129
|
+
id: string,
|
|
130
|
+
to: string,
|
|
131
|
+
note: string | undefined,
|
|
132
|
+
now: number,
|
|
133
|
+
dispatch: Dispatch,
|
|
134
|
+
): Promise<ReturnType<typeof textResult<TeamDetails>> | ToolFailure<TeamDetails>> {
|
|
135
|
+
const card = await store.read(id);
|
|
136
|
+
if (!card) return refusal("assign", `${cardRef(id)} does not exist.`);
|
|
137
|
+
const owner: Actor = { label: to };
|
|
138
|
+
// The board, not just the card: a card that waits on unfinished work is refused here,
|
|
139
|
+
// so a handoff for it is never sent.
|
|
140
|
+
const dry = applyOp(card, { kind: "assign", to: owner }, me, now, indexOf(await store.list()));
|
|
141
|
+
if (!dry.ok) return refusal("assign", dry.error, card);
|
|
142
|
+
|
|
143
|
+
let delivered: string[];
|
|
144
|
+
try {
|
|
145
|
+
delivered = (await dispatch.send({ kind: "handoff", to, text: handoffText(card, to, note) })).delivered;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return refusal("assign", formatHandoffFailure(card, to, messageOf(error)), card);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Ask the bus which session answers to the label: an owner recorded with its id
|
|
151
|
+
// survives the peer renaming itself, which a label alone does not — and the same read
|
|
152
|
+
// says whether that peer will be woken by the message it just got.
|
|
153
|
+
const peers = await dispatch.peers().catch(() => undefined);
|
|
154
|
+
const peer = peerFor(peers, to);
|
|
155
|
+
const applied = await store.mutate(
|
|
156
|
+
id,
|
|
157
|
+
{ kind: "assign", to: peer === undefined ? ownerFor(to, delivered) : { id: peer.id, label: peer.label } },
|
|
158
|
+
me,
|
|
159
|
+
now,
|
|
160
|
+
);
|
|
161
|
+
if (!applied.ok)
|
|
162
|
+
return refusal("assign", formatRacedAssignment(applied.card ?? card, to, applied.error), applied.card ?? card);
|
|
163
|
+
const silent = notListening(peers, delivered);
|
|
164
|
+
return textResult(formatHandoff(applied.card, to, delivered, silent), {
|
|
165
|
+
action: "assign",
|
|
166
|
+
card: applied.card,
|
|
167
|
+
assigned: applied.card.owner,
|
|
168
|
+
delivered,
|
|
169
|
+
...(silent.length === 0 ? {} : { notListening: silent }),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The actor an offer's preconditions are judged as: nobody in particular.
|
|
175
|
+
*
|
|
176
|
+
* `claim` is what the offer asks a peer to do, so the dry run is a claim by someone who
|
|
177
|
+
* holds nothing — the rules' own refusals decide whether a card may be offered at all,
|
|
178
|
+
* without a second copy of the sentences.
|
|
179
|
+
*/
|
|
180
|
+
const OFFER_CLAIMANT: Actor = { label: "*" };
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* `offer`: check the claim's preconditions, broadcast the card, and write nothing.
|
|
184
|
+
*
|
|
185
|
+
* The order matters less here than in `assign`, because nothing is written: a failed
|
|
186
|
+
* broadcast leaves the card exactly as it was, and the first peer to claim is what makes
|
|
187
|
+
* an offer into work. Two peers racing are settled by the store's compare-and-set.
|
|
188
|
+
*/
|
|
189
|
+
async function offerCard(
|
|
190
|
+
store: BoardStore,
|
|
191
|
+
id: string,
|
|
192
|
+
note: string | undefined,
|
|
193
|
+
now: number,
|
|
194
|
+
dispatch: Dispatch,
|
|
195
|
+
): Promise<ReturnType<typeof textResult<TeamDetails>> | ToolFailure<TeamDetails>> {
|
|
196
|
+
const card = await store.read(id);
|
|
197
|
+
if (!card) return refusal("offer", `${cardRef(id)} does not exist.`);
|
|
198
|
+
// The board, not just the card: a card that waits on unfinished work, or one somebody
|
|
199
|
+
// holds, is refused here, so a broadcast for it is never sent.
|
|
200
|
+
const dry = applyOp(card, { kind: "claim" }, OFFER_CLAIMANT, now, indexOf(await store.list()));
|
|
201
|
+
if (!dry.ok) return refusal("offer", dry.error, card);
|
|
202
|
+
|
|
203
|
+
let delivered: string[];
|
|
204
|
+
try {
|
|
205
|
+
delivered = (await dispatch.send({ kind: "event", to: "*", text: offerText(card, note) })).delivered;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
return refusal("offer", formatOfferFailure(card, messageOf(error)), card);
|
|
208
|
+
}
|
|
209
|
+
const peers = await dispatch.peers().catch(() => undefined);
|
|
210
|
+
const silent = notListening(peers, delivered);
|
|
211
|
+
return textResult(formatOffer(card, delivered, silent), {
|
|
212
|
+
action: "offer",
|
|
213
|
+
card,
|
|
214
|
+
delivered,
|
|
215
|
+
...(silent.length === 0 ? {} : { notListening: silent }),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** `reclaim`: the owner clears and the card returns to To do, notes and all. */
|
|
220
|
+
async function reclaimCard(
|
|
221
|
+
store: BoardStore,
|
|
222
|
+
me: Actor,
|
|
223
|
+
id: string,
|
|
224
|
+
now: number,
|
|
225
|
+
): Promise<ReturnType<typeof textResult<TeamDetails>> | ToolFailure<TeamDetails>> {
|
|
226
|
+
const before = await store.read(id);
|
|
227
|
+
if (!before) return refusal("reclaim", `${cardRef(id)} does not exist.`);
|
|
228
|
+
const previous = before.owner?.label;
|
|
229
|
+
if (previous === undefined) return refusal("reclaim", `${cardRef(id)} has no owner to reclaim.`, before);
|
|
230
|
+
const result = await store.mutate(id, { kind: "reclaim" }, me, now);
|
|
231
|
+
if (!result.ok) return refusal("reclaim", result.error, result.card ?? before);
|
|
232
|
+
return textResult(formatReclaimed(result.card, previous), { action: "reclaim", card: result.card });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The collapsed line for a successful call, from its details. */
|
|
236
|
+
function summaryOf(details: TeamDetails): string {
|
|
237
|
+
const card = details.card;
|
|
238
|
+
if (!card) return details.action === "reclaim" ? "reclaimed" : "handed off";
|
|
239
|
+
if (details.action === "offer") {
|
|
240
|
+
const count = details.delivered?.length ?? 0;
|
|
241
|
+
return `${cardRef(card.id)} offered to ${plural(count, "session")}`;
|
|
242
|
+
}
|
|
243
|
+
const who = details.assigned?.label ?? card.owner?.label;
|
|
244
|
+
const target = details.action === "assign" && who !== undefined ? ` → ${who}` : "";
|
|
245
|
+
return `${cardRef(card.id)} ${card.status}${target}`;
|
|
246
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes `team` registers and reports.
|
|
3
|
+
*
|
|
4
|
+
* A `team` result is one card plus what happened to the handoff, so the details say
|
|
5
|
+
* both: `assigned` is who the card went to, `delivered` is the labels the bus wrote
|
|
6
|
+
* a message file for, and `messageError` is the send that never landed — a card is
|
|
7
|
+
* never left in a state the report does not describe.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Card } from "../../lib/board/card.ts";
|
|
11
|
+
import type { CardOwner } from "../../lib/board/card.ts";
|
|
12
|
+
|
|
13
|
+
/** The tool's name. `board` owns the cards; `team` owns the round. */
|
|
14
|
+
export const TEAM_TOOL = "team";
|
|
15
|
+
|
|
16
|
+
/** The three actions: the lead's whole surface — hand out, offer, take back. */
|
|
17
|
+
export const TEAM_ACTIONS = ["assign", "offer", "reclaim"] as const;
|
|
18
|
+
|
|
19
|
+
/** One action. */
|
|
20
|
+
export type TeamAction = (typeof TEAM_ACTIONS)[number];
|
|
21
|
+
|
|
22
|
+
/** What one call did. */
|
|
23
|
+
export interface TeamDetails {
|
|
24
|
+
action: TeamAction;
|
|
25
|
+
/** The card after the action, or the one a refusal left alone. */
|
|
26
|
+
card?: Card;
|
|
27
|
+
/** Who the card was assigned to, when the assignment happened. */
|
|
28
|
+
assigned?: CardOwner;
|
|
29
|
+
/**
|
|
30
|
+
* The labels a message file was written for, when the send landed: the one peer for
|
|
31
|
+
* `assign`, every other live session for `offer`.
|
|
32
|
+
*/
|
|
33
|
+
delivered?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* The peers the message reached whose record says they are not listening, so it waits
|
|
36
|
+
* for their next turn instead of being raised as one. Absent when every peer listens.
|
|
37
|
+
*/
|
|
38
|
+
notListening?: string[];
|
|
39
|
+
/** Why the handoff was not sent; the card is untouched when this is set. */
|
|
40
|
+
messageError?: string;
|
|
41
|
+
/** A refusal's reason, which the renderer draws instead of the details. */
|
|
42
|
+
error?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Whether a value is one of the actions, so an unknown one is a refusal and not a crash. */
|
|
46
|
+
export function normalizeTeamAction(raw: unknown): TeamAction | undefined {
|
|
47
|
+
return typeof raw === "string" && TEAM_ACTIONS.includes(raw as TeamAction) ? (raw as TeamAction) : undefined;
|
|
48
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { WebFetchConfig } from "./config.ts";
|
|
13
13
|
import { extractReadable } from "./extract.ts";
|
|
14
|
+
import { renderStructured } from "./structured.ts";
|
|
14
15
|
import { extractPdfText, type PdfExtractor } from "../../../lib/pdf.ts";
|
|
15
16
|
import { createFetchRunner, type HttpRunner, type HttpResponse } from "../http.ts";
|
|
16
17
|
import { decodeHtml } from "../charset.ts";
|
|
@@ -157,7 +158,9 @@ export async function runFetch(
|
|
|
157
158
|
title = page.title;
|
|
158
159
|
text = page.text;
|
|
159
160
|
} else if (isTextualType(response.contentType)) {
|
|
160
|
-
|
|
161
|
+
// Rendered here, once per fetch, because the result is what the cache stores:
|
|
162
|
+
// paging and `find` then read one stable string instead of a fresh rendering.
|
|
163
|
+
text = renderStructured(response.body, response.contentType) ?? response.body;
|
|
161
164
|
} else {
|
|
162
165
|
text = `(binary content: ${response.contentType || "unknown"}, ${response.sizeBytes} bytes; web_fetch returns text only)`;
|
|
163
166
|
}
|