@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
package/lib/qr.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A QR code as terminal lines or an inline SVG.
|
|
3
|
+
*
|
|
4
|
+
* Uses the `qrcode` package only for the module matrix, then draws Unicode
|
|
5
|
+
* half-blocks for a terminal and hands the package's SVG to a page. The package
|
|
6
|
+
* loads on the first QR, not when an extension registers: a session that never
|
|
7
|
+
* shows one pays nothing for it. No ANSI is emitted; the caller decides colours,
|
|
8
|
+
* and the width is the matrix width plus a quiet zone.
|
|
9
|
+
*
|
|
10
|
+
* Two surfaces share this: `wechat` shows a login QR, and `plan` shows a review
|
|
11
|
+
* link a teammate can scan.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface QrModules {
|
|
15
|
+
size: number;
|
|
16
|
+
get(row: number, col: number): boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** QR error-correction level, one setting for both surfaces. */
|
|
20
|
+
const ERROR_CORRECTION = "M" as const;
|
|
21
|
+
|
|
22
|
+
/** Build the module matrix for `text`, loading `qrcode` on first use. */
|
|
23
|
+
export async function qrMatrix(text: string): Promise<QrModules> {
|
|
24
|
+
const { default: QRCode } = await import("qrcode");
|
|
25
|
+
const qr = QRCode.create(text, { errorCorrectionLevel: ERROR_CORRECTION });
|
|
26
|
+
return qr.modules as unknown as QrModules;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The QR as terminal lines. Each string is exactly `matrix.size + 2 * quiet`
|
|
31
|
+
* columns wide (half-block characters are single width), so a caller can test
|
|
32
|
+
* `qrLines(text)[0].length` against the terminal width.
|
|
33
|
+
*/
|
|
34
|
+
export async function qrLines(text: string, options: { quiet?: number } = {}): Promise<string[]> {
|
|
35
|
+
const quiet = Math.max(0, options.quiet ?? 2);
|
|
36
|
+
const matrix = await qrMatrix(text);
|
|
37
|
+
const size = matrix.size;
|
|
38
|
+
const isDark = (row: number, col: number): boolean =>
|
|
39
|
+
row >= 0 && col >= 0 && row < size && col < size && matrix.get(row, col);
|
|
40
|
+
|
|
41
|
+
const lines: string[] = [];
|
|
42
|
+
for (let y = -quiet; y < size + quiet; y += 2) {
|
|
43
|
+
let line = "";
|
|
44
|
+
for (let x = -quiet; x < size + quiet; x += 1) {
|
|
45
|
+
const top = isDark(y, x);
|
|
46
|
+
const bottom = isDark(y + 1, x);
|
|
47
|
+
line += top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " ";
|
|
48
|
+
}
|
|
49
|
+
lines.push(line);
|
|
50
|
+
}
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The QR as an inline SVG, for a page that can paste markup instead of pixels.
|
|
56
|
+
*
|
|
57
|
+
* The markup is generated by the package from a URL this process built, so it
|
|
58
|
+
* carries no caller-supplied text; a page still serves it under its own CSP,
|
|
59
|
+
* which allows no external image.
|
|
60
|
+
*/
|
|
61
|
+
export async function qrSvg(text: string): Promise<string> {
|
|
62
|
+
const { default: QRCode } = await import("qrcode");
|
|
63
|
+
return QRCode.toString(text, { type: "svg", errorCorrectionLevel: ERROR_CORRECTION, margin: 1 });
|
|
64
|
+
}
|
package/lib/repo-key.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-repository storage key that outlives one checkout.
|
|
3
|
+
*
|
|
4
|
+
* Two extensions file state by repository — `session-bus` its sessions, mail, and
|
|
5
|
+
* claims, `plan` its plans — and both must resolve the same key from any
|
|
6
|
+
* worktree, so a plan approved in `.pi/worktrees/x` is the same plan from the
|
|
7
|
+
* main checkout. The key is the repository's *common* git directory, which every
|
|
8
|
+
* worktree of that repository shares; outside a repository the working directory
|
|
9
|
+
* is the identity.
|
|
10
|
+
*
|
|
11
|
+
* Hashing keeps a mail directory, a claims directory, and a plans directory short
|
|
12
|
+
* enough to read and safe on every filesystem, while staying unique per
|
|
13
|
+
* repository.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { realpathSync } from "node:fs";
|
|
18
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { createExecRunner, gitCommonDir, repoRoot } from "./git/index.ts";
|
|
20
|
+
|
|
21
|
+
/** Hex characters kept from the digest: collision-free in practice and short enough for a path. */
|
|
22
|
+
export const REPO_KEY_LENGTH = 12;
|
|
23
|
+
|
|
24
|
+
/** The key for one identity string, hashed and shortened. */
|
|
25
|
+
export function repoKeyOf(value: string): string {
|
|
26
|
+
return createHash("sha256").update(canonical(value)).digest("hex").slice(0, REPO_KEY_LENGTH);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One path, one spelling.
|
|
31
|
+
*
|
|
32
|
+
* git prints the path it was told to work from, so a repository reached through a
|
|
33
|
+
* symlinked prefix reports a different common dir for the same repository. Two
|
|
34
|
+
* spellings would file one repository's plans and claims under two keys, so the
|
|
35
|
+
* real path is what gets hashed; a path that cannot be resolved keeps its own
|
|
36
|
+
* spelling rather than failing.
|
|
37
|
+
*/
|
|
38
|
+
function canonical(value: string): string {
|
|
39
|
+
try {
|
|
40
|
+
return realpathSync.native(value);
|
|
41
|
+
} catch {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A resolved repository identity. */
|
|
47
|
+
export interface RepoKey {
|
|
48
|
+
/** Absolute repository root, or `cwd` when git did not answer. */
|
|
49
|
+
root: string;
|
|
50
|
+
/** The shared key, derived from the common git directory. */
|
|
51
|
+
key: string;
|
|
52
|
+
/**
|
|
53
|
+
* Whether git resolved the repository. A caller that needs a *project-local*
|
|
54
|
+
* location branches on this; one that only needs a stable directory does not.
|
|
55
|
+
*/
|
|
56
|
+
fromGit: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the repository identity for `cwd`.
|
|
61
|
+
*
|
|
62
|
+
* Never throws: without git, or when a git call fails, the working directory is
|
|
63
|
+
* the identity — which is what a caller outside a repository wants anyway.
|
|
64
|
+
*/
|
|
65
|
+
export async function repoKeyFor(pi: Pick<ExtensionAPI, "exec">, cwd: string): Promise<RepoKey> {
|
|
66
|
+
try {
|
|
67
|
+
const run = createExecRunner(pi);
|
|
68
|
+
const [common, root] = await Promise.all([gitCommonDir(run, cwd), repoRoot(run, cwd)]);
|
|
69
|
+
if (root) return { root, key: repoKeyOf(common ?? root), fromGit: true };
|
|
70
|
+
} catch {
|
|
71
|
+
// Fall through: no git, or a git call that failed, is the same answer here.
|
|
72
|
+
}
|
|
73
|
+
return { root: cwd, key: repoKeyOf(cwd), fromGit: false };
|
|
74
|
+
}
|
package/lib/report.ts
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
* The pending-report coordinator: how a live feature tells the model what
|
|
3
3
|
* happened without interrupting it.
|
|
4
4
|
*
|
|
5
|
-
* A watcher match
|
|
6
|
-
* and raise at most one triggered turn when the work
|
|
7
|
-
*
|
|
5
|
+
* A watcher match, a finished job, and arriving mail follow one rule — report at
|
|
6
|
+
* the next turn, and raise at most one triggered turn per burst when the work
|
|
7
|
+
* opted into `wake`, as soon as the agent is idle. The three traps are subtle
|
|
8
|
+
* enough to have been re-derived once per extension:
|
|
8
9
|
*
|
|
9
10
|
* - a wake is only safe while the agent is idle, so an outcome that arrives
|
|
10
|
-
* mid-turn
|
|
11
|
+
* mid-turn is queued rather than sent, and `agent_settled` retries it;
|
|
12
|
+
* - an outcome outlives the run it arrived in, so the retry asks
|
|
13
|
+
* `wakeRequested()` rather than trusting the one call that queued it;
|
|
11
14
|
* - between the triggered send and the `agent_start` it causes, the agent still
|
|
12
15
|
* looks idle, so a second outcome in that window must not trigger a second
|
|
13
16
|
* turn. `waking` closes that window.
|
|
@@ -33,6 +36,11 @@ export interface PendingReporterOptions<T> {
|
|
|
33
36
|
markReported(items: T[]): void;
|
|
34
37
|
/** Build the notice for one burst: model text plus renderer details. */
|
|
35
38
|
buildNotice(items: T[]): NoticeMessage;
|
|
39
|
+
/**
|
|
40
|
+
* Whether any pending outcome asks for a triggered turn. Called by the settle
|
|
41
|
+
* retry, so a mid-turn outcome still wakes once the agent is idle.
|
|
42
|
+
*/
|
|
43
|
+
wakeRequested(): boolean;
|
|
36
44
|
/** The notice's source, so only its own stale entries are pruned. */
|
|
37
45
|
source: string;
|
|
38
46
|
/** Runs after a wake turn was successfully requested, for an extra announcement. */
|
|
@@ -47,8 +55,8 @@ export interface PendingReporter {
|
|
|
47
55
|
sessionStart(): void;
|
|
48
56
|
/** A turn began: outcomes queue up, and none of them triggers a turn. */
|
|
49
57
|
agentStart(): void;
|
|
50
|
-
/** The turn settled: a
|
|
51
|
-
agentSettled(): void;
|
|
58
|
+
/** The turn settled: retry a wake it suppressed, unless it was aborted. */
|
|
59
|
+
agentSettled(event?: { aborted?: boolean }): void;
|
|
52
60
|
/** Report what is queued at the start of a turn, without triggering one. */
|
|
53
61
|
beforeAgentStart(): { message: NoticeMessage } | undefined;
|
|
54
62
|
/** Drop this source's older notices from the outgoing context. */
|
|
@@ -56,26 +64,29 @@ export interface PendingReporter {
|
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
export function createPendingReporter<T>(options: PendingReporterOptions<T>): PendingReporter {
|
|
59
|
-
const { wake, pending, markReported, buildNotice, source, onWoke } = options;
|
|
67
|
+
const { wake, pending, markReported, buildNotice, wakeRequested, source, onWoke } = options;
|
|
60
68
|
let busy = false;
|
|
61
69
|
let waking = false;
|
|
62
70
|
|
|
71
|
+
const report = (): void => {
|
|
72
|
+
if (busy || waking) return;
|
|
73
|
+
if (!wakeRequested()) return;
|
|
74
|
+
const items = pending();
|
|
75
|
+
if (items.length === 0) return;
|
|
76
|
+
try {
|
|
77
|
+
wake(buildNotice(items));
|
|
78
|
+
} catch {
|
|
79
|
+
// Reporting is best-effort; the next turn reports the same items.
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Mark only after the send succeeded, so a throw changes nothing.
|
|
83
|
+
markReported(items);
|
|
84
|
+
waking = true;
|
|
85
|
+
onWoke?.(items);
|
|
86
|
+
};
|
|
87
|
+
|
|
63
88
|
return {
|
|
64
|
-
report
|
|
65
|
-
if (busy || waking) return;
|
|
66
|
-
const items = pending();
|
|
67
|
-
if (items.length === 0) return;
|
|
68
|
-
try {
|
|
69
|
-
wake(buildNotice(items));
|
|
70
|
-
} catch {
|
|
71
|
-
// Reporting is best-effort; the next turn reports the same items.
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
// Mark only after the send succeeded, so a throw changes nothing.
|
|
75
|
-
markReported(items);
|
|
76
|
-
waking = true;
|
|
77
|
-
onWoke?.(items);
|
|
78
|
-
},
|
|
89
|
+
report,
|
|
79
90
|
sessionStart: () => {
|
|
80
91
|
busy = false;
|
|
81
92
|
waking = false;
|
|
@@ -83,9 +94,13 @@ export function createPendingReporter<T>(options: PendingReporterOptions<T>): Pe
|
|
|
83
94
|
agentStart: () => {
|
|
84
95
|
busy = true;
|
|
85
96
|
},
|
|
86
|
-
agentSettled: () => {
|
|
97
|
+
agentSettled: (event) => {
|
|
87
98
|
busy = false;
|
|
88
99
|
waking = false;
|
|
100
|
+
// An interrupted run is not a request to interrupt again; its outcomes
|
|
101
|
+
// stay queued for the next turn.
|
|
102
|
+
if (event?.aborted === true) return;
|
|
103
|
+
report();
|
|
89
104
|
},
|
|
90
105
|
beforeAgentStart: () => {
|
|
91
106
|
const items = pending();
|
|
@@ -103,10 +118,14 @@ export function createPendingReporter<T>(options: PendingReporterOptions<T>): Pe
|
|
|
103
118
|
|
|
104
119
|
/** The session-scoped state a channel drives, narrowed to what its hooks need. */
|
|
105
120
|
export interface PendingChannelRuntime {
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Load this session's records on every session start. A promise is awaited, so a
|
|
123
|
+
* runtime that resolves a repository or opens a store is fully loaded before the
|
|
124
|
+
* report trigger is armed.
|
|
125
|
+
*/
|
|
126
|
+
load(ctx: ExtensionContext): void | Promise<void>;
|
|
127
|
+
/** Repaint the status chip; a runtime that publishes none omits it. */
|
|
128
|
+
setStatus?(ctx: ExtensionContext): void;
|
|
110
129
|
/** Release the session's resources; idempotent. */
|
|
111
130
|
shutdown(): Promise<void>;
|
|
112
131
|
}
|
|
@@ -135,29 +154,29 @@ export function registerPendingChannel(
|
|
|
135
154
|
pi: Pick<ExtensionAPI, "on">,
|
|
136
155
|
{ reporter, runtime, onSessionStart, onSessionShutdown }: PendingChannelOptions,
|
|
137
156
|
): void {
|
|
138
|
-
pi.on("session_start", (_event, ctx) => {
|
|
157
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
139
158
|
reporter.sessionStart();
|
|
140
|
-
runtime.load(ctx);
|
|
159
|
+
await runtime.load(ctx);
|
|
141
160
|
onSessionStart?.(ctx);
|
|
142
161
|
});
|
|
143
162
|
|
|
144
163
|
// Tree navigation does not change the pending records; only repaint.
|
|
145
164
|
pi.on("session_tree", (_event, ctx) => {
|
|
146
|
-
runtime.setStatus(ctx);
|
|
165
|
+
runtime.setStatus?.(ctx);
|
|
147
166
|
});
|
|
148
167
|
|
|
149
168
|
pi.on("agent_start", () => {
|
|
150
169
|
reporter.agentStart();
|
|
151
170
|
});
|
|
152
|
-
pi.on("agent_settled", () => {
|
|
153
|
-
reporter.agentSettled();
|
|
171
|
+
pi.on("agent_settled", (event) => {
|
|
172
|
+
reporter.agentSettled(event);
|
|
154
173
|
});
|
|
155
174
|
|
|
156
175
|
// Report what is queued at the start of the next turn, without triggering one.
|
|
157
176
|
pi.on("before_agent_start", (_event, ctx) => {
|
|
158
177
|
const reported = reporter.beforeAgentStart();
|
|
159
178
|
if (!reported) return undefined;
|
|
160
|
-
runtime.setStatus(ctx);
|
|
179
|
+
runtime.setStatus?.(ctx);
|
|
161
180
|
return reported;
|
|
162
181
|
});
|
|
163
182
|
|
package/lib/requirements.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-extension runtime prerequisites
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* a
|
|
6
|
-
* extensions that own an external dependency format their error text from the
|
|
7
|
-
* same entries, so the hint a model sees in a tool error and the hint `doctor`
|
|
8
|
-
* prints cannot drift apart.
|
|
2
|
+
* Per-extension runtime prerequisites: one place that says what each extension needs
|
|
3
|
+
* in order to actually work, plus a pure probe over injected seams. `doctor` reports
|
|
4
|
+
* on this table, and the extensions that own an external dependency format their error
|
|
5
|
+
* text from the same entries, so a tool error and a `doctor` line cannot drift apart.
|
|
9
6
|
*
|
|
10
7
|
* The table is data: no module state, no I/O, no host access. Everything a probe
|
|
11
8
|
* needs (running a command, reading a file, loading an optional package, the
|
|
@@ -19,7 +16,6 @@ import { configSection, configValueAt, type JsonReadResult, SHARED_CONFIG_FILE }
|
|
|
19
16
|
import type { WebSearchProviderEvent } from "./events.ts";
|
|
20
17
|
import { pdfGuidance } from "./pdf.ts";
|
|
21
18
|
import { getPiInvocation, type PiInvocation } from "./pi-invocation.ts";
|
|
22
|
-
|
|
23
19
|
/** How a check came out. Only `missing` is worth interrupting a session for. */
|
|
24
20
|
export type FindingSeverity = "ok" | "missing" | "degraded" | "info";
|
|
25
21
|
|
|
@@ -212,11 +208,12 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
212
208
|
},
|
|
213
209
|
todo: {},
|
|
214
210
|
goal: {},
|
|
211
|
+
board: {},
|
|
212
|
+
team: { notes: ["`assign` needs `session-bus` loaded to deliver a handoff; without it the card is untouched."] },
|
|
215
213
|
check: {
|
|
216
214
|
notes: ["Runs the project's detected command through a shell; a stack with no detector needs `check.command`."],
|
|
217
215
|
},
|
|
218
216
|
plan: {
|
|
219
|
-
optional: [gitCheck("project-local `.pi/plans` (otherwise plans land in the agent directory)")],
|
|
220
217
|
notes: ["Needs an interactive UI to enter plan mode."],
|
|
221
218
|
},
|
|
222
219
|
subagent: {
|
|
@@ -267,6 +264,7 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
267
264
|
optional: [gitCheck("project-scope memory under the repository root")],
|
|
268
265
|
},
|
|
269
266
|
context: {},
|
|
267
|
+
"session-bus": {},
|
|
270
268
|
wechat: {
|
|
271
269
|
optional: [
|
|
272
270
|
{
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session label: the one name another session addresses this one by.
|
|
3
|
+
*
|
|
4
|
+
* `session-bus` settles a label for every session — the Pi session name, else the
|
|
5
|
+
* working directory's basename, else the id — and the board records the same name
|
|
6
|
+
* on a card it claims, so the two surfaces agree on who "billing-core" is without
|
|
7
|
+
* the board reading the bus. The rules are here rather than in either extension
|
|
8
|
+
* because a label is a shared vocabulary: it is displayed, it is a send target, and
|
|
9
|
+
* it is stored in a card file that outlives one session.
|
|
10
|
+
*
|
|
11
|
+
* Value-only: no state, no I/O.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { sanitizeLine } from "./format.ts";
|
|
16
|
+
|
|
17
|
+
/** Longest label, in characters, that a session may be addressed by. */
|
|
18
|
+
export const MAX_LABEL = 40;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A safe label, used both for display and as a target.
|
|
22
|
+
*
|
|
23
|
+
* Flattened to one line and capped, so a name can never wrap a row or open a
|
|
24
|
+
* second line in a page; an empty one falls back rather than leaving a blank chip.
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeLabel(raw: string | undefined, fallback: string): string {
|
|
27
|
+
const text = sanitizeLine(raw ?? "")
|
|
28
|
+
.replace(/\s+/g, " ")
|
|
29
|
+
.trim();
|
|
30
|
+
return (text || fallback).slice(0, MAX_LABEL);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A short, display-only suffix for a session id. */
|
|
34
|
+
export function shortId(id: string): string {
|
|
35
|
+
const clean = id.replace(/[^a-zA-Z0-9]/g, "");
|
|
36
|
+
return (clean || id).slice(-6);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The automatic label, before uniqueness is applied.
|
|
41
|
+
*
|
|
42
|
+
* The bus appends a suffix when two live sessions share one; that decision belongs
|
|
43
|
+
* to the registry, so this is only the name a session would pick for itself.
|
|
44
|
+
*/
|
|
45
|
+
export function defaultLabel(sessionName: string | undefined, cwd: string, id: string): string {
|
|
46
|
+
const directory = cwd.split(/[\\/]/).filter(Boolean).at(-1);
|
|
47
|
+
const base = normalizeLabel(sessionName, normalizeLabel(directory, "session"));
|
|
48
|
+
return normalizeLabel(base, `session-${shortId(id)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Pi's session name, which is the first choice for a label.
|
|
53
|
+
*
|
|
54
|
+
* A host that has no session name, or refuses the call, yields `undefined` rather
|
|
55
|
+
* than an error: a label is a convenience, and a session that cannot name itself
|
|
56
|
+
* still works under the fallback chain above.
|
|
57
|
+
*/
|
|
58
|
+
export function sessionNameOf(ctx: Pick<ExtensionContext, "sessionManager">): string | undefined {
|
|
59
|
+
try {
|
|
60
|
+
return ctx.sessionManager.getSessionName();
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is on this repository's session bus, read as data.
|
|
3
|
+
*
|
|
4
|
+
* The Team tab lists teammates, and `session-bus` owns those records. Neither
|
|
5
|
+
* extension may import the other, and the board must keep working when the bus is
|
|
6
|
+
* not loaded — so the records are read where the bus already put them,
|
|
7
|
+
* `<agentDir>/session-bus/<repo-key>/`, as files rather than through its code. The
|
|
8
|
+
* directory names, the record fields this reader trusts, and the liveness rule live
|
|
9
|
+
* here once, and `session-bus` imports them, so a layout change cannot land in one
|
|
10
|
+
* copy and not the other.
|
|
11
|
+
*
|
|
12
|
+
* A missing directory is an answer, not an error: it means no session has ever
|
|
13
|
+
* joined this repository, and a caller renders that differently from an empty bus.
|
|
14
|
+
*
|
|
15
|
+
* Pure I/O, no state: one read answers one caller, and nothing is cached — a
|
|
16
|
+
* heartbeat is ten seconds old at most, and the caller polls on its own clock.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
/** The bus directory under the agent dir, `session-bus`. */
|
|
23
|
+
export const BUS_DIR_NAME = "session-bus";
|
|
24
|
+
/** One file per session inside it. */
|
|
25
|
+
export const SESSIONS_DIR_NAME = "sessions";
|
|
26
|
+
/** One directory per session, holding its mail. */
|
|
27
|
+
export const INBOX_DIR_NAME = "inbox";
|
|
28
|
+
/** How long a record's heartbeat stays believable: the rule the bus prunes by. */
|
|
29
|
+
export const SESSION_TTL_MS = 10 * 60_000;
|
|
30
|
+
|
|
31
|
+
/** One session as the page shows it. The rest of the record stays on disk. */
|
|
32
|
+
export interface RosterSession {
|
|
33
|
+
id: string;
|
|
34
|
+
label: string;
|
|
35
|
+
mode: string;
|
|
36
|
+
/** When the session last proved it was alive. */
|
|
37
|
+
lastSeen: number;
|
|
38
|
+
/** Messages waiting in its inbox. */
|
|
39
|
+
mail: number;
|
|
40
|
+
/**
|
|
41
|
+
* Whether the session is listening for mail, when its record says.
|
|
42
|
+
*
|
|
43
|
+
* Absent is "not listening": a record written before this existed and a session that
|
|
44
|
+
* stopped listening read the same way, so a reader never claims a peer will be reached.
|
|
45
|
+
*/
|
|
46
|
+
watching?: { armed: boolean; wake: boolean };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether a record's heartbeat is fresh enough to call the session live. */
|
|
50
|
+
export function rosterStateOf(session: RosterSession, now: number, ttlMs = SESSION_TTL_MS): "live" | "quiet" {
|
|
51
|
+
return now - session.lastSeen <= ttlMs ? "live" : "quiet";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The fields this reader trusts; anything else in the file is the bus's business. */
|
|
55
|
+
interface SessionPayload {
|
|
56
|
+
id: string;
|
|
57
|
+
label: string;
|
|
58
|
+
mode: string;
|
|
59
|
+
lastSeen: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isSessionPayload(value: unknown): value is SessionPayload {
|
|
63
|
+
if (typeof value !== "object" || value === null) return false;
|
|
64
|
+
const { id, label, mode, lastSeen } = value as Record<string, unknown>;
|
|
65
|
+
return (
|
|
66
|
+
typeof id === "string" && typeof label === "string" && typeof mode === "string" && typeof lastSeen === "number"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The listening field, read only when it is the shape the bus writes.
|
|
72
|
+
*
|
|
73
|
+
* A malformed field is not a reason to hide a teammate: the row still draws, and the
|
|
74
|
+
* only thing lost is the claim that mail will reach it. Exported because the field is
|
|
75
|
+
* the record's, and `lib/session-tool.ts` reads the same one over the tool rather than
|
|
76
|
+
* over the files — one reader for one shape.
|
|
77
|
+
*/
|
|
78
|
+
export function watchingOf(value: unknown): { armed: boolean; wake: boolean } | undefined {
|
|
79
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
80
|
+
const { armed, wake } = value as Record<string, unknown>;
|
|
81
|
+
return typeof armed === "boolean" && typeof wake === "boolean" ? { armed, wake } : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** How many message files one session's inbox holds. A missing inbox is empty. */
|
|
85
|
+
async function mailCount(dir: string, stem: string): Promise<number> {
|
|
86
|
+
try {
|
|
87
|
+
const entries = await readdir(join(dir, INBOX_DIR_NAME, stem));
|
|
88
|
+
return entries.filter((name) => name.endsWith(".json")).length;
|
|
89
|
+
} catch {
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Every session the bus has recorded for one repository, or `undefined` when the bus
|
|
96
|
+
* has never been used here.
|
|
97
|
+
*
|
|
98
|
+
* One file that does not parse is skipped rather than thrown: a roster with three
|
|
99
|
+
* good sessions is worth more than an error about the fourth.
|
|
100
|
+
*/
|
|
101
|
+
export async function readRoster(dir: string): Promise<RosterSession[] | undefined> {
|
|
102
|
+
let names: string[];
|
|
103
|
+
try {
|
|
104
|
+
names = await readdir(join(dir, SESSIONS_DIR_NAME));
|
|
105
|
+
} catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const sessions: RosterSession[] = [];
|
|
109
|
+
for (const name of names) {
|
|
110
|
+
if (!name.endsWith(".json")) continue;
|
|
111
|
+
let parsed: unknown;
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(await readFile(join(dir, SESSIONS_DIR_NAME, name), "utf-8"));
|
|
114
|
+
} catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!isSessionPayload(parsed)) continue;
|
|
118
|
+
const { id, label, mode, lastSeen } = parsed;
|
|
119
|
+
const watching = watchingOf((parsed as { watching?: unknown }).watching);
|
|
120
|
+
sessions.push({
|
|
121
|
+
id,
|
|
122
|
+
label,
|
|
123
|
+
mode,
|
|
124
|
+
lastSeen,
|
|
125
|
+
mail: await mailCount(dir, name.slice(0, -".json".length)),
|
|
126
|
+
...(watching === undefined ? {} : { watching }),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return sessions.sort((a, b) => a.label.localeCompare(b.label) || a.id.localeCompare(b.id));
|
|
130
|
+
}
|