@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 +29 -15
- package/README.zh-CN.md +27 -15
- package/extensions/bytefly/README.md +195 -115
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +29 -39
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +89 -22
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +74 -459
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +26 -152
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +27 -6
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +10 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +35 -52
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/README.md +3 -3
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +333 -84
- package/extensions/plan/commands.ts +73 -7
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +184 -58
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +95 -95
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +123 -107
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +17 -12
- package/extensions/plan/tools/exit.ts +148 -59
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +29 -31
- package/extensions/plan/tools.ts +6 -3
- package/extensions/plan/tui.ts +60 -15
- package/extensions/plan/types.ts +45 -3
- package/extensions/plan/web-chrome.ts +102 -0
- package/extensions/plan/web-client.ts +177 -446
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +131 -19
- package/extensions/plan/web-router.ts +148 -32
- package/extensions/plan/web-share-session.ts +197 -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 +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +26 -13
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +9 -5
- package/extensions/watch/index.ts +25 -44
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +45 -36
- package/extensions/wechat/bridge.ts +46 -48
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +3 -3
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +54 -18
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +9 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +53 -23
- package/extensions/wechat/reply-pipeline.ts +12 -4
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -40
- package/lib/README.md +12 -7
- package/lib/claims/overlap.ts +43 -0
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +28 -1
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +117 -25
- package/lib/requirements.ts +1 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +30 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +91 -19
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +5 -2
- package/skills/changelog/SKILL.md +57 -45
- 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 +209 -0
- package/skills/parallel-dev/SKILL.md +150 -0
- package/extensions/wechat/qrcode.ts +0 -46
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The plan review gate: ask the human about a plan file and return one decision.
|
|
3
3
|
*
|
|
4
|
-
* A review has three presentations — the scrollable TUI screen, a
|
|
5
|
-
* editor
|
|
6
|
-
*
|
|
4
|
+
* A review has three presentations — the scrollable TUI screen, a dialog-only
|
|
5
|
+
* editor, and a remote turn's compact editor prompt — and one optional browser
|
|
6
|
+
* server. This module owns the loop that switches between
|
|
7
7
|
* them, so `exit_plan_mode` and `/plan review` share one lifecycle and only map
|
|
8
8
|
* the returned {@link ReviewDecision} to their own result.
|
|
9
9
|
*
|
|
@@ -12,14 +12,15 @@
|
|
|
12
12
|
* signal reaches the browser server so an aborted turn closes it.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
16
15
|
import { basename } from "node:path";
|
|
17
|
-
import {
|
|
16
|
+
import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { askHuman, isRemoteTurn, remoteLanguage } from "../../lib/interaction.ts";
|
|
18
18
|
import { FULL_SCREEN_OVERLAY } from "../../lib/tui/chrome.ts";
|
|
19
19
|
import { feedbackSummary, type PlanAnnotation } from "./feedback.ts";
|
|
20
|
+
import type { PlanUnit } from "../../lib/plans/team.ts";
|
|
20
21
|
import type { PlanStore, StoredPlan } from "./plans.ts";
|
|
21
22
|
import type { SectionDigest } from "./plan-text.ts";
|
|
22
|
-
import {
|
|
23
|
+
import { planReviewPrompt, parsePlanReviewReply } from "./review-prompt.ts";
|
|
23
24
|
import { PlanViewComponent, type PlanViewAction } from "./tui.ts";
|
|
24
25
|
import type { PlanReviewResult, PlanReviewSession, PlanWebReview } from "./web.ts";
|
|
25
26
|
|
|
@@ -29,9 +30,6 @@ interface PlanReviewChoice {
|
|
|
29
30
|
refinement?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
/** A dialog-only choice: post the full plan text, then ask again. Never returned. */
|
|
33
|
-
type ReviewChoice = PlanReviewChoice | { action: "send" };
|
|
34
|
-
|
|
35
33
|
/** What a review decided, and the plan state it decided on. */
|
|
36
34
|
export type ReviewDecision =
|
|
37
35
|
| { action: "approve"; plan: StoredPlan; comments: PlanAnnotation[] }
|
|
@@ -44,7 +42,9 @@ export type ReviewDecision =
|
|
|
44
42
|
/** True when the request came from the browser, which is reported differently. */
|
|
45
43
|
fromBrowser: boolean;
|
|
46
44
|
}
|
|
47
|
-
| { action: "keep"; plan: StoredPlan }
|
|
45
|
+
| { action: "keep"; plan: StoredPlan }
|
|
46
|
+
/** Leave plan mode without approving; the plan file stays for a later review. */
|
|
47
|
+
| { action: "cancel"; plan: StoredPlan };
|
|
48
48
|
|
|
49
49
|
/** What the previous review showed, so this one can mark what changed. */
|
|
50
50
|
export interface ReviewBaselines {
|
|
@@ -64,95 +64,68 @@ export interface ReviewPlanOptions {
|
|
|
64
64
|
plan: StoredPlan;
|
|
65
65
|
/** Aborting the turn closes a browser review and returns the plan unapproved. */
|
|
66
66
|
signal?: AbortSignal;
|
|
67
|
+
/** The split approval would write, so the screen shows what is being approved. */
|
|
68
|
+
split?: ReviewSplit;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A validated split, as the review screen displays it. */
|
|
72
|
+
export interface ReviewSplit {
|
|
73
|
+
lead: string;
|
|
74
|
+
units: readonly PlanUnit[];
|
|
75
|
+
verdict: "pair" | "team";
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
/** Prompt shown by the dialog-based refine editor in non-TUI modes. */
|
|
70
79
|
const REFINE_PROMPT = "Refine the plan — what should change? (Enter to save, Esc to cancel)";
|
|
71
80
|
|
|
72
|
-
/** How much of one step the remote digest shows. */
|
|
73
|
-
const DIGEST_STEP_CHARS = 90;
|
|
74
|
-
|
|
75
|
-
/** The refinement request used when a browser save carried no comments. */
|
|
76
|
-
const NO_BROWSER_COMMENTS = "See the review comments in the plan file.";
|
|
77
|
-
|
|
78
81
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* it, and the peer still has to choose approve / refine. The digest carries the
|
|
83
|
-
* title, the step count, and the first few steps; the full text follows as a
|
|
84
|
-
* file or as the fallback message.
|
|
82
|
+
* How long a remote review waits for its answer. Much longer than a plain dialog's
|
|
83
|
+
* bound, because the human has to read the plan first and a phone is a slow place
|
|
84
|
+
* to do it; an expiry here also ends plan mode, so it must not be easy to hit.
|
|
85
85
|
*/
|
|
86
|
-
|
|
87
|
-
const title = /^#\s+(.+)$/m.exec(content)?.[1]?.trim();
|
|
88
|
-
const steps = extractPlanSteps(content);
|
|
89
|
-
const count = steps.length;
|
|
90
|
-
const head = `Plan${title ? `: ${title}` : ""}${count > 0 ? ` (${count} step${count === 1 ? "" : "s"})` : ""}`;
|
|
91
|
-
const preview = steps.slice(0, 3).map((step, index) => {
|
|
92
|
-
const text = step.content.replace(/\s+/g, " ").trim();
|
|
93
|
-
const shown = text.length > DIGEST_STEP_CHARS ? `${text.slice(0, DIGEST_STEP_CHARS - 1)}…` : text;
|
|
94
|
-
return `${index + 1}. ${shown}`;
|
|
95
|
-
});
|
|
96
|
-
const more = count > preview.length ? `… (${count - preview.length} more)` : "";
|
|
97
|
-
return [head, ...preview, more, "The full plan is attached above, or sent as text if the file failed."]
|
|
98
|
-
.filter((line) => line !== "")
|
|
99
|
-
.join("\n");
|
|
100
|
-
}
|
|
86
|
+
export const PLAN_REVIEW_TIMEOUT_MS = 600_000;
|
|
101
87
|
|
|
102
88
|
/**
|
|
103
|
-
* Ask the user to review the plan file. The TUI opens the scrollable
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* first. Asking for the full plan re-enters with the text only, then shows the
|
|
107
|
-
* choices again.
|
|
89
|
+
* Ask the user to review the plan file. The TUI opens the scrollable screen;
|
|
90
|
+
* dialog-capable modes (RPC/WeChat) get one editor prompt. A reply that is not an
|
|
91
|
+
* action is a refinement.
|
|
108
92
|
*/
|
|
109
93
|
async function askPlanReview(
|
|
110
94
|
ctx: ExtensionToolContext,
|
|
111
95
|
plan: StoredPlan,
|
|
112
96
|
baseline: SectionDigest[] | undefined,
|
|
97
|
+
split: ReviewSplit | undefined,
|
|
113
98
|
): Promise<PlanReviewChoice> {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
},
|
|
145
|
-
// A remote turn gets the plan file up front, falling back to the full text;
|
|
146
|
-
// a local TUI turn ignores both and renders the component. The digest goes
|
|
147
|
-
// out either way so the choices stay answerable when a phone client does not
|
|
148
|
-
// render the file bubble.
|
|
149
|
-
file: sentFullText ? undefined : { path: plan.path, name: basename(plan.relativePath) },
|
|
150
|
-
body: plan.content,
|
|
151
|
-
note: sentFullText ? undefined : planReviewDigest(plan.content),
|
|
152
|
-
});
|
|
153
|
-
if (outcome.action !== "send") return outcome;
|
|
154
|
-
sentFullText = true;
|
|
155
|
-
}
|
|
99
|
+
const language = remoteLanguage(ctx) ?? "en";
|
|
100
|
+
const remote = isRemoteTurn(ctx);
|
|
101
|
+
return askHuman<PlanReviewChoice>(ctx, {
|
|
102
|
+
custom: async () => {
|
|
103
|
+
const choice = await ctx.ui.custom<PlanReviewChoice | undefined>(
|
|
104
|
+
(tui, theme, _keybindings, done) =>
|
|
105
|
+
new PlanViewComponent({
|
|
106
|
+
plan,
|
|
107
|
+
baseline,
|
|
108
|
+
...(split === undefined ? {} : { split }),
|
|
109
|
+
theme,
|
|
110
|
+
onClose: (action, refinement) => done({ action, refinement }),
|
|
111
|
+
requestRender: () => tui.requestRender(),
|
|
112
|
+
viewportRows: () => tui.terminal?.rows,
|
|
113
|
+
tui,
|
|
114
|
+
}),
|
|
115
|
+
FULL_SCREEN_OVERLAY,
|
|
116
|
+
);
|
|
117
|
+
return choice ?? { action: "keep" };
|
|
118
|
+
},
|
|
119
|
+
dialogs: async () => {
|
|
120
|
+
const answer = await ctx.ui.editor(planReviewPrompt(plan.content, language, remote), "");
|
|
121
|
+
return parsePlanReviewReply(answer ?? "");
|
|
122
|
+
},
|
|
123
|
+
// The full plan is the artifact under review, so it goes out before the
|
|
124
|
+
// choices; if the upload fails, the same text is the fallback.
|
|
125
|
+
file: { path: plan.path, name: basename(plan.relativePath) },
|
|
126
|
+
body: plan.content,
|
|
127
|
+
timeoutMs: PLAN_REVIEW_TIMEOUT_MS,
|
|
128
|
+
});
|
|
156
129
|
}
|
|
157
130
|
|
|
158
131
|
/** How a browser review may decide, and what to report when it opens. */
|
|
@@ -165,6 +138,13 @@ export interface BrowserReviewOptions {
|
|
|
165
138
|
signal?: AbortSignal;
|
|
166
139
|
/** Called once the server is listening and the browser was opened. */
|
|
167
140
|
onOpened?: (session: PlanReviewSession) => void;
|
|
141
|
+
/**
|
|
142
|
+
* Called when a shared reviewer's post lands, so the owner learns about it where
|
|
143
|
+
* they are waiting: the page updates itself, the terminal has to be told.
|
|
144
|
+
*/
|
|
145
|
+
onReviewerComment?: (author: string, count: number) => void;
|
|
146
|
+
/** A line the page shows the owner, such as why approval belongs in the terminal. */
|
|
147
|
+
note?: string;
|
|
168
148
|
}
|
|
169
149
|
|
|
170
150
|
/**
|
|
@@ -179,7 +159,13 @@ export async function runBrowserReview(
|
|
|
179
159
|
): Promise<PlanReviewResult | undefined> {
|
|
180
160
|
if (options.signal?.aborted) return { status: "cancelled" };
|
|
181
161
|
const session = await web
|
|
182
|
-
.start(cwd, plan, {
|
|
162
|
+
.start(cwd, plan, {
|
|
163
|
+
canApprove: options.canApprove,
|
|
164
|
+
baseline: options.baseline,
|
|
165
|
+
signal: options.signal,
|
|
166
|
+
...(options.note === undefined ? {} : { note: options.note }),
|
|
167
|
+
...(options.onReviewerComment === undefined ? {} : { onReviewerComment: options.onReviewerComment }),
|
|
168
|
+
})
|
|
183
169
|
.catch(() => undefined);
|
|
184
170
|
if (!session) return undefined;
|
|
185
171
|
options.onOpened?.(session);
|
|
@@ -196,7 +182,7 @@ export async function runBrowserReview(
|
|
|
196
182
|
* cancelled returns to the screen it was opened from.
|
|
197
183
|
*/
|
|
198
184
|
export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDecision> {
|
|
199
|
-
const { ctx, web, plans, baselines, signal } = options;
|
|
185
|
+
const { ctx, web, plans, baselines, signal, split } = options;
|
|
200
186
|
let plan = options.plan;
|
|
201
187
|
for (;;) {
|
|
202
188
|
// An aborted turn has nobody left to answer the screen.
|
|
@@ -204,11 +190,12 @@ export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDeci
|
|
|
204
190
|
// Read the baseline before the screen and mark it after, so what the next
|
|
205
191
|
// review compares against is the content this one actually showed.
|
|
206
192
|
const baseline = baselines.reviewed(plan.path);
|
|
207
|
-
const choice = await askPlanReview(ctx, plan, baseline);
|
|
193
|
+
const choice = await askPlanReview(ctx, plan, baseline, split);
|
|
208
194
|
baselines.markReviewed(plan.path, plan.content);
|
|
209
195
|
|
|
210
196
|
if (choice.action === "approve") return { action: "approve", plan, comments: [] };
|
|
211
197
|
if (choice.action === "keep") return { action: "keep", plan };
|
|
198
|
+
if (choice.action === "cancel") return { action: "cancel", plan };
|
|
212
199
|
if (choice.action === "refine") {
|
|
213
200
|
const refinement = choice.refinement ?? (await ctx.ui.editor(REFINE_PROMPT, ""))?.trim();
|
|
214
201
|
// An empty or cancelled editor returns to the review screen instead of
|
|
@@ -219,11 +206,22 @@ export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDeci
|
|
|
219
206
|
// An unrecognized action keeps planning rather than opening a server.
|
|
220
207
|
if (choice.action !== "browser") return { action: "keep", plan };
|
|
221
208
|
|
|
209
|
+
// A split section exists only after approval, and the page anchors comments to
|
|
210
|
+
// source lines, so a page cannot show — or approve — a plan it cannot carry.
|
|
211
|
+
// The terminal review is where the split is approved.
|
|
212
|
+
const splitNote =
|
|
213
|
+
split === undefined
|
|
214
|
+
? undefined
|
|
215
|
+
: "This plan has a split, which the page cannot show; approve it in the terminal review.";
|
|
216
|
+
if (splitNote !== undefined) ctx.ui.notify(splitNote, "info");
|
|
222
217
|
const result = await runBrowserReview(web, ctx.cwd, plan, {
|
|
223
|
-
canApprove:
|
|
218
|
+
canApprove: split === undefined,
|
|
219
|
+
...(splitNote === undefined ? {} : { note: splitNote }),
|
|
224
220
|
baseline,
|
|
225
221
|
signal,
|
|
226
222
|
onOpened: (session) => ctx.ui.notify(`Plan review: opened ${session.url}`, "info"),
|
|
223
|
+
onReviewerComment: (author, count) =>
|
|
224
|
+
ctx.ui.notify(`Plan review: @${author} posted ${count === 1 ? "a comment" : `${count} comments`}.`, "info"),
|
|
227
225
|
});
|
|
228
226
|
if (!result) {
|
|
229
227
|
ctx.ui.notify("Plan review: could not start the browser review server.", "error");
|
|
@@ -231,13 +229,15 @@ export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDeci
|
|
|
231
229
|
}
|
|
232
230
|
if (result.status === "approved") return { action: "approve", plan: result.plan, comments: result.annotations };
|
|
233
231
|
if (result.status === "saved") {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
232
|
+
// A save with no comments asks for nothing, so it returns to the screen the
|
|
233
|
+
// way an empty refine editor does, instead of reporting a refinement nobody
|
|
234
|
+
// wrote.
|
|
235
|
+
const refinement = feedbackSummary(result.annotations);
|
|
236
|
+
if (!refinement) {
|
|
237
|
+
plan = result.plan;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
return { action: "refine", plan: result.plan, refinement, comments: result.annotations, fromBrowser: true };
|
|
241
241
|
}
|
|
242
242
|
// Cancelled or the tab closed: the file may have been rewritten underneath,
|
|
243
243
|
// so re-read it before showing the screen again.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Enabling plan mode removes every tool the policy does not consider read-only
|
|
5
5
|
* from the active set and activates the plan-mode control tools: `exit_plan_mode`
|
|
6
6
|
* (present the plan for approval) and `write_plan` (save the plan artifact under
|
|
7
|
-
*
|
|
7
|
+
* the plans directory). Disabling restores exactly what it hid. The enabled flag is
|
|
8
8
|
* persisted as a custom session entry so `/resume` and `/tree` follow the
|
|
9
9
|
* branch, and it is also mirrored into the footer status. The review baseline
|
|
10
10
|
* travels the same way, as one digest per heading section, so the changed-section
|
|
@@ -32,6 +32,8 @@ import type { PlanModeEntry, ReviewBaselineEntry } from "./types.ts";
|
|
|
32
32
|
|
|
33
33
|
/** Tool the model calls to leave plan mode after approval. */
|
|
34
34
|
export const EXIT_TOOL = "exit_plan_mode";
|
|
35
|
+
/** Tool the model calls to leave plan mode without approving. */
|
|
36
|
+
export const CANCEL_TOOL = "cancel_plan_mode";
|
|
35
37
|
/** Tool the model calls to ask to enter plan mode. */
|
|
36
38
|
export const ENTER_TOOL = PLAN_ENTER_TOOL;
|
|
37
39
|
/** Tool the model calls to save the plan artifact. */
|
|
@@ -47,7 +49,7 @@ export const BASELINE_TYPE = "plan-baseline";
|
|
|
47
49
|
const STATUS_NAME_LENGTH = 28;
|
|
48
50
|
|
|
49
51
|
/** Tools plan mode activates itself; the guard permits only this extension's own. */
|
|
50
|
-
const CONTROL_TOOLS = [EXIT_TOOL, WRITE_PLAN_TOOL];
|
|
52
|
+
const CONTROL_TOOLS = [EXIT_TOOL, CANCEL_TOOL, WRITE_PLAN_TOOL];
|
|
51
53
|
|
|
52
54
|
export interface PlanRuntime {
|
|
53
55
|
/** Whether plan mode is currently on. */
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The split at approval: the switch that enables it, the roster card, and the
|
|
3
|
+
* `## Team` section it writes.
|
|
4
|
+
*
|
|
5
|
+
* Plan mode keeps one job — the plan document and its approval gate — and this
|
|
6
|
+
* module is the part of that gate that concerns a team: whether the feature is on,
|
|
7
|
+
* what the user is told to run in the sessions this package cannot start, and the
|
|
8
|
+
* write that makes the team real. Everything mechanical about the split itself
|
|
9
|
+
* (which tables are legal, which paths collide) is `lib/plans/split.ts`, because
|
|
10
|
+
* the bus reads the same section.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
14
|
+
import { splitEnabled } from "../../lib/plans/setting.ts";
|
|
15
|
+
import { checkSplit, mergeOrder } from "../../lib/plans/split.ts";
|
|
16
|
+
import type { PlanTeam, PlanUnit } from "../../lib/plans/team.ts";
|
|
17
|
+
|
|
18
|
+
export { SPLIT_OFF_NOTE, SPLIT_SETTING_ID, SPLIT_SETTING_LABEL, splitEnabled } from "../../lib/plans/setting.ts";
|
|
19
|
+
|
|
20
|
+
/** What approval decides about the units a model proposed. */
|
|
21
|
+
export type SplitDecision =
|
|
22
|
+
| { kind: "none" }
|
|
23
|
+
| { kind: "off" }
|
|
24
|
+
| { kind: "refused"; reason: string }
|
|
25
|
+
| { kind: "serial" }
|
|
26
|
+
| { kind: "team"; team: PlanTeam; verdict: "pair" | "team" };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decide what to do with the `units` an approval carried.
|
|
30
|
+
*
|
|
31
|
+
* The switch is checked first: with it off the units are ignored entirely rather
|
|
32
|
+
* than validated, so a user who never enabled the feature cannot be blocked by a
|
|
33
|
+
* rule about it.
|
|
34
|
+
*/
|
|
35
|
+
export function decideSplit(
|
|
36
|
+
cwd: string,
|
|
37
|
+
root: string | undefined,
|
|
38
|
+
units: readonly PlanUnit[] | undefined,
|
|
39
|
+
): SplitDecision {
|
|
40
|
+
if (units === undefined || units.length === 0) return { kind: "none" };
|
|
41
|
+
if (!splitEnabled(cwd, root)) return { kind: "off" };
|
|
42
|
+
const check = checkSplit(units);
|
|
43
|
+
if (!check.ok) return { kind: "refused", reason: check.reason };
|
|
44
|
+
if (check.verdict === "serial") return { kind: "serial" };
|
|
45
|
+
return { kind: "team", team: { lead: check.lead, units: check.units }, verdict: check.verdict };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The roster card: what the user runs, in which session, and in what order.
|
|
50
|
+
*
|
|
51
|
+
* A lead cannot start, name, or prompt a peer session, so the card is the whole of
|
|
52
|
+
* what step 3 can do — and its first line says so, because the rest of the flow
|
|
53
|
+
* depends on the user believing it.
|
|
54
|
+
*/
|
|
55
|
+
export function rosterCard(team: PlanTeam, planPath: string, sessionLabel?: string): string {
|
|
56
|
+
const lead = team.units.find((unit) => unit.lead)?.name ?? team.lead;
|
|
57
|
+
const owners = team.units.filter((unit) => !unit.lead);
|
|
58
|
+
const rows = owners.map((unit) => {
|
|
59
|
+
const join = `session({action:"join", team:"${teamName(planPath)}", unit:"${unit.name}"})`;
|
|
60
|
+
return ` ${unit.name}: enter_worktree ${unit.name} · /sessions name ${unit.name} · /sessions watch wake\n ${join}`;
|
|
61
|
+
});
|
|
62
|
+
const order = mergeOrderLine(team);
|
|
63
|
+
// The lead's own label is the one line of the card it cannot get itself until
|
|
64
|
+
// after this card is printed, so the card either names it or says how to set it.
|
|
65
|
+
const name = sessionLabel?.trim();
|
|
66
|
+
const label = name
|
|
67
|
+
? ` your label: ${sanitizeLine(name)} — owners send their report there`
|
|
68
|
+
: ` your label: none — name this session: session({action:"name", label:"${lead}"})`;
|
|
69
|
+
return [
|
|
70
|
+
`Split: ${team.units.length} units · ${shortenPath(planPath)}`,
|
|
71
|
+
" You start and switch every session; nothing here can.",
|
|
72
|
+
...rows,
|
|
73
|
+
` ${order} · your unit: ${lead}`,
|
|
74
|
+
label,
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The team's name is the plan file's stem, so two plans never share one. */
|
|
79
|
+
export function teamName(planPath: string): string {
|
|
80
|
+
const name = planPath.split(/[\\/]/).pop() ?? planPath;
|
|
81
|
+
return name.replace(/\.md$/i, "");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Merge order in one line, from the dependency graph the validation accepted. */
|
|
85
|
+
export function mergeOrderLine(team: PlanTeam): string {
|
|
86
|
+
return `merge order: ${mergeOrder(team.units).join(" → ")}`;
|
|
87
|
+
}
|