@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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* It presents the saved plan for approval, then seeds the approved steps into
|
|
5
5
|
* the todo list and finalizes the session goal: a `planning` goal becomes the
|
|
6
|
-
* plan's own `## Goal`, and a session with no goal is
|
|
6
|
+
* plan's own `## Goal`, and a session with no goal is given the same. The
|
|
7
7
|
* review loop and the browser lifecycle belong to `review.ts`; the nested writes
|
|
8
8
|
* belong to `todo-seed.ts` and the goal tool.
|
|
9
9
|
*/
|
|
@@ -12,17 +12,21 @@ import { basename } from "node:path";
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { Type, type Static } from "typebox";
|
|
14
14
|
import { sanitizeLine } from "../../../lib/format.ts";
|
|
15
|
+
import { messageOf } from "../../../lib/errors.ts";
|
|
15
16
|
import { lastGoal } from "../../../lib/goal-state.ts";
|
|
16
17
|
import { GOAL_TOOL } from "../../../lib/tool-names.ts";
|
|
17
18
|
import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
18
19
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
19
20
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
20
21
|
import { callLine, detailsOrLine, moreHint } from "../../../lib/tui/tool.ts";
|
|
22
|
+
import { failedResult } from "../../../lib/tool-result.ts";
|
|
21
23
|
import { feedbackSummary } from "../feedback.ts";
|
|
22
24
|
import { mergeSummary, overCap, preview, verificationNote } from "../format.ts";
|
|
23
25
|
import { sanitizePlanText } from "../plan-text.ts";
|
|
24
26
|
import { planTitle, type StoredPlan } from "../plans.ts";
|
|
25
27
|
import { reviewPlan } from "../review.ts";
|
|
28
|
+
import { decideSplit, rosterCard, SPLIT_OFF_NOTE } from "../split.ts";
|
|
29
|
+
import { writeTeam } from "../../../lib/plans/team.ts";
|
|
26
30
|
import { EXIT_TOOL, SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
27
31
|
import { goalSection, MAX_STEPS, planStepSummary } from "../steps.ts";
|
|
28
32
|
import type { ExitPlanModeDetails } from "../types.ts";
|
|
@@ -30,11 +34,27 @@ import type { PlanWebReview } from "../web.ts";
|
|
|
30
34
|
import { firstTextLine } from "./nested.ts";
|
|
31
35
|
import { seedTodos } from "./todo-seed.ts";
|
|
32
36
|
|
|
37
|
+
const PlanUnitParams = Type.Object({
|
|
38
|
+
name: Type.String({ description: "Lowercase slug; the owner session takes it as its label." }),
|
|
39
|
+
paths: Type.Array(Type.String(), { description: "Repository-relative path prefixes this unit owns." }),
|
|
40
|
+
test: Type.String({ description: "The command that proves the unit is done." }),
|
|
41
|
+
dependsOn: Type.Optional(Type.Array(Type.String(), { description: "Units that must land first." })),
|
|
42
|
+
lead: Type.Optional(Type.Boolean({ description: "True for the one unit this session keeps." })),
|
|
43
|
+
});
|
|
44
|
+
|
|
33
45
|
const ExitPlanModeParams = Type.Object({
|
|
34
46
|
plan_path: Type.String({
|
|
35
47
|
description:
|
|
36
|
-
"Path to the plan file written by write_plan
|
|
48
|
+
"Path to the plan file written by write_plan: the absolute path it returned, or the `~/…` form it displayed. A bare name resolves inside the plans directory. The user reviews this file.",
|
|
37
49
|
}),
|
|
50
|
+
units: Type.Optional(
|
|
51
|
+
Type.Array(PlanUnitParams, {
|
|
52
|
+
description:
|
|
53
|
+
"The plan's slices, when it has two or more independent ones the user will run in separate sessions. " +
|
|
54
|
+
"Only used when the plan.split setting is on; validation refuses an overlapping, looping, or oversized " +
|
|
55
|
+
"split before the review opens. Omit it to approve as one flow.",
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
38
58
|
});
|
|
39
59
|
|
|
40
60
|
type ExitPlanModeArgs = Static<typeof ExitPlanModeParams>;
|
|
@@ -50,21 +70,16 @@ interface GoalOutcome {
|
|
|
50
70
|
/**
|
|
51
71
|
* Finalise the session goal from the approved plan.
|
|
52
72
|
*
|
|
53
|
-
*
|
|
54
|
-
* session with
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
73
|
+
* Approving the plan *is* the decision, so neither a `planning` goal nor a
|
|
74
|
+
* session with none is asked again. A final goal (`active` or `achieved`) is the
|
|
75
|
+
* user's own: approving ordinary work never rewrites it. Recording runs through
|
|
76
|
+
* the goal tool, so validation and the widget stay goal's, and no model turn is
|
|
77
|
+
* spent.
|
|
58
78
|
*/
|
|
59
79
|
async function finalizeGoal(ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOutcome> {
|
|
60
80
|
const objective = planObjective(plan);
|
|
61
81
|
const current = lastGoal(ctx.sessionManager.getBranch());
|
|
62
82
|
if (current !== null && current.status !== "planning") return { objective };
|
|
63
|
-
if (current === null) {
|
|
64
|
-
if (!ctx.hasUI) return { objective };
|
|
65
|
-
const approved = await ctx.ui.confirm("Record this plan as the session goal?", objective);
|
|
66
|
-
if (!approved) return { objective };
|
|
67
|
-
}
|
|
68
83
|
const outcome = await ctx.executeTool(GOAL_TOOL, { objective, status: "active" });
|
|
69
84
|
const action = current === null ? "recorded" : "promoted";
|
|
70
85
|
return outcome.isError ? { objective, action, error: firstTextLine(outcome) } : { objective, action };
|
|
@@ -96,68 +111,71 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
96
111
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
97
112
|
|
|
98
113
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
99
|
-
const
|
|
114
|
+
const args = params as ExitPlanModeArgs;
|
|
115
|
+
const planPath = args.plan_path.trim();
|
|
100
116
|
|
|
101
117
|
if (!runtime.isEnabled()) {
|
|
102
|
-
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
};
|
|
118
|
+
return failedResult("Not in plan mode; there is no plan to approve.", {
|
|
119
|
+
approved: false,
|
|
120
|
+
plan: "",
|
|
121
|
+
planPath,
|
|
122
|
+
} satisfies ExitPlanModeDetails);
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
const file = await runtime.plans.read(ctx.cwd, planPath);
|
|
110
126
|
if (!file) {
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
text: `No plan file at "${planPath}". Save the plan with write_plan first, then pass the returned path.`,
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
details: { approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
|
|
119
|
-
isError: true,
|
|
120
|
-
};
|
|
127
|
+
return failedResult(
|
|
128
|
+
`No plan file at "${planPath}". Save the plan with write_plan first, then pass the returned path.`,
|
|
129
|
+
{ approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
|
|
130
|
+
);
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
const total = planStepSummary(file.content).total;
|
|
124
134
|
if (total > MAX_STEPS) {
|
|
125
135
|
// A hand-edited or older plan file can still be over the cap; refuse it here
|
|
126
136
|
// too, so approval never seeds a list it cannot hold in full.
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
type: "text",
|
|
131
|
-
text: `${overCap(total)} It was not presented for approval; call write_plan with plan_path to group the steps, then exit_plan_mode again.`,
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
details: {
|
|
137
|
+
return failedResult(
|
|
138
|
+
`${overCap(total)} It was not presented for approval; call write_plan with plan_path to group the steps, then exit_plan_mode again.`,
|
|
139
|
+
{
|
|
135
140
|
approved: false,
|
|
136
141
|
plan: file.content,
|
|
137
142
|
planPath: file.path,
|
|
138
143
|
relativePath: file.relativePath,
|
|
139
144
|
} satisfies ExitPlanModeDetails,
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The split is decided before the review opens, so a refused or rejected split
|
|
149
|
+
// never spends the user's review — and the screen can show what it approves.
|
|
150
|
+
const units = args.units?.map((unit) => ({
|
|
151
|
+
name: unit.name,
|
|
152
|
+
paths: [...unit.paths],
|
|
153
|
+
test: unit.test,
|
|
154
|
+
dependsOn: [...(unit.dependsOn ?? [])],
|
|
155
|
+
...(unit.lead ? { lead: true } : {}),
|
|
156
|
+
}));
|
|
157
|
+
const split = decideSplit(ctx.cwd, await runtime.plans.rootFor(ctx.cwd), units);
|
|
158
|
+
if (split.kind === "refused") {
|
|
159
|
+
return failedResult(`Refused: ${split.reason}`, {
|
|
160
|
+
approved: false,
|
|
161
|
+
plan: file.content,
|
|
162
|
+
planPath: file.path,
|
|
163
|
+
relativePath: file.relativePath,
|
|
164
|
+
} satisfies ExitPlanModeDetails);
|
|
142
165
|
}
|
|
166
|
+
const pending = split.kind === "team" ? split : undefined;
|
|
143
167
|
|
|
144
168
|
if (!ctx.hasUI) {
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
type: "text",
|
|
149
|
-
text: `No interactive UI is available to approve the plan. It is saved at ${file.relativePath}; present it and ask the user to approve it in their reply.`,
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
details: {
|
|
169
|
+
return failedResult(
|
|
170
|
+
`No interactive UI is available to approve the plan. It is saved at ${file.relativePath}; present it and ask the user to approve it in their reply.`,
|
|
171
|
+
{
|
|
153
172
|
approved: false,
|
|
154
173
|
plan: file.content,
|
|
155
174
|
planPath: file.path,
|
|
156
175
|
relativePath: file.relativePath,
|
|
157
176
|
unavailable: true,
|
|
158
177
|
} satisfies ExitPlanModeDetails,
|
|
159
|
-
|
|
160
|
-
};
|
|
178
|
+
);
|
|
161
179
|
}
|
|
162
180
|
|
|
163
181
|
// One decision, however the session is attached; `review.ts` owns the loop,
|
|
@@ -169,6 +187,7 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
169
187
|
baselines: runtime,
|
|
170
188
|
plan: file,
|
|
171
189
|
signal,
|
|
190
|
+
...(pending === undefined ? {} : { split: { ...pending.team, verdict: pending.verdict } }),
|
|
172
191
|
});
|
|
173
192
|
|
|
174
193
|
if (decision.action === "refine") {
|
|
@@ -208,10 +227,56 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
208
227
|
} satisfies ExitPlanModeDetails,
|
|
209
228
|
};
|
|
210
229
|
}
|
|
230
|
+
if (decision.action === "cancel") {
|
|
231
|
+
runtime.disable(ctx);
|
|
232
|
+
const shown = decision.plan.relativePath;
|
|
233
|
+
ctx.ui.notify("Plan mode off — full access restored. The plan was not approved.", "info");
|
|
234
|
+
return {
|
|
235
|
+
content: [
|
|
236
|
+
{
|
|
237
|
+
type: "text",
|
|
238
|
+
text:
|
|
239
|
+
"Plan mode is off and write access is restored. The plan was not approved and no steps were recorded. " +
|
|
240
|
+
`The plan file ${shown} remains if the user wants to review it later.`,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
details: {
|
|
244
|
+
approved: false,
|
|
245
|
+
cancelled: true,
|
|
246
|
+
plan: decision.plan.content,
|
|
247
|
+
planPath: decision.plan.path,
|
|
248
|
+
relativePath: decision.plan.relativePath,
|
|
249
|
+
} satisfies ExitPlanModeDetails,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
211
252
|
|
|
253
|
+
// The team becomes real here, never before: the section is what a joining
|
|
254
|
+
// session reads, so it appears only after the user approved the split. The
|
|
255
|
+
// write happens while plan mode is still on, so a failure leaves the session
|
|
256
|
+
// able to retry instead of approved-but-empty.
|
|
257
|
+
let approved = decision.plan;
|
|
258
|
+
if (pending !== undefined) {
|
|
259
|
+
try {
|
|
260
|
+
approved = await runtime.plans.write(ctx.cwd, {
|
|
261
|
+
title: planTitle(basename(decision.plan.path)),
|
|
262
|
+
content: writeTeam(decision.plan.content, { lead: pending.team.lead, units: pending.team.units }),
|
|
263
|
+
planPath: decision.plan.path,
|
|
264
|
+
});
|
|
265
|
+
} catch (writeError) {
|
|
266
|
+
return failedResult(
|
|
267
|
+
`The plan was approved but its team could not be written: ${messageOf(writeError)}. Plan mode is still on and no steps were seeded; fix the plan file and call exit_plan_mode again.`,
|
|
268
|
+
{
|
|
269
|
+
approved: false,
|
|
270
|
+
plan: decision.plan.content,
|
|
271
|
+
planPath: decision.plan.path,
|
|
272
|
+
relativePath: decision.plan.relativePath,
|
|
273
|
+
} satisfies ExitPlanModeDetails,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
212
277
|
runtime.disable(ctx);
|
|
213
|
-
const { steps, merged, error } = await seedTodos(pi, ctx,
|
|
214
|
-
const goal = await finalizeGoal(ctx,
|
|
278
|
+
const { steps, merged, error } = await seedTodos(pi, ctx, approved.content);
|
|
279
|
+
const goal = await finalizeGoal(ctx, approved);
|
|
215
280
|
const listing =
|
|
216
281
|
steps.length > 0
|
|
217
282
|
? `\n\n${steps
|
|
@@ -238,19 +303,41 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
238
303
|
const browserNote = decision.comments.length
|
|
239
304
|
? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(decision.comments)}`
|
|
240
305
|
: "";
|
|
306
|
+
const splitNote =
|
|
307
|
+
pending === undefined
|
|
308
|
+
? split.kind === "off"
|
|
309
|
+
? `\n\n${SPLIT_OFF_NOTE}`
|
|
310
|
+
: ""
|
|
311
|
+
: `\n\n${rosterCard(pending.team, approved.path, pi.getSessionName())}\n\nWork only your own unit (${pending.team.lead}); the other units belong to the sessions the user starts.`;
|
|
241
312
|
return {
|
|
242
313
|
content: [
|
|
243
314
|
{
|
|
244
315
|
type: "text",
|
|
245
|
-
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${
|
|
316
|
+
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${approved.relativePath} is the source of truth; follow its steps.${browserNote}${splitNote}${tail}${verification}${goalNote}${listing}`,
|
|
246
317
|
},
|
|
247
318
|
],
|
|
248
319
|
details: {
|
|
249
320
|
approved: true,
|
|
250
|
-
plan:
|
|
251
|
-
planPath:
|
|
252
|
-
relativePath:
|
|
321
|
+
plan: approved.content,
|
|
322
|
+
planPath: approved.path,
|
|
323
|
+
relativePath: approved.relativePath,
|
|
253
324
|
annotations: decision.comments.length > 0 ? decision.comments : undefined,
|
|
325
|
+
...(pending === undefined
|
|
326
|
+
? split.kind === "off"
|
|
327
|
+
? { splitOff: true }
|
|
328
|
+
: {}
|
|
329
|
+
: {
|
|
330
|
+
team: {
|
|
331
|
+
lead: pending.team.lead,
|
|
332
|
+
verdict: pending.verdict,
|
|
333
|
+
units: pending.team.units.map((unit) => ({
|
|
334
|
+
name: unit.name,
|
|
335
|
+
paths: unit.paths,
|
|
336
|
+
test: unit.test,
|
|
337
|
+
dependsOn: unit.dependsOn,
|
|
338
|
+
})),
|
|
339
|
+
},
|
|
340
|
+
}),
|
|
254
341
|
} satisfies ExitPlanModeDetails,
|
|
255
342
|
};
|
|
256
343
|
},
|
|
@@ -269,11 +356,13 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
269
356
|
const details = outcome.details;
|
|
270
357
|
const heading = details.unavailable
|
|
271
358
|
? paint(theme, "warn", "No UI to approve the plan")
|
|
272
|
-
: details.
|
|
273
|
-
? paint(theme, "
|
|
274
|
-
: details.
|
|
275
|
-
? paint(theme, "
|
|
276
|
-
:
|
|
359
|
+
: details.cancelled
|
|
360
|
+
? paint(theme, "warn", "Plan mode off — plan not approved")
|
|
361
|
+
: details.approved
|
|
362
|
+
? paint(theme, "ok", `${GLYPHS.check} Plan approved`)
|
|
363
|
+
: details.refined
|
|
364
|
+
? paint(theme, "accent", "Refining the plan")
|
|
365
|
+
: paint(theme, "warn", "Plan not approved");
|
|
277
366
|
const extra =
|
|
278
367
|
details.refined && details.refinement
|
|
279
368
|
? `\n${paint(theme, "meta", `↳ ${sanitizePlanText(details.refinement)}`)}`
|
|
@@ -13,6 +13,7 @@ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
|
13
13
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
14
14
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
15
15
|
import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
|
|
16
|
+
import { failedResult, textResult } from "../../../lib/tool-result.ts";
|
|
16
17
|
import { describeUpdates, stepLabel } from "../format.ts";
|
|
17
18
|
import { SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
18
19
|
import { extractPlanSteps } from "../steps.ts";
|
|
@@ -66,39 +67,26 @@ export function registerSetPlanStepsTool(pi: ExtensionAPI, runtime: PlanRuntime)
|
|
|
66
67
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
67
68
|
const args = params as SetPlanStepsArgs;
|
|
68
69
|
if (runtime.isEnabled()) {
|
|
69
|
-
return
|
|
70
|
-
content: [{ type: "text", text: "Not while planning; refine the plan with write_plan instead." }],
|
|
71
|
-
details: undefined,
|
|
72
|
-
isError: true,
|
|
73
|
-
};
|
|
70
|
+
return failedResult("Not while planning; refine the plan with write_plan instead.", undefined);
|
|
74
71
|
}
|
|
75
72
|
try {
|
|
76
73
|
const plan = await runtime.plans.setSteps(ctx.cwd, args.plan_path, args.updates);
|
|
77
74
|
const steps = extractPlanSteps(plan.content);
|
|
78
75
|
const done = steps.filter((step) => step.status === "completed").length;
|
|
79
76
|
const todoNote = await mirrorTodos(ctx, args.updates);
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
type: "text",
|
|
84
|
-
text: `${describeUpdates(args.updates)} (${done}/${steps.length} done) in ${plan.relativePath}${todoNote}.`,
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
details: {
|
|
77
|
+
return textResult(
|
|
78
|
+
`${describeUpdates(args.updates)} (${done}/${steps.length} done) in ${plan.relativePath}${todoNote}.`,
|
|
79
|
+
{
|
|
88
80
|
path: plan.path,
|
|
89
81
|
relativePath: plan.relativePath,
|
|
90
82
|
updates: args.updates,
|
|
91
83
|
done,
|
|
92
84
|
total: steps.length,
|
|
93
85
|
} satisfies SetPlanStepsDetails,
|
|
94
|
-
|
|
86
|
+
);
|
|
95
87
|
} catch (error) {
|
|
96
88
|
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
-
return {
|
|
98
|
-
content: [{ type: "text", text: `Could not update the plan: ${message}` }],
|
|
99
|
-
details: undefined,
|
|
100
|
-
isError: true,
|
|
101
|
-
};
|
|
89
|
+
return failedResult(`Could not update the plan: ${message}`, undefined);
|
|
102
90
|
}
|
|
103
91
|
},
|
|
104
92
|
|
|
@@ -12,6 +12,7 @@ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
|
12
12
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
13
13
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
14
14
|
import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
|
|
15
|
+
import { failedResult, textResult } from "../../../lib/tool-result.ts";
|
|
15
16
|
import { overCap, stepElisionNote } from "../format.ts";
|
|
16
17
|
import { WRITE_PLAN_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
17
18
|
import { MAX_STEPS, PLAN_OUTLINE, planStepSummary } from "../steps.ts";
|
|
@@ -39,14 +40,26 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
|
|
|
39
40
|
name: WRITE_PLAN_TOOL,
|
|
40
41
|
label: "Write plan",
|
|
41
42
|
description:
|
|
42
|
-
"Save the plan you designed as a markdown file
|
|
43
|
+
"Save the plan you designed as a markdown file in the plans directory (the agent directory, keyed by " +
|
|
43
44
|
"repository). Write the complete plan, not a summary, in this outline so it is consistent and scannable:\n\n" +
|
|
44
45
|
`${PLAN_OUTLINE}\n\n` +
|
|
46
|
+
"Each section has one job: lead it with its outcome, keep a prose section to a few bullets or two or three " +
|
|
47
|
+
"sentences, give each step one verifiable outcome with the detail in a nested note, and put the sketch, files, " +
|
|
48
|
+
"and commands in inline code or a fence rather than pasted prose. " +
|
|
49
|
+
"When the change alters a surface that already exists, Visual shows it before and after in one fence each; " +
|
|
50
|
+
"tag a fence only when every line in it is code. " +
|
|
51
|
+
"The outline is a guide, not a form: drop Context, Approach, Visual, or Risks when there is nothing to say. " +
|
|
52
|
+
"Keep Steps and Verification — the steps are read from that section (without it, the shallowest list " +
|
|
53
|
+
"anywhere becomes the steps), and Verification becomes the last todo item. " +
|
|
54
|
+
"In Steps, each top-level bullet is one outcome in the order you will do them; " +
|
|
55
|
+
"an indented line continues that outcome, and a deeper bullet is a note. " +
|
|
45
56
|
"Returns the path to pass to exit_plan_mode; plan_path overwrites an existing file when refining. " +
|
|
46
57
|
`A plan with more than ${MAX_STEPS} top-level steps in its Steps section is refused, so approval never drops steps it cannot track.`,
|
|
47
|
-
promptSnippet: "Save the designed plan as a markdown file
|
|
58
|
+
promptSnippet: "Save the designed plan as a markdown file in the plans directory.",
|
|
48
59
|
promptGuidelines: [
|
|
49
60
|
"While plan mode is active, write the full plan with write_plan, then call exit_plan_mode with the returned path.",
|
|
61
|
+
"When refining with plan_path, keep every section heading as it was: the review marks changed sections by heading, " +
|
|
62
|
+
"so a rename reads as an edit.",
|
|
50
63
|
],
|
|
51
64
|
parameters: WritePlanParams,
|
|
52
65
|
defaultActive: false,
|
|
@@ -57,27 +70,17 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
|
|
|
57
70
|
|
|
58
71
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
59
72
|
if (!runtime.isEnabled()) {
|
|
60
|
-
return
|
|
61
|
-
content: [{ type: "text", text: "Not in plan mode; call enter_plan_mode first." }],
|
|
62
|
-
details: undefined,
|
|
63
|
-
isError: true,
|
|
64
|
-
};
|
|
73
|
+
return failedResult("Not in plan mode; call enter_plan_mode first.", undefined);
|
|
65
74
|
}
|
|
66
75
|
const args = params as WritePlanArgs;
|
|
67
76
|
const summary = planStepSummary(args.content);
|
|
68
77
|
if (summary.total > MAX_STEPS) {
|
|
69
78
|
// Refuse before writing: a note after the fact arrives once the file exists and
|
|
70
79
|
// the review has listed every step, so the overflow is already lost.
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
text: `${overCap(summary.total)} Nothing was written; group the steps (one outcome each, sub-bullets for detail) and call write_plan again.`,
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
details: undefined,
|
|
79
|
-
isError: true,
|
|
80
|
-
};
|
|
80
|
+
return failedResult(
|
|
81
|
+
`${overCap(summary.total)} Nothing was written; group the steps (one outcome each, sub-bullets for detail) and call write_plan again.`,
|
|
82
|
+
undefined,
|
|
83
|
+
);
|
|
81
84
|
}
|
|
82
85
|
try {
|
|
83
86
|
const file = await runtime.plans.write(ctx.cwd, {
|
|
@@ -86,22 +89,17 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
|
|
|
86
89
|
planPath: args.plan_path,
|
|
87
90
|
});
|
|
88
91
|
runtime.setLastPlan(ctx, file.path);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
};
|
|
92
|
+
// A store an older version kept in the checkout was moved in by this
|
|
93
|
+
// write; say so once, where the user is looking.
|
|
94
|
+
const moved = runtime.plans.takeNotice();
|
|
95
|
+
if (moved) ctx.ui.notify(moved, "info");
|
|
96
|
+
return textResult(
|
|
97
|
+
`Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
|
|
98
|
+
{ path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
|
|
99
|
+
);
|
|
98
100
|
} catch (error) {
|
|
99
101
|
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
-
return {
|
|
101
|
-
content: [{ type: "text", text: `Could not write the plan: ${message}` }],
|
|
102
|
-
details: undefined,
|
|
103
|
-
isError: true,
|
|
104
|
-
};
|
|
102
|
+
return failedResult(`Could not write the plan: ${message}`, undefined);
|
|
105
103
|
}
|
|
106
104
|
},
|
|
107
105
|
|
package/extensions/plan/tools.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Model-facing registration for the plan-mode tools.
|
|
3
3
|
*
|
|
4
4
|
* `enter_plan_mode` lets the model ask to plan before touching code; it needs
|
|
5
|
-
* user confirmation. `write_plan` saves the plan artifact
|
|
6
|
-
* returns its path. `
|
|
7
|
-
*
|
|
5
|
+
* user confirmation. `write_plan` saves the plan artifact in the plans directory and
|
|
6
|
+
* returns its path. `cancel_plan_mode` leaves planning without approving, so a
|
|
7
|
+
* phone user is never trapped in read-only mode. `exit_plan_mode` reads that file
|
|
8
|
+
* back, presents it for approval (a scrollable review screen in the TUI, a select dialog elsewhere),
|
|
8
9
|
* and on approval seeds the `todo` tool with the plan's steps (through
|
|
9
10
|
* `ctx.executeTool`, so the todo widget and validation run normally).
|
|
10
11
|
* `set_plan_steps` then keeps the plan current: after approval the model ticks
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
|
|
23
24
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
24
25
|
import type { PlanRuntime } from "./runtime.ts";
|
|
26
|
+
import { registerCancelTool } from "./tools/cancel.ts";
|
|
25
27
|
import { registerEnterTool } from "./tools/enter.ts";
|
|
26
28
|
import { registerExitTool } from "./tools/exit.ts";
|
|
27
29
|
import { registerSetPlanStepsTool } from "./tools/steps.ts";
|
|
@@ -34,6 +36,7 @@ export function registerTools(
|
|
|
34
36
|
web: PlanWebReview = createPlanWebReview(runtime.plans),
|
|
35
37
|
): void {
|
|
36
38
|
registerEnterTool(pi, runtime);
|
|
39
|
+
registerCancelTool(pi, runtime);
|
|
37
40
|
registerWriteTool(pi, runtime);
|
|
38
41
|
registerExitTool(pi, runtime, web);
|
|
39
42
|
registerSetPlanStepsTool(pi, runtime);
|