@gavin-hu/my-pi-agent 0.21.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 -31
- package/README.zh-CN.md +40 -26
- 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 -24
- package/docs/extensions/board.md +361 -147
- package/docs/extensions/bytefly.md +152 -111
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +30 -31
- 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 +166 -126
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +34 -18
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +72 -33
- 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 +56 -15
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +5 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +4 -40
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +13 -11
- package/extensions/board/result.ts +30 -16
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +42 -10
- package/extensions/board/snapshot.ts +2 -1
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +131 -44
- package/extensions/board/types.ts +36 -32
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +33 -15
- 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 +89 -21
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +23 -9
- package/extensions/board/view/render.ts +208 -271
- package/extensions/board/view/styles.ts +123 -31
- 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 +4 -2
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/lines.ts +7 -4
- package/extensions/bytefly/footer/render/segments.ts +37 -10
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- 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/{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/index.ts +67 -36
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -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 +3 -3
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
- 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/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +53 -52
- package/extensions/plan/plan-index.ts +4 -2
- 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 +49 -39
- 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-client.ts +3 -0
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +9 -67
- package/extensions/plan/web-payload.ts +3 -3
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web.ts +1 -12
- 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 +10 -8
- package/extensions/serve/index.ts +17 -3
- package/extensions/serve/registry.ts +26 -17
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +2 -1
- 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/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- 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 -7
- 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/view/todo-screen.ts +122 -0
- 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 +27 -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 +1 -1
- 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 +136 -54
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +87 -30
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +70 -58
- 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 +14 -12
- package/lib/http/port.ts +15 -10
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +39 -10
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +8 -8
- package/lib/session-label.ts +111 -5
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +5 -4
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +23 -8
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +9 -7
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +3 -3
- package/lib/web/README.md +21 -0
- 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 +2 -1
- package/lib/web/runtime.ts +9 -10
- package/package.json +6 -1
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/seat.ts +0 -28
- package/extensions/todo/tui.ts +0 -212
- 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
|
@@ -202,6 +202,119 @@ function checkFlagAndPositional(rest: string[], spec: RestCheck, label: string):
|
|
|
202
202
|
return { ok: true };
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/** `git config` flags that write the file, each of which the guard refuses by name. */
|
|
206
|
+
const CONFIG_WRITE_FLAGS = new Set([
|
|
207
|
+
"--add",
|
|
208
|
+
"--edit",
|
|
209
|
+
"-e",
|
|
210
|
+
"--remove-section",
|
|
211
|
+
"--rename-section",
|
|
212
|
+
"--replace-all",
|
|
213
|
+
"--unset",
|
|
214
|
+
"--unset-all",
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
/** `git config` flags whose value is the next token, or the part after `=`. */
|
|
218
|
+
const CONFIG_VALUE_FLAGS = new Set([
|
|
219
|
+
"--get",
|
|
220
|
+
"--get-all",
|
|
221
|
+
"--get-regexp",
|
|
222
|
+
"--get-urlmatch",
|
|
223
|
+
"--get-color",
|
|
224
|
+
"--get-colorbool",
|
|
225
|
+
"--default",
|
|
226
|
+
"--file",
|
|
227
|
+
"-f",
|
|
228
|
+
"--type",
|
|
229
|
+
"--blob",
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
/** `git config` flags that select or shape a read without taking a value. */
|
|
233
|
+
const CONFIG_READ_FLAGS = new Set([
|
|
234
|
+
"--list",
|
|
235
|
+
"-l",
|
|
236
|
+
"--show-origin",
|
|
237
|
+
"--show-scope",
|
|
238
|
+
"--name-only",
|
|
239
|
+
"--null",
|
|
240
|
+
"-z",
|
|
241
|
+
"--local",
|
|
242
|
+
"--global",
|
|
243
|
+
"--system",
|
|
244
|
+
"--worktree",
|
|
245
|
+
"--includes",
|
|
246
|
+
"--no-includes",
|
|
247
|
+
"--bool",
|
|
248
|
+
"--int",
|
|
249
|
+
"--bool-or-int",
|
|
250
|
+
"--path",
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
/** `git config` flags that ask for a value rather than only shaping the output. */
|
|
254
|
+
const CONFIG_GET_FLAGS = new Set([
|
|
255
|
+
"--get",
|
|
256
|
+
"--get-all",
|
|
257
|
+
"--get-regexp",
|
|
258
|
+
"--get-urlmatch",
|
|
259
|
+
"--get-color",
|
|
260
|
+
"--get-colorbool",
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* `git config` reads only, by allowlist.
|
|
265
|
+
*
|
|
266
|
+
* Asking whether a read flag appeared *anywhere* let every write through beside one:
|
|
267
|
+
* `git config --show-scope --replace-all core.fsmonitor /tmp/hook.sh` wrote `.git/config`
|
|
268
|
+
* from inside plan mode, and the `git status` that followed ran the hook. Each flag is now
|
|
269
|
+
* one this guard knows, a write action is refused by name, positionals are at most one, and
|
|
270
|
+
* the only forms left are the ones git reads with: `--get*`, `--list`, or one bare name.
|
|
271
|
+
*/
|
|
272
|
+
function checkConfig(rest: string[]): GitCommandVerdict {
|
|
273
|
+
const tokens = rest.map(unquote);
|
|
274
|
+
const positionals: string[] = [];
|
|
275
|
+
let reads = false;
|
|
276
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
277
|
+
const token = tokens[index] ?? "";
|
|
278
|
+
if (!token.startsWith("-")) {
|
|
279
|
+
positionals.push(token);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const [flag = "", inline] = token.split("=", 2);
|
|
283
|
+
if (CONFIG_WRITE_FLAGS.has(flag)) {
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
reason: `"git config ${flag}" writes .git/config; "git config" may only read while planning.`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (!CONFIG_READ_FLAGS.has(flag) && !CONFIG_VALUE_FLAGS.has(flag)) {
|
|
290
|
+
return { ok: false, reason: `"git config" flag "${token}" is not a read-only option.` };
|
|
291
|
+
}
|
|
292
|
+
if (!CONFIG_VALUE_FLAGS.has(flag)) {
|
|
293
|
+
if (flag === "--list" || flag === "-l") reads = true;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (CONFIG_GET_FLAGS.has(flag)) reads = true;
|
|
297
|
+
if (inline !== undefined) continue;
|
|
298
|
+
const value = tokens[index + 1];
|
|
299
|
+
// A flag is not a value: reading `--replace-all` as `--get`'s key would refuse nothing
|
|
300
|
+
// and leave git to act on it, so the value is refused rather than guessed.
|
|
301
|
+
if (value === undefined || value.startsWith("-")) {
|
|
302
|
+
return { ok: false, reason: `"git config ${flag}" needs a value, and plan mode does not guess one.` };
|
|
303
|
+
}
|
|
304
|
+
index += 1;
|
|
305
|
+
}
|
|
306
|
+
if (positionals.length > 1) {
|
|
307
|
+
return { ok: false, reason: '"git config <name> <value>" writes .git/config; plan mode reads config only.' };
|
|
308
|
+
}
|
|
309
|
+
if (!reads && positionals.length !== 1) {
|
|
310
|
+
return {
|
|
311
|
+
ok: false,
|
|
312
|
+
reason: '"git config" may only read; use "git config <name>", --get, --get-all, --get-regexp, or --list.',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return { ok: true };
|
|
316
|
+
}
|
|
317
|
+
|
|
205
318
|
/** Subcommand-specific read-only rules; the rest only pass the option denylist. */
|
|
206
319
|
function checkRest(subcommand: string, rest: string[]): GitCommandVerdict {
|
|
207
320
|
switch (subcommand) {
|
|
@@ -219,22 +332,8 @@ function checkRest(subcommand: string, rest: string[]): GitCommandVerdict {
|
|
|
219
332
|
reason: `"git remote ${first}" can change a remote; use "git remote -v" or "git remote show".`,
|
|
220
333
|
};
|
|
221
334
|
}
|
|
222
|
-
case "config":
|
|
223
|
-
|
|
224
|
-
const token = unquote(raw);
|
|
225
|
-
return (
|
|
226
|
-
token.startsWith("--get") ||
|
|
227
|
-
token === "--list" ||
|
|
228
|
-
token === "-l" ||
|
|
229
|
-
token === "--show-origin" ||
|
|
230
|
-
token === "--show-scope"
|
|
231
|
-
);
|
|
232
|
-
});
|
|
233
|
-
if (!hasReadFlag) {
|
|
234
|
-
return { ok: false, reason: '"git config" may only read; add --get, --get-all, --get-regexp, or --list.' };
|
|
235
|
-
}
|
|
236
|
-
return { ok: true };
|
|
237
|
-
}
|
|
335
|
+
case "config":
|
|
336
|
+
return checkConfig(rest);
|
|
238
337
|
case "stash": {
|
|
239
338
|
const [first] = rest.map(unquote);
|
|
240
339
|
if (first === "list" || first === "show") return { ok: true };
|
package/extensions/plan/index.ts
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* active set or blocked: write and edit are hidden, bash is limited to a single
|
|
6
6
|
* read-only git command, powershell is blocked, and everything else that mutates
|
|
7
7
|
* (worktree, MCP tools) is blocked. `subagent` stays active but is forced
|
|
8
|
-
* read-only. The one permitted write is `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* read-only. The one permitted write is the `plan` tool's `write` action, which
|
|
9
|
+
* saves the plan in the agent directory. The model investigates, saves the plan,
|
|
10
|
+
* and calls `plan` action `exit` to read the file back and ask for approval.
|
|
11
11
|
*
|
|
12
|
-
* Toggle with `/plan
|
|
12
|
+
* Toggle with `/plan` (or Shift+Tab, which bytefly's editor binds), or start with
|
|
13
|
+
* `--plan`. The state is
|
|
13
14
|
* persisted as a custom session entry, so it follows the active branch.
|
|
14
15
|
*
|
|
15
16
|
* Load with: pi --extension ./extensions/plan
|
|
@@ -17,30 +18,27 @@
|
|
|
17
18
|
|
|
18
19
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
19
20
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import { Key } from "@earendil-works/pi-tui";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
|
-
import { hasPathInput } from "./path-guard.ts";
|
|
23
22
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
24
|
-
import { requestMount } from "../../lib/http/
|
|
23
|
+
import { requestMount } from "../../lib/http/mount.ts";
|
|
25
24
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
26
25
|
import { lastGoal, isPursuedGoal, type PublishedGoal } from "../../lib/goal-state.ts";
|
|
27
|
-
import { READ_ONLY_SUMMARY, PLAN_ENTER_REQUEST, type PlanEnterRequest } from "../../lib/plan-entry.ts";
|
|
28
|
-
import { isBoardRead } from "../../lib/policy.ts";
|
|
29
26
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} from "../../lib/
|
|
27
|
+
READ_ONLY_SUMMARY,
|
|
28
|
+
PLAN_ENTER_REQUEST,
|
|
29
|
+
PLAN_TOGGLE_REQUEST,
|
|
30
|
+
type PlanModeRequest,
|
|
31
|
+
} from "../../lib/plan-entry.ts";
|
|
32
|
+
import { isBoardRead } from "../../lib/policy.ts";
|
|
33
|
+
import { boardWriteRefusal } from "../../lib/board/actions.ts";
|
|
34
|
+
import { ASK_USER_QUESTION_TOOL, BOARD_TOOL, SUBAGENT_READ_ONLY_PARAM, SUBAGENT_TOOL } from "../../lib/tool-names.ts";
|
|
36
35
|
import { clip } from "../../lib/tui/columns.ts";
|
|
37
36
|
import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
38
37
|
import { planModeNotice, registerCommands } from "./commands.ts";
|
|
39
|
-
import { checkReadOnlyGit } from "./git-bash.ts";
|
|
40
38
|
import { createPlanPolicy, BASH_BLOCKED_GUIDANCE, BLOCKED_GUIDANCE, PLAN_SAFE_TOOLS } from "./policy.ts";
|
|
41
39
|
import { createPlanRuntime } from "./runtime.ts";
|
|
42
40
|
import { registerTools } from "./tools.ts";
|
|
43
|
-
import {
|
|
41
|
+
import { createLazyPlanWebReview } from "./web-lazy.ts";
|
|
44
42
|
|
|
45
43
|
/** Marker embedded in the injected prompt and used to filter stale context. */
|
|
46
44
|
export const PLAN_MODE_MARKER = "[PLAN MODE ACTIVE]";
|
|
@@ -63,7 +61,7 @@ export function planModeContext(goal?: PublishedGoal | null): string {
|
|
|
63
61
|
"",
|
|
64
62
|
`- Do not modify anything except the plan file: while planning, ${READ_ONLY_SUMMARY}.`,
|
|
65
63
|
"- bash is git-only: read-only git commands chain with && or ; — no pipes, redirection, or substitution, and no powershell. The cwd is already the project root, so skip cd, and read files with read, grep, find, and ls.",
|
|
66
|
-
"- Investigate and design a concrete plan, then save it with
|
|
64
|
+
"- Investigate and design a concrete plan, then save it with `plan` action `write` and call `plan` action `exit` with the returned plan_path for approval.",
|
|
67
65
|
"- Delegate broad exploration with subagent.",
|
|
68
66
|
`- To choose between approaches, ask with ${ASK_USER_QUESTION_TOOL} before finalizing.`,
|
|
69
67
|
];
|
|
@@ -94,7 +92,7 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
94
92
|
const runtime = createPlanRuntime(pi, policy, { entryPath: extensionEntryPath() });
|
|
95
93
|
/** The session this review serves as; its prefix lands on that session's one server. */
|
|
96
94
|
let sessionId = "";
|
|
97
|
-
const web =
|
|
95
|
+
const web = createLazyPlanWebReview(runtime.plans, {
|
|
98
96
|
mount: (spec) => requestMount(pi, spec),
|
|
99
97
|
session: () => sessionId,
|
|
100
98
|
});
|
|
@@ -108,33 +106,36 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
108
106
|
default: false,
|
|
109
107
|
});
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
ctx.ui.notify(planModeNotice(runtime.isEnabled()), "info");
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Another extension that offers plan mode (the goal workflow) asks on this
|
|
120
|
-
// channel instead of calling a tool it cannot reach from a command context. It
|
|
121
|
-
// owns the user's answer; enabling here must therefore not ask again.
|
|
109
|
+
// Another extension asks on these channels instead of calling a tool it cannot
|
|
110
|
+
// reach from a command or editor context: `goal` offers plan mode as a phase, and
|
|
111
|
+
// bytefly's editor binds Shift+Tab to the toggle. The asker owns the user's answer,
|
|
112
|
+
// so enabling here must not ask again, and a toggle answers with the state it left.
|
|
122
113
|
const unsubscribes: Array<() => void> = [];
|
|
123
|
-
const
|
|
114
|
+
const listenForRequests = (): void => {
|
|
124
115
|
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
125
116
|
unsubscribes.length = 0;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
117
|
+
const listen = (channel: string, handle: (request: PlanModeRequest) => void): void => {
|
|
118
|
+
unsubscribes.push(
|
|
119
|
+
pi.events.on(channel, (data) => {
|
|
120
|
+
const request = data as Partial<PlanModeRequest> | undefined;
|
|
121
|
+
if (!request?.ctx || typeof request.respond !== "function") return;
|
|
122
|
+
handle(request as PlanModeRequest);
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
listen(PLAN_ENTER_REQUEST, (request) => {
|
|
127
|
+
if (!runtime.isEnabled()) runtime.enable(request.ctx);
|
|
128
|
+
request.respond(true);
|
|
129
|
+
});
|
|
130
|
+
listen(PLAN_TOGGLE_REQUEST, (request) => {
|
|
131
|
+
runtime.toggle(request.ctx);
|
|
132
|
+
request.ctx.ui.notify(planModeNotice(runtime.isEnabled()), "info");
|
|
133
|
+
request.respond(runtime.isEnabled());
|
|
134
|
+
});
|
|
134
135
|
};
|
|
135
136
|
|
|
136
137
|
pi.on("session_start", (_event, ctx) => {
|
|
137
|
-
|
|
138
|
+
listenForRequests();
|
|
138
139
|
runtime.restore(ctx);
|
|
139
140
|
});
|
|
140
141
|
pi.on("session_tree", (_event, ctx) => runtime.restore(ctx));
|
|
@@ -161,12 +162,12 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
161
162
|
* proceed. Two layers: the policy (readers, `readOnlyHint`, deny list), then
|
|
162
163
|
* the path backstop (a hint on a path-carrying tool is only a claim).
|
|
163
164
|
*/
|
|
164
|
-
const blockedReason = (toolName: string, input: unknown): string | undefined => {
|
|
165
|
-
// Plan
|
|
166
|
-
// artifact, `
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
// another extension from borrowing the exemption.
|
|
165
|
+
const blockedReason = async (toolName: string, input: unknown): Promise<string | undefined> => {
|
|
166
|
+
// Plan's own tool is the sanctioned exception: it is the mode's vocabulary —
|
|
167
|
+
// `write` saves the artifact, `exit` presents it, `cancel` restores access, and
|
|
168
|
+
// `enter` answers "already" — so its actions decide for themselves whether they
|
|
169
|
+
// apply, and each refuses outside its mode. The source-path check keeps a
|
|
170
|
+
// same-named tool from another extension from borrowing the exemption.
|
|
170
171
|
if (runtime.isOwnTool(toolName)) return undefined;
|
|
171
172
|
// `subagent` stays active, but only as read-only delegation. The flag is
|
|
172
173
|
// forced in place (the child enforces it through `--tools`), so the model
|
|
@@ -183,12 +184,10 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
183
184
|
}
|
|
184
185
|
// `board` stays active so planning can read the board it is about to add cards
|
|
185
186
|
// to. The action is the truth here, not the annotation: the same tool writes, so
|
|
186
|
-
// `readOnlyHint` cannot say it, and every verb but the
|
|
187
|
+
// `readOnlyHint` cannot say it, and every verb but the reads are refused by name.
|
|
187
188
|
if (toolName === BOARD_TOOL) {
|
|
188
189
|
if (isBoardRead(input)) return undefined;
|
|
189
|
-
|
|
190
|
-
const verb = typeof action === "string" && action.trim() !== "" ? action.trim() : "board";
|
|
191
|
-
return `Plan mode: \`${verb}\` changes the board — only \`${BOARD_READ_ACTION}\` reads it. ${BLOCKED_GUIDANCE}`;
|
|
190
|
+
return boardWriteRefusal((input as { action?: unknown } | undefined)?.action, BLOCKED_GUIDANCE);
|
|
192
191
|
}
|
|
193
192
|
const blocked = policy.check(toolName);
|
|
194
193
|
if (blocked) return `Plan mode: ${blocked.reason}`;
|
|
@@ -201,9 +200,11 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
201
200
|
if (typeof command !== "string" || command.trim() === "") {
|
|
202
201
|
return `Plan mode: an empty bash command is not allowed. ${BASH_BLOCKED_GUIDANCE}`;
|
|
203
202
|
}
|
|
203
|
+
const { checkReadOnlyGit } = await import("./git-bash.ts");
|
|
204
204
|
const verdict = checkReadOnlyGit(command);
|
|
205
205
|
if (!verdict.ok) return `Plan mode: ${verdict.reason} ${BASH_BLOCKED_GUIDANCE}`;
|
|
206
206
|
}
|
|
207
|
+
const { hasPathInput } = await import("./path-guard.ts");
|
|
207
208
|
if (hasPathInput(input) && !PLAN_SAFE_TOOLS.includes(toolName)) {
|
|
208
209
|
return `Plan mode: "${toolName}" takes a file path and is not a known reader. ${BLOCKED_GUIDANCE}`;
|
|
209
210
|
}
|
|
@@ -214,9 +215,9 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
214
215
|
// above hides most of these; this is the second layer for tools already
|
|
215
216
|
// declared in an in-flight request, and the guard for tools we did not know
|
|
216
217
|
// about when the extension loaded (MCP servers, future extensions).
|
|
217
|
-
pi.on("tool_call", (event) => {
|
|
218
|
+
pi.on("tool_call", async (event) => {
|
|
218
219
|
if (!runtime.isEnabled()) return undefined;
|
|
219
|
-
const reason = blockedReason(event.toolName, event.input);
|
|
220
|
+
const reason = await blockedReason(event.toolName, event.input);
|
|
220
221
|
return reason ? { block: true, reason } : undefined;
|
|
221
222
|
});
|
|
222
223
|
|
|
@@ -90,7 +90,7 @@ function footprintText(summaries: readonly PlanSummary[], nowMs: number): string
|
|
|
90
90
|
export function formatPlanIndex(summaries: readonly PlanSummary[], dir: string, now: number = Date.now()): string {
|
|
91
91
|
const where = sanitizeLine(shortenPath(dir));
|
|
92
92
|
if (summaries.length === 0) {
|
|
93
|
-
return `No plans in ${where} yet;
|
|
93
|
+
return `No plans in ${where} yet; \`plan\` action \`write\` saves one there.`;
|
|
94
94
|
}
|
|
95
95
|
const lines = summaries.map(
|
|
96
96
|
(summary) =>
|
|
@@ -241,7 +241,9 @@ export class PlanIndexComponent implements Component {
|
|
|
241
241
|
|
|
242
242
|
const body: string[] = [];
|
|
243
243
|
if (this.summaries.length === 0) {
|
|
244
|
-
body.push(
|
|
244
|
+
body.push(
|
|
245
|
+
...emptyState(theme, w, { label: "No plans yet. `plan` action `write` saves one in the agent directory." }),
|
|
246
|
+
);
|
|
245
247
|
} else {
|
|
246
248
|
const { start, end } = this.cursor.window(this.summaries.length);
|
|
247
249
|
for (let index = start; index < end; index++) {
|
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[];
|