@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 +46 -30
- package/README.zh-CN.md +40 -25
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -23
- package/docs/extensions/board.md +397 -168
- package/docs/extensions/bytefly.md +234 -197
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +57 -63
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +197 -140
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +145 -0
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +83 -44
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +79 -25
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +17 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +5 -20
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +65 -93
- package/extensions/board/result.ts +30 -39
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +49 -11
- package/extensions/board/snapshot.ts +8 -6
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +154 -55
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +36 -42
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +35 -16
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +90 -28
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +38 -13
- package/extensions/board/view/render.ts +219 -277
- package/extensions/board/view/styles.ts +130 -32
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +7 -8
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +10 -7
- package/extensions/bytefly/footer/render/segments.ts +41 -38
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/render.ts +10 -1
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +89 -79
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -4
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/html.ts +73 -40
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +13 -14
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +9 -9
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +21 -9
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +58 -51
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +61 -41
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +102 -2
- package/extensions/plan/web-collab.ts +4 -4
- package/extensions/plan/web-comments.ts +39 -8
- package/extensions/plan/web-composer.ts +14 -3
- package/extensions/plan/web-keys.ts +149 -25
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +10 -68
- package/extensions/plan/web-outline.ts +22 -2
- package/extensions/plan/web-page.ts +56 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +23 -13
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web-settle.ts +14 -7
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +67 -131
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +39 -0
- package/extensions/serve/index.ts +85 -0
- package/extensions/serve/registry.ts +260 -0
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +17 -9
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -5
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +28 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +30 -5
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +199 -51
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +119 -36
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +81 -63
- package/lib/bus-request.ts +83 -0
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +17 -11
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +30 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +46 -21
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +10 -10
- package/lib/session-label.ts +129 -7
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +39 -22
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +29 -11
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +11 -11
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +33 -4
- package/lib/web/README.md +21 -0
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +20 -2
- package/lib/web/runtime.ts +9 -10
- package/package.json +8 -2
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/config.ts +0 -36
- package/extensions/board/seat.ts +0 -28
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -17,6 +17,7 @@ import { createHash } from "node:crypto";
|
|
|
17
17
|
import { stripControlChars } from "../../lib/format.ts";
|
|
18
18
|
import { displayFeedback, stripFeedback } from "./feedback.ts";
|
|
19
19
|
import { extractSections, sectionKeys, type PlanSection } from "./sections.ts";
|
|
20
|
+
import { planBlocks } from "./blocks.ts";
|
|
20
21
|
|
|
21
22
|
/** Spaces a tab expands to; the terminal's own tab stops are not width-modelled. */
|
|
22
23
|
const TAB_WIDTH = 4;
|
|
@@ -27,6 +28,14 @@ export interface SectionDigest {
|
|
|
27
28
|
title: string;
|
|
28
29
|
/** SHA-256 of the section's normalized text, heading included. */
|
|
29
30
|
digest: string;
|
|
31
|
+
/**
|
|
32
|
+
* Short digests of this section's content blocks, in order.
|
|
33
|
+
*
|
|
34
|
+
* Absent in a baseline a build before block marks wrote, which is why every reader
|
|
35
|
+
* treats it as optional: a section with no block digests marks nothing, and the page
|
|
36
|
+
* says it cannot compare at that depth rather than reading as "nothing changed".
|
|
37
|
+
*/
|
|
38
|
+
blocks?: string[];
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
/**
|
|
@@ -60,6 +69,17 @@ function digestOf(text: string): string {
|
|
|
60
69
|
return createHash("sha256").update(text.trim()).digest("hex");
|
|
61
70
|
}
|
|
62
71
|
|
|
72
|
+
/**
|
|
73
|
+
* A short, stable digest of one block's lines.
|
|
74
|
+
*
|
|
75
|
+
* Short because it is persisted once per content block in a session entry: eight hex
|
|
76
|
+
* characters tell two blocks apart for the length of one review, which is all a change
|
|
77
|
+
* mark needs, and keep a long plan's baseline small enough to sit in the branch.
|
|
78
|
+
*/
|
|
79
|
+
export function blockDigest(lines: readonly string[]): string {
|
|
80
|
+
return digestOf(lines.join("\n")).slice(0, 8);
|
|
81
|
+
}
|
|
82
|
+
|
|
63
83
|
/**
|
|
64
84
|
* A short, stable id for one plan text. The review page keys this tab's unsaved
|
|
65
85
|
* draft on it, so a draft is restored for the plan it was written against and
|
|
@@ -74,9 +94,103 @@ export function sectionDigests(content: string): SectionDigest[] {
|
|
|
74
94
|
return extractSections(reviewText(content)).map((section) => ({
|
|
75
95
|
title: section.title,
|
|
76
96
|
digest: digestOf(section.text),
|
|
97
|
+
// Blank blocks are separators, not content: a plan that gained a blank line has not
|
|
98
|
+
// changed, and a digest per separator would say it had.
|
|
99
|
+
blocks: planBlocks(section.text)
|
|
100
|
+
.filter((block) => block.kind !== "blank")
|
|
101
|
+
.map((block) => blockDigest(block.lines)),
|
|
77
102
|
}));
|
|
78
103
|
}
|
|
79
104
|
|
|
105
|
+
/** Where the blocks changed, in the document the page is about to render. */
|
|
106
|
+
export interface BlockChangeMarks {
|
|
107
|
+
/** Absolute 0-based start lines of the blocks that are new or edited. */
|
|
108
|
+
starts: number[];
|
|
109
|
+
/** Content blocks each section no longer holds, keyed by the section's heading line. */
|
|
110
|
+
removed: Record<number, number>;
|
|
111
|
+
/**
|
|
112
|
+
* Whether the baseline carried block digests at all.
|
|
113
|
+
*
|
|
114
|
+
* False for a baseline a build before block marks wrote: section marks still mean
|
|
115
|
+
* something, block marks cannot, and the page says which instead of drawing nothing.
|
|
116
|
+
*/
|
|
117
|
+
compared: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Where the blocks changed since the baseline, as the rendered document numbers them.
|
|
122
|
+
*
|
|
123
|
+
* The pages marks what it draws, so every line here is a line of `current`: a section's
|
|
124
|
+
* blocks are numbered from its own heading, which is why the section's line is added
|
|
125
|
+
* back. Sections are paired the way {@link changedSections} pairs them — by occurrence,
|
|
126
|
+
* so a second `## Notes` is never compared with the first — and a section the baseline
|
|
127
|
+
* never held is all new: every block in it marks, because adding or renaming a heading is
|
|
128
|
+
* a refinement rather than a reason to refuse the comparison. Only a baseline section
|
|
129
|
+
* written before block digests is beyond comparing at that depth, and it alone clears
|
|
130
|
+
* `compared`.
|
|
131
|
+
*/
|
|
132
|
+
export function blockChangeMarks(baseline: readonly SectionDigest[] | undefined, current: string): BlockChangeMarks {
|
|
133
|
+
const sections = extractSections(reviewText(current));
|
|
134
|
+
if (baseline === undefined) return { starts: [], removed: {}, compared: false };
|
|
135
|
+
const before = new Map<string, SectionDigest>();
|
|
136
|
+
sectionKeys(baseline).forEach((key, at) => {
|
|
137
|
+
const section = baseline[at];
|
|
138
|
+
if (section !== undefined) before.set(key, section);
|
|
139
|
+
});
|
|
140
|
+
const starts: number[] = [];
|
|
141
|
+
const removed: Record<number, number> = {};
|
|
142
|
+
let compared = true;
|
|
143
|
+
sectionKeys(sections).forEach((key, at) => {
|
|
144
|
+
const section = sections[at];
|
|
145
|
+
if (section === undefined) return;
|
|
146
|
+
const prior = before.get(key);
|
|
147
|
+
if (prior !== undefined && prior.blocks === undefined) {
|
|
148
|
+
compared = false;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const mine = planBlocks(section.text).filter((block) => block.kind !== "blank");
|
|
152
|
+
// An unheld section pairs with no blocks, so every block in it is new.
|
|
153
|
+
const priorBlocks = new Set(prior?.blocks ?? []);
|
|
154
|
+
for (const block of mine) {
|
|
155
|
+
if (!priorBlocks.has(blockDigest(block.lines))) starts.push(section.line + block.start);
|
|
156
|
+
}
|
|
157
|
+
const shrunk = Math.max(0, (prior?.blocks?.length ?? 0) - mine.length);
|
|
158
|
+
if (shrunk > 0) removed[section.line] = shrunk;
|
|
159
|
+
});
|
|
160
|
+
return { starts, removed, compared };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** What one section's blocks look like against the baseline's. */
|
|
164
|
+
export interface BlockMarks {
|
|
165
|
+
/** Indexes, among the section's content blocks, of the blocks that are new or edited. */
|
|
166
|
+
changed: number[];
|
|
167
|
+
/** How many content blocks the section no longer holds: the part it shrank by. */
|
|
168
|
+
removed: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Mark the blocks a section gained or edited since the baseline.
|
|
173
|
+
*
|
|
174
|
+
* Membership rather than position: a block is changed when its digest is not among the
|
|
175
|
+
* baseline's, so an inserted step marks itself and leaves every block after it alone —
|
|
176
|
+
* the index-by-index pairing a diff would use would mark the whole rest of the section.
|
|
177
|
+
* A block that left cannot be drawn in place, so the section reports how much shorter it
|
|
178
|
+
* has become instead — the shrinkage, not the set difference, so an edited block reads
|
|
179
|
+
* as one change rather than as one removal beside one addition.
|
|
180
|
+
*
|
|
181
|
+
* An absent or blockless baseline marks nothing: a build before block marks wrote no
|
|
182
|
+
* block digests, and claiming every block changed would be a worse lie than saying the
|
|
183
|
+
* comparison cannot be made at that depth.
|
|
184
|
+
*/
|
|
185
|
+
export function changedBlocks(prior: readonly string[] | undefined, current: readonly string[]): BlockMarks {
|
|
186
|
+
if (prior === undefined) return { changed: [], removed: 0 };
|
|
187
|
+
const before = new Set(prior);
|
|
188
|
+
return {
|
|
189
|
+
changed: current.flatMap((digest, index) => (before.has(digest) ? [] : [index])),
|
|
190
|
+
removed: Math.max(0, prior.length - current.length),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
80
194
|
/** Whether two baselines describe the same content, section for section. */
|
|
81
195
|
export function sameSections(a: readonly SectionDigest[], b: readonly SectionDigest[]): boolean {
|
|
82
196
|
if (a.length !== b.length) return false;
|
package/extensions/plan/plans.ts
CHANGED
|
@@ -33,13 +33,12 @@ import {
|
|
|
33
33
|
unlinkSync,
|
|
34
34
|
writeFileSync,
|
|
35
35
|
} from "node:fs";
|
|
36
|
-
import {
|
|
37
|
-
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
36
|
+
import { join } from "node:path";
|
|
38
37
|
import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
39
38
|
import { resolveEffectiveCwd } from "../../lib/env.ts";
|
|
40
39
|
import { keepRevision, writeAtomic } from "./plan-files.ts";
|
|
41
40
|
import { sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
42
|
-
import {
|
|
41
|
+
import { isContainedPlan, isPlanFileName, plansDirFor, resolvePlanArg } from "../../lib/plans/paths.ts";
|
|
43
42
|
import { repoKeyFor } from "../../lib/repo-key.ts";
|
|
44
43
|
import { carryFeedback, formatFeedback, normalizeAuthor, type PlanAnnotation } from "./feedback.ts";
|
|
45
44
|
import { extractPlanSteps, matchesStepText, setStepStatuses, type StepUpdate } from "./steps.ts";
|
|
@@ -48,14 +47,6 @@ import { extractPlanSteps, matchesStepText, setStepStatuses, type StepUpdate } f
|
|
|
48
47
|
export const MAX_PLAN_BYTES = 256 * 1024;
|
|
49
48
|
/** Longest slug derived from a plan title. */
|
|
50
49
|
const MAX_SLUG_LENGTH = 60;
|
|
51
|
-
/** The agent-directory entry plans are filed under. */
|
|
52
|
-
const PLANS_DIRNAME = "plans";
|
|
53
|
-
|
|
54
|
-
/** The plan store: `<agent-dir>/plans`, or `<agent-dir>/plans/<key>` inside a repository. */
|
|
55
|
-
function plansDir(agentDir: string, key?: string): string {
|
|
56
|
-
return key === undefined ? join(agentDir, PLANS_DIRNAME) : join(agentDir, PLANS_DIRNAME, key);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
50
|
/** A plan read from disk. */
|
|
60
51
|
export interface StoredPlan {
|
|
61
52
|
/** Absolute path. */
|
|
@@ -160,46 +151,6 @@ export function stamp(date: Date): string {
|
|
|
160
151
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}`;
|
|
161
152
|
}
|
|
162
153
|
|
|
163
|
-
/** A plan file name: markdown and not a dotfile, so the self-ignore file is never a target. */
|
|
164
|
-
function isPlanFileName(path: string): boolean {
|
|
165
|
-
const name = basename(path);
|
|
166
|
-
return name.toLowerCase().endsWith(".md") && !name.startsWith(".");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Whether `path` may be read, written, or deleted as a plan. Beyond lexical
|
|
171
|
-
* containment (shared with the other path guards through `lib/path.ts`), this
|
|
172
|
-
* rejects dotfiles (including the directory's own `.gitignore`) and resolves
|
|
173
|
-
* symlinks, so a link placed inside the plans directory cannot redirect a read
|
|
174
|
-
* or write outside it.
|
|
175
|
-
*/
|
|
176
|
-
function isContainedPlan(dir: string, path: string): boolean {
|
|
177
|
-
if (!isInside(dir, path) || !isPlanFileName(path)) return false;
|
|
178
|
-
try {
|
|
179
|
-
// A symlink itself must never be the target; a missing file is a new plan.
|
|
180
|
-
if (lstatSync(path).isSymbolicLink()) return false;
|
|
181
|
-
} catch {
|
|
182
|
-
// Missing (ENOENT) is expected; any other error falls through to the
|
|
183
|
-
// real-path check, which rejects the path if it cannot be resolved.
|
|
184
|
-
}
|
|
185
|
-
return isInsideReal(dir, dirname(path));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Resolve a `plan_path` argument against the store.
|
|
190
|
-
*
|
|
191
|
-
* A plan lives outside the project, so a path relative to the working directory
|
|
192
|
-
* can no longer name one. Absolute paths and the `~/…` form the tools display are
|
|
193
|
-
* accepted, and a bare name resolves inside the store — which is what a reader of
|
|
194
|
-
* a tool result has in hand.
|
|
195
|
-
*/
|
|
196
|
-
function resolvePlanArg(dir: string, planPath: string): string {
|
|
197
|
-
const text = planPath.trim();
|
|
198
|
-
if (text === "~") return homedir();
|
|
199
|
-
if (text.startsWith("~/") || text.startsWith(`~${sep}`)) return resolve(homedir(), text.slice(2));
|
|
200
|
-
return isAbsolute(text) ? resolve(text) : resolve(dir, text);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
154
|
function uniquePath(dir: string, base: string): string {
|
|
204
155
|
const taken = (candidate: string): boolean => {
|
|
205
156
|
try {
|
|
@@ -300,7 +251,7 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
300
251
|
const cached = dirCache.get(base);
|
|
301
252
|
if (cached) return cached;
|
|
302
253
|
const repo = await repoKeyFor(pi, base);
|
|
303
|
-
const dir =
|
|
254
|
+
const dir = plansDirFor(getAgentDir(), repo);
|
|
304
255
|
dirCache.set(base, { dir, root: repo.fromGit ? repo.root : undefined });
|
|
305
256
|
if (repo.fromGit) {
|
|
306
257
|
// The keyed directory is the store; a store an older version left in the
|
|
@@ -387,7 +338,7 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
387
338
|
|
|
388
339
|
const setSteps = async (cwd: string, planPath: string, updates: StepUpdate[]): Promise<StoredPlan> => {
|
|
389
340
|
const plan = await read(cwd, planPath);
|
|
390
|
-
if (!plan) throw new Error("No plan file there; pass a plan_path written by
|
|
341
|
+
if (!plan) throw new Error("No plan file there; pass a plan_path written by `plan` action `write`.");
|
|
391
342
|
const steps = extractPlanSteps(plan.content);
|
|
392
343
|
const total = steps.length;
|
|
393
344
|
const outOfRange = updates.find(
|
|
@@ -433,7 +384,7 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
433
384
|
options: { author?: string } = {},
|
|
434
385
|
): Promise<StoredPlan> => {
|
|
435
386
|
const plan = await read(cwd, planPath);
|
|
436
|
-
if (!plan) throw new Error("No plan file there; pass a plan_path written by
|
|
387
|
+
if (!plan) throw new Error("No plan file there; pass a plan_path written by `plan` action `write`.");
|
|
437
388
|
const author = normalizeAuthor(options.author);
|
|
438
389
|
// An untagged entry is the writer's own; one tagged with a different name is
|
|
439
390
|
// another writer's comment, so it is dropped rather than re-signed.
|
|
@@ -25,8 +25,6 @@ import {
|
|
|
25
25
|
WEB_SEARCH_TOOL,
|
|
26
26
|
} from "../../lib/tool-names.ts";
|
|
27
27
|
|
|
28
|
-
import { ENTER_TOOL } from "./runtime.ts";
|
|
29
|
-
|
|
30
28
|
/**
|
|
31
29
|
* Tools that stay available while planning: structured readers (documents
|
|
32
30
|
* included), the trackers, the git-only `bash`, and the known web/question
|
|
@@ -53,23 +51,23 @@ export const PLAN_SAFE_TOOLS = [
|
|
|
53
51
|
*/
|
|
54
52
|
export const PLAN_ALLOWED_TOOLS = [...PLAN_SAFE_TOOLS, SUBAGENT_TOOL, BOARD_TOOL];
|
|
55
53
|
/**
|
|
56
|
-
* Tools always blocked while planning, even when annotated read-only.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
54
|
+
* Tools always blocked while planning, even when annotated read-only. The `plan`
|
|
55
|
+
* tool is not one of them: plan mode keeps it active in both modes, because its
|
|
56
|
+
* actions are the mode's own verbs and each one refuses outside its mode.
|
|
59
57
|
*/
|
|
60
|
-
const ALWAYS_BLOCKED_TOOLS = ["write", "edit"
|
|
58
|
+
const ALWAYS_BLOCKED_TOOLS = ["write", "edit"];
|
|
61
59
|
|
|
62
60
|
/** Shown when a tool call is refused while planning. */
|
|
63
|
-
export const BLOCKED_GUIDANCE = "Call
|
|
61
|
+
export const BLOCKED_GUIDANCE = "Call `plan` action `exit` and get approval before using it.";
|
|
64
62
|
|
|
65
63
|
/** Appended to a refused `bash` call; names the readers and the exit hatch. */
|
|
66
64
|
export const BASH_BLOCKED_GUIDANCE =
|
|
67
|
-
"Use the read, grep, find, and ls tools for files, or call
|
|
65
|
+
"Use the read, grep, find, and ls tools for files, or call `plan` action `exit` and get approval before using other shell commands.";
|
|
68
66
|
|
|
69
67
|
/**
|
|
70
68
|
* Build plan mode's read-only policy: known readers and the plan/goal trackers
|
|
71
|
-
* are allowed, write/edit
|
|
72
|
-
*
|
|
69
|
+
* are allowed, write/edit and the shells are always blocked, and every other tool
|
|
70
|
+
* must carry the MCP `readOnlyHint` to pass.
|
|
73
71
|
*/
|
|
74
72
|
export function createPlanPolicy(pi: ExtensionAPI): ReadOnlyPolicy {
|
|
75
73
|
return createReadOnlyPolicy({
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan review gate, loaded when a review actually asks a human.
|
|
3
|
+
*
|
|
4
|
+
* `review.ts` owns the loop, the TUI screen (`tui.ts`), the compact remote prompt,
|
|
5
|
+
* and the baseline rule — about 1,000 lines that registration never needs:
|
|
6
|
+
* `/plan review` and the `plan` tool's `exit` action are the only two doors in. Both import this
|
|
7
|
+
* module instead, so the loop loads once, on the first review of the session.
|
|
8
|
+
*
|
|
9
|
+
* The types stay here rather than in `review.ts` because a type import is erased:
|
|
10
|
+
* the `plan` tool declares its `exit` details from them at registration time.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import type { SectionDigest } from "./plan-text.ts";
|
|
15
|
+
import type { PlanStore, StoredPlan } from "./plans.ts";
|
|
16
|
+
import type { PlanAnnotation } from "./feedback.ts";
|
|
17
|
+
import type { PlanReviewResult, PlanReviewSession, PlanWebReview } from "./web.ts";
|
|
18
|
+
|
|
19
|
+
/** A choice from the review screen, with the refinement the screen carried, if any. */
|
|
20
|
+
export interface PlanReviewChoice {
|
|
21
|
+
action: "approve" | "refine" | "keep" | "cancel";
|
|
22
|
+
refinement?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What a review decided, and the plan state it decided on. */
|
|
26
|
+
export type ReviewDecision =
|
|
27
|
+
| { action: "approve"; plan: StoredPlan; comments: PlanAnnotation[] }
|
|
28
|
+
| {
|
|
29
|
+
action: "refine";
|
|
30
|
+
plan: StoredPlan;
|
|
31
|
+
/** The refinement request: the saved comments, or what the user typed. */
|
|
32
|
+
refinement: string;
|
|
33
|
+
comments: PlanAnnotation[];
|
|
34
|
+
/** True when the request came from the browser, which is reported differently. */
|
|
35
|
+
fromBrowser: boolean;
|
|
36
|
+
}
|
|
37
|
+
| { action: "keep"; plan: StoredPlan }
|
|
38
|
+
/** Leave plan mode without approving; the plan file stays for a later review. */
|
|
39
|
+
| { action: "cancel"; plan: StoredPlan };
|
|
40
|
+
|
|
41
|
+
/** What the previous review showed, so this one can mark what changed. */
|
|
42
|
+
export interface ReviewBaselines {
|
|
43
|
+
/** The sections the last review of `path` showed, when this branch has one. */
|
|
44
|
+
reviewed(path: string): SectionDigest[] | undefined;
|
|
45
|
+
/** Remember what a review just showed, as a session entry on this branch. */
|
|
46
|
+
markReviewed(path: string, content: string): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Everything one review needs: whom to ask, where to ask, and what to compare against. */
|
|
50
|
+
export interface ReviewPlanOptions {
|
|
51
|
+
ctx: ExtensionToolContext;
|
|
52
|
+
web: PlanWebReview;
|
|
53
|
+
/** Re-read the plan after a browser review that may have changed it. */
|
|
54
|
+
plans: PlanStore;
|
|
55
|
+
baselines: ReviewBaselines;
|
|
56
|
+
plan: StoredPlan;
|
|
57
|
+
/** Aborting the turn closes the browser server. */
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The review module, as the loader returns it. */
|
|
62
|
+
export type ReviewModule = typeof import("./review.ts");
|
|
63
|
+
|
|
64
|
+
/** How a review reaches its implementation; production uses the dynamic import. */
|
|
65
|
+
export type ReviewLoader = () => Promise<ReviewModule>;
|
|
66
|
+
|
|
67
|
+
/** Ask the human about `plan` and return one decision, loading the gate on first use. */
|
|
68
|
+
export async function reviewPlan(options: ReviewPlanOptions, load?: ReviewLoader): Promise<ReviewDecision> {
|
|
69
|
+
const review = await (load ?? (() => import("./review.ts")))();
|
|
70
|
+
return review.reviewPlan(options);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The browser-only review `/plan review` opens, loaded on first use. */
|
|
74
|
+
export async function runBrowserReview(
|
|
75
|
+
web: PlanWebReview,
|
|
76
|
+
cwd: string,
|
|
77
|
+
plan: StoredPlan,
|
|
78
|
+
options: Parameters<ReviewModule["runBrowserReview"]>[3],
|
|
79
|
+
load?: ReviewLoader,
|
|
80
|
+
): Promise<PlanReviewResult | undefined> {
|
|
81
|
+
const review = await (load ?? (() => import("./review.ts")))();
|
|
82
|
+
return review.runBrowserReview(web, cwd, plan, options);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type { PlanReviewSession };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* A review has three presentations — the scrollable TUI screen, a dialog-only
|
|
5
5
|
* editor, and a remote turn's compact editor prompt — and one optional browser
|
|
6
6
|
* server. This module owns the loop that switches between
|
|
7
|
-
* them, so `
|
|
7
|
+
* them, so the `plan` tool's `exit` action and `/plan review` share one lifecycle and only map
|
|
8
8
|
* the returned {@link ReviewDecision} to their own result.
|
|
9
9
|
*
|
|
10
10
|
* It also owns the review baseline: a review marks the content the human was
|
|
@@ -118,7 +118,7 @@ async function askPlanReview(
|
|
|
118
118
|
|
|
119
119
|
/** How a browser review may decide, and what to report when it opens. */
|
|
120
120
|
export interface BrowserReviewOptions {
|
|
121
|
-
/** Whether the browser may approve the plan (only the `
|
|
121
|
+
/** Whether the browser may approve the plan (only the `plan` tool's `exit` flow). */
|
|
122
122
|
canApprove: boolean;
|
|
123
123
|
/** The section digests the previous review showed, for the changed markers. */
|
|
124
124
|
baseline?: SectionDigest[];
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
* Session-scoped state and tool gating for plan mode.
|
|
3
3
|
*
|
|
4
4
|
* Enabling plan mode removes every tool the policy does not consider read-only
|
|
5
|
-
* from the active set and
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* persisted as a custom session entry so `/resume` and `/tree`
|
|
9
|
-
* branch, and it is also mirrored into the footer status. The review
|
|
10
|
-
* travels the same way, as one digest per heading section, so the
|
|
11
|
-
* markers survive a resume too.
|
|
5
|
+
* from the active set, and keeps the one `plan` tool active in both modes: its
|
|
6
|
+
* actions are what ask to enter, save the artifact under the plans directory,
|
|
7
|
+
* present it, tick it, and leave. Disabling restores exactly what it hid. The
|
|
8
|
+
* enabled flag is persisted as a custom session entry so `/resume` and `/tree`
|
|
9
|
+
* follow the branch, and it is also mirrored into the footer status. The review
|
|
10
|
+
* baseline travels the same way, as one digest per heading section, so the
|
|
11
|
+
* changed-section markers survive a resume too.
|
|
12
12
|
*
|
|
13
|
-
* `
|
|
14
|
-
* in `index.ts` permits
|
|
15
|
-
* extension's (matched by source path), so a same-named tool from another
|
|
16
|
-
* extension cannot borrow the exemption.
|
|
13
|
+
* The `plan` tool's `write` action is the one sanctioned write while planning. The
|
|
14
|
+
* read-only guard in `index.ts` permits that tool only when the registered one is
|
|
15
|
+
* this extension's (matched by source path), so a same-named tool from another
|
|
16
|
+
* extension cannot borrow the exemption, and each action refuses outside its mode.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { basename, resolve } from "node:path";
|
|
@@ -23,23 +23,25 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
23
23
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
24
24
|
import { PLAN_MODE_ENTRY_TYPE } from "../../lib/plan-state.ts";
|
|
25
25
|
import type { ReadOnlyPolicy } from "../../lib/policy.ts";
|
|
26
|
-
import {
|
|
26
|
+
import { PLAN_TOOL as PLAN_TOOL_NAME } from "../../lib/tool-names.ts";
|
|
27
27
|
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
28
|
-
import { paint } from "../../lib/tui/tokens.ts";
|
|
29
28
|
import { createPlanStore, type PlanStore } from "./plans.ts";
|
|
30
29
|
import { sameSections, sectionDigests, type SectionDigest } from "./plan-text.ts";
|
|
31
30
|
import type { PlanModeEntry, ReviewBaselineEntry, SeededStepsEntry } from "./types.ts";
|
|
32
31
|
|
|
33
|
-
/**
|
|
34
|
-
export const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
export const
|
|
32
|
+
/** The one model-facing tool: every planning verb is one of its actions. */
|
|
33
|
+
export const PLAN_TOOL = PLAN_TOOL_NAME;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The verbs of the one `plan` tool, in the order the schema offers them.
|
|
37
|
+
*
|
|
38
|
+
* One action tool rather than five tools, so every verb shares a name and a
|
|
39
|
+
* registration; the order is the one the description walks them in.
|
|
40
|
+
*/
|
|
41
|
+
export const PLAN_ACTIONS = ["enter", "write", "exit", "steps", "cancel"] as const;
|
|
42
|
+
|
|
43
|
+
/** One verb of the `plan` tool. */
|
|
44
|
+
export type PlanAction = (typeof PLAN_ACTIONS)[number];
|
|
43
45
|
/** Custom-entry type used for plan-mode state. */
|
|
44
46
|
export const STATE_TYPE = PLAN_MODE_ENTRY_TYPE;
|
|
45
47
|
/** Custom-entry type used for a plan's review baseline. */
|
|
@@ -50,16 +52,13 @@ export const SEEDED_TYPE = "plan-seeded";
|
|
|
50
52
|
/** Longest plan-file basename shown in the footer chip. */
|
|
51
53
|
const STATUS_NAME_LENGTH = 28;
|
|
52
54
|
|
|
53
|
-
/** Tools plan mode activates itself. */
|
|
54
|
-
const CONTROL_TOOLS = [EXIT_TOOL, CANCEL_TOOL, WRITE_PLAN_TOOL];
|
|
55
|
-
|
|
56
55
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
56
|
+
* The one tool plan mode keeps in the active set in both modes. Every other tool
|
|
57
|
+
* follows the policy, which is about writing files and cannot answer for a tool
|
|
58
|
+
* whose actions are the mode's own verbs: hiding it while planning would hide the
|
|
59
|
+
* way to write the plan and the way out.
|
|
61
60
|
*/
|
|
62
|
-
const
|
|
61
|
+
const MODE_TOOLS = [PLAN_TOOL];
|
|
63
62
|
|
|
64
63
|
export interface PlanRuntime {
|
|
65
64
|
/** Whether plan mode is currently on. */
|
|
@@ -69,9 +68,9 @@ export interface PlanRuntime {
|
|
|
69
68
|
toggle(ctx: ExtensionContext): void;
|
|
70
69
|
/** Restore state from the branch and the `--plan` flag. */
|
|
71
70
|
restore(ctx: ExtensionContext): void;
|
|
72
|
-
/** Plan-file storage for `
|
|
71
|
+
/** Plan-file storage for the `write` and `exit` actions. */
|
|
73
72
|
plans: PlanStore;
|
|
74
|
-
/** Whether `toolName` is
|
|
73
|
+
/** Whether `toolName` is the `plan` tool (not a same-named impostor) the read-only guard exempts. */
|
|
75
74
|
isOwnTool(toolName: string): boolean;
|
|
76
75
|
/** Path of the most recently written plan, for the footer chip. */
|
|
77
76
|
lastPlanPath(): string | undefined;
|
|
@@ -100,9 +99,19 @@ function readSections(value: unknown): SectionDigest[] | undefined {
|
|
|
100
99
|
if (!Array.isArray(value)) return undefined;
|
|
101
100
|
const sections: SectionDigest[] = [];
|
|
102
101
|
for (const raw of value) {
|
|
103
|
-
const section = raw as { title?: unknown; digest?: unknown } | null | undefined;
|
|
102
|
+
const section = raw as { title?: unknown; digest?: unknown; blocks?: unknown } | null | undefined;
|
|
104
103
|
if (typeof section?.title !== "string" || typeof section.digest !== "string") return undefined;
|
|
105
|
-
|
|
104
|
+
// Branch data is untrusted, so the block digests are kept only when they are a list
|
|
105
|
+
// of strings: a baseline written before block marks simply has none, and one that
|
|
106
|
+
// holds something else is read at section depth rather than repaired.
|
|
107
|
+
const blocks = Array.isArray(section.blocks)
|
|
108
|
+
? section.blocks.filter((entry): entry is string => typeof entry === "string")
|
|
109
|
+
: undefined;
|
|
110
|
+
sections.push(
|
|
111
|
+
blocks === undefined
|
|
112
|
+
? { title: section.title, digest: section.digest }
|
|
113
|
+
: { title: section.title, digest: section.digest, blocks },
|
|
114
|
+
);
|
|
106
115
|
}
|
|
107
116
|
return sections;
|
|
108
117
|
}
|
|
@@ -185,6 +194,9 @@ export function createPlanRuntime(
|
|
|
185
194
|
// Tools plan mode deactivated, so disabling restores exactly those and does
|
|
186
195
|
// not re-activate a tool another extension intentionally hid.
|
|
187
196
|
let removedForPlan: string[] = [];
|
|
197
|
+
// Mode tools this enable had to activate, so disabling puts the user's own
|
|
198
|
+
// active set back instead of widening it.
|
|
199
|
+
let addedForPlan: string[] = [];
|
|
188
200
|
|
|
189
201
|
const plans = createPlanStore(pi);
|
|
190
202
|
|
|
@@ -194,7 +206,7 @@ export function createPlanRuntime(
|
|
|
194
206
|
* is rejected.
|
|
195
207
|
*/
|
|
196
208
|
const isOwnTool = (toolName: string): boolean => {
|
|
197
|
-
if (
|
|
209
|
+
if (toolName !== PLAN_TOOL) return false;
|
|
198
210
|
const info = pi.getAllTools().find((tool) => tool.name === toolName);
|
|
199
211
|
const source = info?.sourceInfo?.path;
|
|
200
212
|
return source === undefined || options.entryPath === undefined || samePath(source, options.entryPath);
|
|
@@ -202,7 +214,7 @@ export function createPlanRuntime(
|
|
|
202
214
|
|
|
203
215
|
/** Tools plan mode would hide from the current active set. */
|
|
204
216
|
const captureHidden = (): string[] => [
|
|
205
|
-
...new Set(pi.getActiveTools().filter((name) => !policy.isAllowed(name) && !
|
|
217
|
+
...new Set(pi.getActiveTools().filter((name) => !policy.isAllowed(name) && !MODE_TOOLS.includes(name))),
|
|
206
218
|
];
|
|
207
219
|
|
|
208
220
|
/**
|
|
@@ -214,12 +226,18 @@ export function createPlanRuntime(
|
|
|
214
226
|
const active = pi.getActiveTools();
|
|
215
227
|
if (enabled) {
|
|
216
228
|
// Keep only tools the policy considers safe to expose while read-only
|
|
217
|
-
// (structured readers and the plan/goal trackers) plus the
|
|
218
|
-
|
|
229
|
+
// (structured readers and the plan/goal trackers), plus the `plan` tool
|
|
230
|
+
// itself, whose actions are the mode's own vocabulary.
|
|
231
|
+
addedForPlan = MODE_TOOLS.filter((name) => !active.includes(name));
|
|
232
|
+
pi.setActiveTools([...new Set([...active.filter((name) => policy.isAllowed(name)), ...MODE_TOOLS])]);
|
|
219
233
|
return;
|
|
220
234
|
}
|
|
235
|
+
// A mode tool this enable had to activate goes back to how the user left it,
|
|
236
|
+
// rather than staying active because the mode needed it for a while.
|
|
237
|
+
const kept = active.filter((name) => !addedForPlan.includes(name));
|
|
238
|
+
addedForPlan = [];
|
|
221
239
|
const restored = addHidden ? removedForPlan : [];
|
|
222
|
-
pi.setActiveTools([...new Set([...
|
|
240
|
+
pi.setActiveTools([...new Set([...kept, ...restored])]);
|
|
223
241
|
removedForPlan = [];
|
|
224
242
|
};
|
|
225
243
|
|
|
@@ -227,7 +245,9 @@ export function createPlanRuntime(
|
|
|
227
245
|
lastPlanPath ? `${GLYPHS.plan} plan · ${statusName(lastPlanPath)}` : `${GLYPHS.plan} plan`;
|
|
228
246
|
|
|
229
247
|
const setStatus = (ctx: ExtensionContext): void => {
|
|
230
|
-
|
|
248
|
+
// Published unstyled: the chip's text is what a footer without bytefly shows, and
|
|
249
|
+
// bytefly's mode segment paints the mode by role instead of reading this pill.
|
|
250
|
+
ctx.ui.setStatus(STATUS_KEYS.planMode, enabled ? statusText() : undefined);
|
|
231
251
|
};
|
|
232
252
|
|
|
233
253
|
const setEnabled = (next: boolean, ctx: ExtensionContext | undefined, persist: boolean): void => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Plan-document sections (pure).
|
|
3
3
|
*
|
|
4
4
|
* One fence-aware scan of a plan's markdown: the review screen navigates by
|
|
5
|
-
* section, `
|
|
5
|
+
* section, the `plan` tool's `steps` action looks up `## Steps`, a carried comment finds the
|
|
6
6
|
* block it belongs to, and the browser outline lists the same headings. A second
|
|
7
7
|
* scanner would disagree about a heading inside a fence, so every reader goes
|
|
8
8
|
* through this module.
|
package/extensions/plan/steps.ts
CHANGED
|
@@ -31,7 +31,7 @@ export const MAX_STEP_LENGTH = 200;
|
|
|
31
31
|
* section that carries the end state — the same surface before and after when the
|
|
32
32
|
* change alters one that already exists, one frame when it is new — in a fence
|
|
33
33
|
* that stays untagged unless every line is code, so the reviewer sees the result
|
|
34
|
-
* before the steps that build it. Guidance only: `
|
|
34
|
+
* before the steps that build it. Guidance only: the `write` action never rejects or
|
|
35
35
|
* rewrites a plan that omits a section, and a change with no visible surface drops
|
|
36
36
|
* `Visual`.
|
|
37
37
|
*/
|
|
@@ -268,7 +268,7 @@ const LIST_PREFIX = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s*)?/;
|
|
|
268
268
|
* list markers and inline emphasis, and the lines are joined with `; `, prefixed
|
|
269
269
|
* with `Verify:` and elided like a long step. The item is deliberately *not* a
|
|
270
270
|
* plan step: it is not in the `Steps` section, so it must not count against the
|
|
271
|
-
* step cap or appear in `
|
|
271
|
+
* step cap or appear in the `steps` action's numbering.
|
|
272
272
|
*
|
|
273
273
|
* A plan with no `Steps` heading feeds its shallowest list as steps (the older
|
|
274
274
|
* rule), so that list already covers the verification bullets; appending a
|
|
@@ -297,7 +297,7 @@ export function extractPlanSteps(plan: string): PlanStep[] {
|
|
|
297
297
|
return topLevelItems(plan).map((item) => ({ content: elide(item.step.content), status: item.step.status }));
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
/** The full cost of a plan's step list, for the warning `
|
|
300
|
+
/** The full cost of a plan's step list, for the warning the `write` action reports. */
|
|
301
301
|
export interface PlanStepSummary {
|
|
302
302
|
/** Top-level items in the step list, before the {@link MAX_STEPS} cap. */
|
|
303
303
|
total: number;
|
package/extensions/plan/todos.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface TodoMerge {
|
|
|
44
44
|
/** Fallback list cap when the registered todo tool does not advertise one. */
|
|
45
45
|
export const DEFAULT_TODO_MAX = 50;
|
|
46
46
|
|
|
47
|
-
/** What the tick-time mirror did for one `
|
|
47
|
+
/** What the tick-time mirror did for one `steps` batch. */
|
|
48
48
|
export interface TodoMirror {
|
|
49
49
|
/** The list after the matching items were brought in line with the plan. */
|
|
50
50
|
todos: ExistingTodo[];
|