@gavin-hu/my-pi-agent 0.13.1 → 0.15.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 +66 -41
- package/README.zh-CN.md +53 -36
- package/extensions/bytefly/README.md +321 -186
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/config.ts +4 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/config.ts +3 -2
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +33 -10
- package/extensions/bytefly/header/config.ts +22 -0
- package/extensions/bytefly/header/register.ts +2 -1
- package/extensions/bytefly/index.ts +6 -6
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +7 -12
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +22 -2
- package/extensions/bytefly/panel/format.ts +17 -21
- package/extensions/bytefly/panel/settings-runtime.ts +86 -22
- package/extensions/bytefly/panel/settings.ts +279 -46
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +98 -13
- package/extensions/bytefly/panel/tui.ts +176 -166
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +17 -6
- package/extensions/bytefly/separator/config.ts +15 -0
- package/extensions/bytefly/separator/format.ts +60 -6
- package/extensions/bytefly/separator/register.ts +68 -9
- package/extensions/bytefly/separator/types.ts +8 -0
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/bytefly/working/config.ts +4 -5
- package/extensions/bytefly/working/format.ts +24 -7
- package/extensions/bytefly/working/register.ts +34 -3
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +3 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +106 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +118 -8
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +62 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +116 -1
- package/extensions/subagent/orchestrate.ts +16 -3
- package/extensions/subagent/render.ts +25 -0
- package/extensions/subagent/run.ts +113 -49
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/transcripts.ts +202 -0
- package/extensions/subagent/types.ts +23 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +16 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +3 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +83 -12
- package/lib/errors.ts +12 -0
- package/lib/format.ts +56 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +23 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/themes/bytefly-dark.json +2 -3
- package/themes/bytefly-light.json +2 -3
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
package/extensions/memory/tui.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type TuiMouseEventResult,
|
|
27
27
|
} from "@earendil-works/pi-tui";
|
|
28
28
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
29
|
+
import { ConfirmState } from "../../lib/tui/confirm.ts";
|
|
29
30
|
import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../lib/tui/cursor.ts";
|
|
30
31
|
import { dockListFrame, editorTheme, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
31
32
|
import { clip } from "../../lib/tui/columns.ts";
|
|
@@ -91,8 +92,8 @@ export class MemoryPickerComponent implements Component {
|
|
|
91
92
|
private mergePair: { first: Note; second: Note } | undefined;
|
|
92
93
|
/** The note marked with `m`, awaiting a second `m` to merge into it. */
|
|
93
94
|
private mergeSource: Note | undefined;
|
|
94
|
-
/**
|
|
95
|
-
private confirm
|
|
95
|
+
/** The y/N forget confirmation, shared with the other destructive screens. */
|
|
96
|
+
private readonly confirm = new ConfirmState();
|
|
96
97
|
/** Transient feedback shown in place of the hint. */
|
|
97
98
|
private notice: string | undefined;
|
|
98
99
|
private componentFocused = false;
|
|
@@ -355,17 +356,7 @@ export class MemoryPickerComponent implements Component {
|
|
|
355
356
|
}
|
|
356
357
|
|
|
357
358
|
private handleConfirm(data: string): void {
|
|
358
|
-
|
|
359
|
-
if (!note) return;
|
|
360
|
-
if (data === "y" || data === "Y" || matchesKey(data, Key.enter)) {
|
|
361
|
-
this.confirm = undefined;
|
|
362
|
-
this.forgetNote(note);
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
if (data === "n" || data === "N" || isCloseKey(data) || data === "q") {
|
|
366
|
-
this.confirm = undefined;
|
|
367
|
-
this.requestRender();
|
|
368
|
-
}
|
|
359
|
+
if (this.confirm.resolve(data) !== undefined) this.requestRender();
|
|
369
360
|
}
|
|
370
361
|
|
|
371
362
|
handleInput(data: string): void {
|
|
@@ -378,7 +369,7 @@ export class MemoryPickerComponent implements Component {
|
|
|
378
369
|
this.requestRender();
|
|
379
370
|
return;
|
|
380
371
|
}
|
|
381
|
-
if (this.confirm) {
|
|
372
|
+
if (this.confirm.pending) {
|
|
382
373
|
this.handleConfirm(data);
|
|
383
374
|
return;
|
|
384
375
|
}
|
|
@@ -417,14 +408,14 @@ export class MemoryPickerComponent implements Component {
|
|
|
417
408
|
else if (data === "p") this.moveNote(focused);
|
|
418
409
|
else if (data === "d") {
|
|
419
410
|
if (!focused) return;
|
|
420
|
-
this.confirm
|
|
411
|
+
this.confirm.ask(this.confirmPrompt(focused), () => this.forgetNote(focused));
|
|
421
412
|
this.requestRender();
|
|
422
413
|
} else if (data === "r") this.reload();
|
|
423
414
|
}
|
|
424
415
|
|
|
425
416
|
/** Wheel scrolling moves the selection, matching the other list screens. */
|
|
426
417
|
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
427
|
-
if (this.mode !== "list" || this.confirm) return undefined;
|
|
418
|
+
if (this.mode !== "list" || this.confirm.pending) return undefined;
|
|
428
419
|
const notes = this.notes();
|
|
429
420
|
const handled = scrollList(event, this.cursor, notes.length);
|
|
430
421
|
if (handled) this.selectedKey = notes[this.cursor.selected] ? noteKey(notes[this.cursor.selected]) : undefined;
|
|
@@ -472,9 +463,7 @@ export class MemoryPickerComponent implements Component {
|
|
|
472
463
|
});
|
|
473
464
|
}
|
|
474
465
|
|
|
475
|
-
private confirmPrompt(): string {
|
|
476
|
-
const note = this.confirm;
|
|
477
|
-
if (!note) return "";
|
|
466
|
+
private confirmPrompt(note: Note): string {
|
|
478
467
|
const label = sanitizeLine(note.text);
|
|
479
468
|
const clipped = label.length > 40 ? `${label.slice(0, 39)}…` : label;
|
|
480
469
|
return `Forget ${scopeLabel(note.scope)} note "${clipped}"? y/N`;
|
|
@@ -519,17 +508,18 @@ export class MemoryPickerComponent implements Component {
|
|
|
519
508
|
}
|
|
520
509
|
}
|
|
521
510
|
|
|
522
|
-
const
|
|
523
|
-
|
|
511
|
+
const pending = this.confirm.pending;
|
|
512
|
+
const hint = pending
|
|
513
|
+
? clip(` ${paint(this.theme, "warn", pending.prompt)}`, w)
|
|
524
514
|
: this.notice !== undefined
|
|
525
515
|
? clip(` ${paint(this.theme, "meta", this.notice)}`, w)
|
|
526
516
|
: this.mergeSource
|
|
527
517
|
? screenHint(this.theme, w, ["m merge into the focused note", "Esc cancel merge", "↑/↓"])
|
|
528
518
|
: screenHint(this.theme, w, [
|
|
529
519
|
"Esc close",
|
|
530
|
-
"Enter/e edit",
|
|
531
|
-
"a add",
|
|
532
|
-
"d forget",
|
|
520
|
+
{ text: "Enter/e edit", color: "accent" },
|
|
521
|
+
{ text: "a add", color: "accent" },
|
|
522
|
+
{ text: "d forget", color: "warning" },
|
|
533
523
|
"m merge",
|
|
534
524
|
"p move",
|
|
535
525
|
"s/Tab scope",
|
|
@@ -38,6 +38,12 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
|
|
|
38
38
|
and `ls` tools.
|
|
39
39
|
- **Model entry point.** The model can call `enter_plan_mode` to ask for plan
|
|
40
40
|
mode before a non-trivial task; the user confirms.
|
|
41
|
+
- **AGENTS.md bootstrap.** The packaged `init` skill (`/skill:init`) enters plan
|
|
42
|
+
mode, reconnoiters the repository, runs one built-in interview of at
|
|
43
|
+
most four questions, creates or updates `AGENTS.md`, gates the draft through a
|
|
44
|
+
quality checklist, and writes only after plan approval. It supports existing and
|
|
45
|
+
empty repositories, and falls back to evidence with recorded assumptions when
|
|
46
|
+
the interview is unavailable or cancelled; it never invents commands.
|
|
41
47
|
- **Plan files.** The model saves the plan with `write_plan`; the file lands in
|
|
42
48
|
`<repo-root>/.pi/plans/<YYYY-MM-DD-HHmm>-<slug>.md` (or `<agent-dir>/plans`
|
|
43
49
|
outside a repository). The plans directory is self-ignoring, so plan files
|
|
@@ -66,8 +72,9 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
|
|
|
66
72
|
or a `/tree` restore marks only what changed on its own branch.
|
|
67
73
|
- **Kept current.** After approval, `set_plan_steps` flips the checkboxes of
|
|
68
74
|
the steps you name in the file, so the plan mirrors what has actually been
|
|
69
|
-
done
|
|
70
|
-
|
|
75
|
+
done, and ticks the todo item approval seeded from that step with it, so the
|
|
76
|
+
widget and the plan do not drift apart. Pass the step's text as `expected` and
|
|
77
|
+
an update whose step number moved is refused instead of ticking the wrong line.
|
|
71
78
|
- **Reviewable plan.** `exit_plan_mode` reads the plan file back and presents
|
|
72
79
|
it for approval. The user approves, keeps planning, or asks for a refinement.
|
|
73
80
|
On a remote turn (WeChat) the plan file is sent first, then a digest of its
|
|
@@ -98,11 +105,16 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
|
|
|
98
105
|
against the 50-step cap. A plan without a `Steps` heading keeps the older rule —
|
|
99
106
|
the shallowest list in the document — so prose bullets in a plan that follows the
|
|
100
107
|
outline never become todos, and its verification bullets are already covered.
|
|
101
|
-
|
|
108
|
+
Each seeded item is matched back to its step by text, so ticking a step with
|
|
109
|
+
`set_plan_steps` completes the item too.
|
|
110
|
+
- **Goal-aware.** While planning, the injected context restates a pursued
|
|
102
111
|
session goal as one bullet, so the plan is designed against the objective
|
|
103
|
-
instead of beside it. On approval,
|
|
104
|
-
is
|
|
105
|
-
|
|
112
|
+
instead of beside it. On approval, a goal the goal workflow left as a draft
|
|
113
|
+
(`planning`) is finalized from the plan's own `## Goal` section — falling back
|
|
114
|
+
to the title — through the `goal` tool, with no extra dialog, because approving
|
|
115
|
+
the plan *is* the decision. A session with no goal at all gets one confirm to
|
|
116
|
+
record the same objective, and an `active` or `achieved` goal is never
|
|
117
|
+
rewritten: a final objective is the user's. No path spends a model turn.
|
|
106
118
|
- **Branch-aware.** The enabled flag and the review baseline are custom session
|
|
107
119
|
entries, so `/resume` and `/tree` restore the right mode and keep the
|
|
108
120
|
changed-section markers.
|
|
@@ -114,7 +126,7 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
|
|
|
114
126
|
| `enter_plan_mode` | normal mode | Ask to enter plan mode; empty schema, confirmed with `ctx.ui.confirm`. |
|
|
115
127
|
| `write_plan` | plan mode | Save the plan: `title` (file-name slug), `content` (full markdown in the documented outline), optional `plan_path` to overwrite a refinement. Returns the absolute path. A plan with more than 50 top-level steps in its `Steps` section is refused, with the cap and the fix named, and nothing is written. |
|
|
116
128
|
| `exit_plan_mode` | plan mode | Read the plan file (`plan_path`) and present it for approval. Refuses a file whose step list is over the cap, before the review screen. |
|
|
117
|
-
| `set_plan_steps` | normal mode | Keep an approved plan current: set the checkboxes of the given top-level steps (`updates`, each a 1-based `step` in the order `exit_plan_mode` listed, plus `status`, and
|
|
129
|
+
| `set_plan_steps` | normal mode | Keep an approved plan current: set the checkboxes of the given top-level steps (`updates`, each a 1-based `step` in the order `exit_plan_mode` listed, plus `status`, and `expected` — the step's text, so a reordered plan is refused) to `pending` or `completed` in one write. Only the checkboxes change, and a reopened step drops its `[DONE:n]` marker. The todo item approval seeded from a step is ticked with it: the file is written first and is authoritative, a renamed item is left alone, and a failed todo write is reported in the result. |
|
|
118
130
|
|
|
119
131
|
### Commands and flags
|
|
120
132
|
|
|
@@ -125,6 +137,7 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
|
|
|
125
137
|
| `/plan <prompt>` | the user, with the task | immediate |
|
|
126
138
|
| `/plan list` | the user, browsing the plans directory | immediate |
|
|
127
139
|
| `/plan review [path]` | the user, reviewing a plan in the browser | immediate |
|
|
140
|
+
| `/skill:init` | the user, creating or updating `AGENTS.md` | enters plan mode; write waits for plan approval |
|
|
128
141
|
| `Ctrl+Alt+P` | the user | immediate |
|
|
129
142
|
| `--plan` | the user, at launch | immediate |
|
|
130
143
|
|
|
@@ -139,8 +152,8 @@ already runs read-only:
|
|
|
139
152
|
`/plan <prompt>` never disables plan mode; only the argument-less `/plan` (or
|
|
140
153
|
`Ctrl+Alt+P`) toggles. Because slash commands go through Pi directly, `/plan` is
|
|
141
154
|
the toggle that works in every terminal (for example Zed's integrated terminal,
|
|
142
|
-
which does not forward `Ctrl+Alt+P`). The footer shows
|
|
143
|
-
is on, and
|
|
155
|
+
which does not forward `Ctrl+Alt+P`). The footer shows `≡ plan` while plan mode
|
|
156
|
+
is on, and `≡ plan · <plan-file>` once the model has written a plan.
|
|
144
157
|
|
|
145
158
|
`/plan review` is the one reserved subcommand. Bare `/plan review` reviews the
|
|
146
159
|
plan this session wrote, or, in a later session, the newest plan file in the
|
|
@@ -149,6 +162,45 @@ with a separator) reviews that plan. A line that merely starts with the word —
|
|
|
149
162
|
`review the auth flow` — is still treated as a planning task, so the subcommand
|
|
150
163
|
never swallows one.
|
|
151
164
|
|
|
165
|
+
### `/skill:init`
|
|
166
|
+
|
|
167
|
+
`/skill:init` loads the packaged `init` skill. The skill enters plan mode itself
|
|
168
|
+
when it is not active, then decides what to do; nothing is written before
|
|
169
|
+
approval. Its workflow is:
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
/skill:init
|
|
173
|
+
└─> plan mode (read-only)
|
|
174
|
+
└─> recon: manifests, docs, CI, tests, existing instructions
|
|
175
|
+
└─> interview: built-in bank, one call of at most 4 questions
|
|
176
|
+
├─ answered ───────────────> reconcile with evidence
|
|
177
|
+
└─ unavailable/cancelled ──> evidence + assumptions
|
|
178
|
+
└─> draft: new file, exact edits, or an honest no-op
|
|
179
|
+
└─> quality gate
|
|
180
|
+
└─> write_plan ──> exit_plan_mode
|
|
181
|
+
├─ not approved ──> refine the same plan
|
|
182
|
+
└─ approved ──────> write/edit AGENTS.md
|
|
183
|
+
└─ read back + quality checklist
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The interview comes from a built-in bank in
|
|
187
|
+
[`skills/init/references/interview-questions.md`](../../skills/init/references/interview-questions.md):
|
|
188
|
+
core questions cover purpose, verification, non-obvious rules, isolation,
|
|
189
|
+
completion, sensitive paths, and dependency or network limits, with state-specific
|
|
190
|
+
entries for an existing file, an empty repository, and related instruction files.
|
|
191
|
+
The first call uses a deterministic four-question set for the repository state;
|
|
192
|
+
a second call is allowed only for a material ambiguity. If the tool is inactive,
|
|
193
|
+
there is no UI, or the user cancels, the skill records the gaps as assumptions
|
|
194
|
+
and continues from repository evidence.
|
|
195
|
+
|
|
196
|
+
The quality reference is
|
|
197
|
+
[`skills/init/references/agents-md-quality.md`](../../skills/init/references/agents-md-quality.md).
|
|
198
|
+
An existing `AGENTS.md` is reviewed and updated only after approval, an accurate
|
|
199
|
+
file can produce a no-op plan, and an empty repository gets only known facts.
|
|
200
|
+
The skill needs the plan extension for its review gate. It also ships with the
|
|
201
|
+
package, so load the whole package (`pi -e .`) or install it; loading only
|
|
202
|
+
`./extensions/plan` leaves `/skill:init` unavailable.
|
|
203
|
+
|
|
152
204
|
## Behaviour by mode
|
|
153
205
|
|
|
154
206
|
The review screen is TUI-only. `exit_plan_mode` opens `PlanViewComponent`: a
|
|
@@ -311,6 +363,18 @@ agent file that overrides it.
|
|
|
311
363
|
- `readOnly` is also a normal `subagent` parameter; outside plan mode a caller
|
|
312
364
|
may set it to run any agent read-only on purpose.
|
|
313
365
|
|
|
366
|
+
## Review screen
|
|
367
|
+
|
|
368
|
+
<img src="../../assets/plan.png" alt="The terminal plan review: a Plan Review title rule, a notice that one section changed since the last review, the plan body with its headings and ◼/◻ task checkboxes, a scroll gauge naming the current section and the visible line range, and the Esc/a/r/w, scroll, and section-jump hints" width="100%">
|
|
369
|
+
|
|
370
|
+
`exit_plan_mode` opens this screen on the plan file it just wrote. The body is
|
|
371
|
+
Pi's Markdown renderer with the active theme, so headings, lists, code, and
|
|
372
|
+
links read as they do in the transcript; the checkboxes in `Steps` use the
|
|
373
|
+
shared `◼`/`◻` vocabulary. A plan taller than the terminal scrolls under a gauge
|
|
374
|
+
that names the section at the top of the viewport and the line range it shows,
|
|
375
|
+
and the footer lists only the keys that apply. `Esc` keeps planning, `a` approves
|
|
376
|
+
and runs, `r` refines inline, and `w` opens the browser review below.
|
|
377
|
+
|
|
314
378
|
## Browser review
|
|
315
379
|
|
|
316
380
|
<img src="../../assets/plan-review.png" alt="The browser-based plan review: a sticky header with the plan title, a status chip, and the Approve & run, Save feedback, and Back to Pi actions, a side panel with step progress and a section outline, and the plan as one measured column with a comment rail and an inline review thread" width="100%">
|
|
@@ -414,6 +478,9 @@ file and returns them to the model.
|
|
|
414
478
|
dropped. A step longer than 200 characters is shortened in the todo list and
|
|
415
479
|
the approval listing — a display rule only, because the plan file always keeps
|
|
416
480
|
the full text.
|
|
481
|
+
- The todo item for a step is matched by its text. An item renamed after
|
|
482
|
+
approval no longer matches, so `set_plan_steps` leaves it alone and says so in
|
|
483
|
+
its result; the plan file is still updated.
|
|
417
484
|
- Browser review needs a local browser: the server is loopback-only, so a remote
|
|
418
485
|
turn cannot drive it.
|
|
419
486
|
- A browser that never connects leaves the review waiting until the user
|
|
@@ -435,13 +502,23 @@ file and returns them to the model.
|
|
|
435
502
|
| Integration point | How |
|
|
436
503
|
|---|---|
|
|
437
504
|
| Tools | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps` registered with `executionMode: "sequential"`. `enter_plan_mode` is active normally with `readOnlyHint: true`; `write_plan` (`readOnlyHint: false`) and `exit_plan_mode` (`readOnlyHint: true`) are `defaultActive: false`. `set_plan_steps` is active normally (`readOnlyHint: false`, `idempotentHint: true`); the read-only policy default-denies it in plan mode. Each returns typed `details` (`EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `SetPlanStepsDetails`), and `ExitPlanModeDetails` carries the review's `annotations` when a browser approval or save supplied comments; no `outputSchema`/`structuredContent`. |
|
|
438
|
-
| Commands and flags | `/plan` (toggle, or enter and send a task), `/plan list` (plans index), `/plan review [path]` (browser review), `--plan` launch flag, `Ctrl+Alt+P` shortcut. |
|
|
439
|
-
| State | Enabled flag and the tools it hid persisted with `pi.appendEntry("plan-mode", { enabled, removed })`, and each review's baseline persisted with `pi.appendEntry("plan-baseline", { path, sections })` as one digest per heading section; both rebuilt from `ctx.sessionManager.getBranch()`, so `/resume` and `/tree` restore the mode and the changed markers of the branch they land on. Plan
|
|
440
|
-
|
|
|
505
|
+
| Commands and flags | `/plan` (toggle, or enter and send a task), `/plan list` (plans index), `/plan review [path]` (browser review), `/skill:init` (skill-backed `AGENTS.md` bootstrap), `--plan` launch flag, `Ctrl+Alt+P` shortcut. |
|
|
506
|
+
| State | Enabled flag and the tools it hid persisted with `pi.appendEntry("plan-mode", { enabled, removed })`, and each review's baseline persisted with `pi.appendEntry("plan-baseline", { path, sections })` as one digest per heading section; both rebuilt from `ctx.sessionManager.getBranch()`, so `/resume` and `/tree` restore the mode and the changed markers of the branch they land on. Plan writes a goal only at approval, through `ctx.executeTool("goal", { objective })`: a `planning` goal is promoted to the plan's `## Goal`, a session with none is offered one, and an `active`/`achieved` goal is left alone. A review that shows the same content appends nothing. The last plan path lives in runtime memory for the footer chip, and a bare `/plan review` falls back to the newest plan file in the directory. |
|
|
507
|
+
| Entry requests | `pi.events` channel `PLAN_ENTER_REQUEST` from `lib/plan-entry.ts`: the `goal` workflow asks, with the shared consent sentence, and plan enables the mode on the caller's context without a second dialog. Subscribed on `session_start`, released on `session_shutdown`. |
|
|
508
|
+
| Hooks | `tool_call` blocks non-read-only calls and forces `subagent` read-only; `before_agent_start` injects the hidden `[PLAN MODE ACTIVE]` context, with a pursued session goal read from the branch through `lib/goal-state.ts`; `context` keeps only the newest plan context while enabled and drops stale ones otherwise. |
|
|
441
509
|
| Lifecycle | `session_start`/`session_tree` restore state; `session_shutdown` clears the footer status and closes any browser review server. |
|
|
442
510
|
|
|
443
511
|
## Design notes
|
|
444
512
|
|
|
513
|
+
- **Consent is one dialog, whichever extension asks.** The goal workflow offers
|
|
514
|
+
plan mode as a phase of the goal, but a command context cannot call
|
|
515
|
+
`enter_plan_mode` (`ExtensionCommandContext` has no `executeTool`). So the
|
|
516
|
+
consent sentence left `policy.ts` for `lib/plan-entry.ts`, and the entry itself
|
|
517
|
+
is a `pi.events` request: the asker shows the question, plan enables silently
|
|
518
|
+
with the asker's context. `enter_plan_mode` keeps its own confirm, using the
|
|
519
|
+
same sentence, so the model-initiated path is unchanged and the two texts
|
|
520
|
+
cannot drift. A request that nobody answers resolves `false` rather than
|
|
521
|
+
hanging, so `goal` loaded alone still works.
|
|
445
522
|
- **One review loop owns the human gate.** `review.ts` asks however the session
|
|
446
523
|
is attached — TUI screen, dialog plus editor, remote file plus digest, or
|
|
447
524
|
browser — and returns one `ReviewDecision`, so `exit_plan_mode` and
|
|
@@ -476,6 +553,13 @@ file and returns them to the model.
|
|
|
476
553
|
channel; no import from that extension) and the steps are merged into them.
|
|
477
554
|
A rejected seed reports the nested tool's own reason in the approval message
|
|
478
555
|
instead of failing silently.
|
|
556
|
+
- **Ticking a step ticks its item, through the same channel.** The seed's text is
|
|
557
|
+
the step's text (`PlanStep.content`, already elided for a long step), so the
|
|
558
|
+
mirror matches on that text and changes only `status`: an item the user
|
|
559
|
+
renamed is left alone and reported as unmatched rather than overwritten, and a
|
|
560
|
+
repeated call writes nothing. The file is written first and stays
|
|
561
|
+
authoritative, so a failed nested write is reported in the result text — once
|
|
562
|
+
one artifact has changed there is no previous state to hand back.
|
|
479
563
|
- **A cap is enforced where it can still be fixed.** Both refusal points —
|
|
480
564
|
`write_plan` and `exit_plan_mode` — sit before the write and before the review,
|
|
481
565
|
because those are the moments the model can still regroup the steps. A note on
|
|
@@ -504,7 +588,7 @@ file and returns them to the model.
|
|
|
504
588
|
(`isContainedPlan`), in a single all-or-nothing write — refusing every entry
|
|
505
589
|
if any step is out of range or an entry's `expected` text no longer matches —
|
|
506
590
|
and refuses while plan mode is on, so the read-only guarantee and the path
|
|
507
|
-
backstop are unchanged.
|
|
591
|
+
backstop are unchanged. A required `expected` is what makes an ordinal safe:
|
|
508
592
|
a plan edited between approval and ticking is caught instead of silently
|
|
509
593
|
marking the step that moved into that number.
|
|
510
594
|
- **Direction: enforce capabilities at the tool boundary.** (1) A shared
|
|
@@ -543,24 +627,28 @@ file and returns them to the model.
|
|
|
543
627
|
| `git-bash.ts` | `checkReadOnlyGit`: the pure allowlist that restricts `bash` to read-only git commands. |
|
|
544
628
|
| `plans.ts` | Plan-file store: slug/stamp naming, directory resolution, containment, atomic `write`/`setSteps`/`saveFeedback`, and `read`/`latest`. |
|
|
545
629
|
| `todos.ts` | Approval-time todo merge (pure): the last list replayed from the branch, and the merge of the plan's steps into it with the cap respected. |
|
|
546
|
-
| `steps.ts` | Plan-document structure (pure): the outline, step extraction and the `Steps`-section scope, heading sections, and the checkbox edit. |
|
|
630
|
+
| `steps.ts` | Plan-document structure (pure): the outline, step extraction and the `Steps`-section scope, heading sections, the plan's own goal (`goalSection`), and the checkbox edit. |
|
|
631
|
+
| `format.ts` | Pure result/approval text: the plan preview, the step-cap diagnosis, step labels, the merge summary, and the verification note. |
|
|
547
632
|
| `plan-text.ts` | The plan text a review shows and compares: `sanitizePlanText`, the one normalization both sides use, the per-section `sectionDigests`, and `changedSections`. |
|
|
548
633
|
| `runtime.ts` | Enabled state, tool gating, persistence (mode and review baseline), control-tool identity, footer status. |
|
|
549
|
-
| `tools.ts` |
|
|
634
|
+
| `tools.ts` | Registration entry: composes the four tool registrations in the order the session sees them. |
|
|
635
|
+
| `tools/` | One module per tool (`enter.ts`, `write.ts`, `exit.ts`, `steps.ts`), plus `todo-seed.ts` (the two nested `todo` writes) and `nested.ts` (reading a nested result). |
|
|
550
636
|
| `review.ts` | The review gate: one decision from the TUI screen, a dialog, or the browser, the shared browser lifecycle, and the "mark what was shown" baseline rule. |
|
|
551
637
|
| `commands.ts` | `/plan` (enter, optional task) and the `/plan review [path]` browser subcommand. |
|
|
552
638
|
| `tui.ts` | `PlanViewComponent` (sectioned Markdown body, changed-section summary and marker, section jump, scroll gauge, approve/refine/browser/keep). |
|
|
553
639
|
| `plan-index.ts` | `/plan list`: the notice text and the docked index screen (title, step progress, age, focused path; Enter reviews). |
|
|
554
640
|
| `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, tables, rules, blanks). |
|
|
555
641
|
| `feedback.ts` | Browser-comment codec: `stripFeedback`, `parseFeedback`, `formatFeedback`, `feedbackSummary`, and payload validation. |
|
|
556
|
-
| `web.ts` | `createPlanWebReview`: the
|
|
557
|
-
| `web-router.ts` | Pure request handler: token/`Host`/method checks and the review routes. |
|
|
642
|
+
| `web.ts` | `createPlanWebReview`: the review session (SSE close detection, the body cap, settle-after-flush, abort) over the shared loopback bind, writer, and close. |
|
|
643
|
+
| `web-router.ts` | Pure request handler: token/`Host`/method checks and the review routes, over the shared page and JSON header sets. |
|
|
558
644
|
| `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet), its data island, and inline CSS. |
|
|
559
645
|
| `web-client.ts` | The `/client.js` source (block/thread rendering, the comment rail, outline and facts, inline comments, selection quotes, tables, highlighted code, keyboard nav, save/approve/back). |
|
|
560
646
|
| `path-guard.ts` | Recursive path-argument detection for the read-only backstop. |
|
|
561
647
|
| [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform default-browser opener (shared with `file-browse`). |
|
|
648
|
+
| [`../../lib/plan-entry.ts`](../../lib/plan-entry.ts) | Plan-mode entry shared with `goal`: the read-only summary, the consent sentence, and the `pi.events` request `plan` answers by enabling the mode. |
|
|
562
649
|
| [`../../lib/shell.ts`](../../lib/shell.ts) | The shell-escape rule the read-only git guard shares with the worktree isolation guard. |
|
|
563
650
|
| [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer (shared with `file-browse`). |
|
|
564
|
-
| [`../../lib/web/index.ts`](../../lib/web/index.ts) | Shared Bytefly web brand system: tokens, base rules, `tok-*` colors, layout, the bootstrap, the toggle wiring, and shared markup. |
|
|
651
|
+
| [`../../lib/web/index.ts`](../../lib/web/index.ts) | Shared Bytefly web brand system: tokens, base rules, `tok-*` colors, layout, the bootstrap, the toggle wiring, the shortcut sheet (`keysSheet`, `keysDialogScript`), and shared markup. |
|
|
652
|
+
| [`../../lib/http/index.ts`](../../lib/http/index.ts) | Loopback HTTP layer shared with `file-browse`: the page policy and header sets, `ServeResponse`, the request-envelope helpers, bind/respond/close. |
|
|
565
653
|
| `../../lib/policy.ts` | Shared read-only capability policy (default-deny + `readOnlyHint`). |
|
|
566
654
|
| `../../lib/tui/chrome.ts` | Shared screen header, viewport-row helper, and styled key-hint footer. |
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { isRemoteTurn } from "../../lib/interaction.ts";
|
|
13
|
+
import { READ_ONLY_SUMMARY } from "../../lib/plan-entry.ts";
|
|
13
14
|
import { feedbackSummary } from "./feedback.ts";
|
|
14
15
|
import { PlanIndexComponent, formatPlanIndex } from "./plan-index.ts";
|
|
15
|
-
import { READ_ONLY_SUMMARY } from "./policy.ts";
|
|
16
16
|
import { runBrowserReview } from "./review.ts";
|
|
17
17
|
import type { PlanRuntime } from "./runtime.ts";
|
|
18
18
|
import type { PlanSummary } from "./plans.ts";
|
|
@@ -28,6 +28,13 @@ export function planModeNotice(enabled: boolean): string {
|
|
|
28
28
|
return enabled ? ENABLED_NOTICE : DISABLED_NOTICE;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Enter plan mode when needed, with the same notice `/plan <task>` shows. */
|
|
32
|
+
function ensurePlanMode(runtime: PlanRuntime, ctx: ExtensionContext): void {
|
|
33
|
+
if (runtime.isEnabled()) return;
|
|
34
|
+
runtime.enable(ctx);
|
|
35
|
+
ctx.ui.notify(ENABLED_NOTICE, "info");
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
/** The plan path a `/plan review` line names, or undefined to review the newest plan. */
|
|
32
39
|
function reviewTarget(args: string): string | undefined {
|
|
33
40
|
const target = args.slice(REVIEW_PREFIX.length).trim();
|
|
@@ -189,10 +196,7 @@ export function registerCommands(
|
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
// A task always means "plan this": enter if needed, then send.
|
|
192
|
-
|
|
193
|
-
runtime.enable(ctx);
|
|
194
|
-
ctx.ui.notify(ENABLED_NOTICE, "info");
|
|
195
|
-
}
|
|
199
|
+
ensurePlanMode(runtime, ctx);
|
|
196
200
|
if (ctx.isIdle()) await pi.sendUserMessage(prompt);
|
|
197
201
|
else await pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
198
202
|
},
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure text for the plan tools' results and approval message.
|
|
3
|
+
*
|
|
4
|
+
* No host calls and no theme: the registrations in `tools/` use these to keep
|
|
5
|
+
* the cap diagnosis, the preview, and the merge summary in one place.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { SET_PLAN_STEPS_TOOL } from "./runtime.ts";
|
|
9
|
+
import { MAX_STEP_LENGTH, MAX_STEPS, type PlanStep, type StepUpdate } from "./steps.ts";
|
|
10
|
+
import type { TodoMerge } from "./todos.ts";
|
|
11
|
+
|
|
12
|
+
/** Lines of plan shown in an unexpanded result. */
|
|
13
|
+
const PREVIEW_LINES = 8;
|
|
14
|
+
|
|
15
|
+
/** The collapsed view of a plan body and how many lines it hid. */
|
|
16
|
+
export function preview(plan: string): { body: string; more: number } {
|
|
17
|
+
const lines = plan.split("\n");
|
|
18
|
+
if (lines.length <= PREVIEW_LINES) return { body: plan, more: 0 };
|
|
19
|
+
return { body: lines.slice(0, PREVIEW_LINES).join("\n"), more: lines.length - PREVIEW_LINES };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One sentence when a step is longer than the display cap. The text stays in the
|
|
24
|
+
* plan file either way; only the todo list and the approval listing shorten it.
|
|
25
|
+
*/
|
|
26
|
+
export function stepElisionNote(elided: number): string {
|
|
27
|
+
if (elided === 0) return "";
|
|
28
|
+
const plural = elided === 1 ? " is" : "s are";
|
|
29
|
+
return ` Note: ${elided} step${plural} longer than ${MAX_STEP_LENGTH} characters and will be shortened in the todo list.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The diagnosis both cap refusals share: a step list longer than approval can
|
|
34
|
+
* track. Each caller appends the fix that is available from where it stands.
|
|
35
|
+
*/
|
|
36
|
+
export function overCap(total: number): string {
|
|
37
|
+
return `The plan has ${total} top-level steps but approval tracks at most ${MAX_STEPS}.`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** "Step 2" or "Steps 1, 2" for a set of step numbers. */
|
|
41
|
+
export function stepLabel(steps: number[]): string {
|
|
42
|
+
return `${steps.length === 1 ? "Step" : "Steps"} ${steps.join(", ")}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** "Steps 1, 2 completed; step 3 reopened" for the tool's text result. */
|
|
46
|
+
export function describeUpdates(updates: StepUpdate[]): string {
|
|
47
|
+
const completed = updates.filter((update) => update.status === "completed").map((update) => update.step);
|
|
48
|
+
const pending = updates.filter((update) => update.status === "pending").map((update) => update.step);
|
|
49
|
+
const parts: string[] = [];
|
|
50
|
+
if (completed.length > 0) parts.push(`${stepLabel(completed)} completed`);
|
|
51
|
+
if (pending.length > 0) parts.push(`${stepLabel(pending)} reopened`);
|
|
52
|
+
return parts.join("; ");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* One sentence about the plan's verification item, or "" when the plan has none
|
|
57
|
+
* (or it was already in the list). The item is a todo, not a plan step, so it is
|
|
58
|
+
* ticked in the list rather than in the file.
|
|
59
|
+
*/
|
|
60
|
+
export function verificationNote(merged: TodoMerge | undefined): string {
|
|
61
|
+
if (merged?.trailing === "added") {
|
|
62
|
+
return "\nThe plan's verification section is the last todo item; tick it in the todo list, not in the file.";
|
|
63
|
+
}
|
|
64
|
+
if (merged?.trailing === "deferred") {
|
|
65
|
+
return "\nThe plan's verification section did not fit in the full todo list; keep it in mind.";
|
|
66
|
+
}
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** What the merge did, for the approval message; `steps` is the plan's step count. */
|
|
71
|
+
export function mergeSummary(merged: TodoMerge, steps: PlanStep[]): string {
|
|
72
|
+
const total = steps.length;
|
|
73
|
+
const counted = `${merged.added + merged.skipped} of ${total} plan step${total === 1 ? "" : "s"}`;
|
|
74
|
+
const notes = [
|
|
75
|
+
merged.skipped > 0 ? `${merged.skipped} already present` : "",
|
|
76
|
+
merged.kept > 0 ? `${merged.kept} existing kept` : "",
|
|
77
|
+
merged.deferred > 0 ? `${merged.deferred} deferred, list full` : "",
|
|
78
|
+
].filter(Boolean);
|
|
79
|
+
const suffix = notes.length > 0 ? ` (${notes.join(", ")})` : "";
|
|
80
|
+
const tick = `Tick each one off in the file with ${SET_PLAN_STEPS_TOOL} as you finish it, passing the step's text as \`expected\`.`;
|
|
81
|
+
if (merged.added === 0 && merged.skipped === 0) {
|
|
82
|
+
return ` The todo list is full, so no plan steps were added${suffix}. Keep these steps in mind. ${tick}`;
|
|
83
|
+
}
|
|
84
|
+
if (merged.added === 0)
|
|
85
|
+
return ` ${total === 1 ? "The plan's step is" : "Every plan step is"} already in the todo list${suffix}. ${tick}`;
|
|
86
|
+
return ` Merged ${counted} into the todo list${suffix}. ${tick}`;
|
|
87
|
+
}
|
package/extensions/plan/index.ts
CHANGED
|
@@ -22,19 +22,14 @@ import { fileURLToPath } from "node:url";
|
|
|
22
22
|
import { hasPathInput } from "./path-guard.ts";
|
|
23
23
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
24
24
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
25
|
-
import { lastGoal, type PublishedGoal } from "../../lib/goal-state.ts";
|
|
25
|
+
import { lastGoal, isPursuedGoal, type PublishedGoal } from "../../lib/goal-state.ts";
|
|
26
|
+
import { READ_ONLY_SUMMARY, PLAN_ENTER_REQUEST, type PlanEnterRequest } from "../../lib/plan-entry.ts";
|
|
26
27
|
import { ASK_USER_QUESTION_TOOL, SUBAGENT_READ_ONLY_PARAM, SUBAGENT_TOOL } from "../../lib/tool-names.ts";
|
|
27
28
|
import { clip } from "../../lib/tui/columns.ts";
|
|
28
29
|
import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
29
30
|
import { planModeNotice, registerCommands } from "./commands.ts";
|
|
30
31
|
import { checkReadOnlyGit } from "./git-bash.ts";
|
|
31
|
-
import {
|
|
32
|
-
createPlanPolicy,
|
|
33
|
-
BASH_BLOCKED_GUIDANCE,
|
|
34
|
-
BLOCKED_GUIDANCE,
|
|
35
|
-
PLAN_SAFE_TOOLS,
|
|
36
|
-
READ_ONLY_SUMMARY,
|
|
37
|
-
} from "./policy.ts";
|
|
32
|
+
import { createPlanPolicy, BASH_BLOCKED_GUIDANCE, BLOCKED_GUIDANCE, PLAN_SAFE_TOOLS } from "./policy.ts";
|
|
38
33
|
import { createPlanRuntime } from "./runtime.ts";
|
|
39
34
|
import { registerTools } from "./tools.ts";
|
|
40
35
|
import { createPlanWebReview } from "./web.ts";
|
|
@@ -51,21 +46,20 @@ const GOAL_CONTEXT_CHARS = 64;
|
|
|
51
46
|
|
|
52
47
|
/**
|
|
53
48
|
* Injected each turn while planning: the facts and the next action, not the tool
|
|
54
|
-
* contracts.
|
|
55
|
-
* designed against the objective instead of beside it.
|
|
49
|
+
* contracts. A pursued session goal — being planned or already final — becomes one
|
|
50
|
+
* extra bullet, so a plan is designed against the objective instead of beside it.
|
|
56
51
|
*/
|
|
57
52
|
export function planModeContext(goal?: PublishedGoal | null): string {
|
|
58
53
|
const lines = [
|
|
59
54
|
PLAN_MODE_MARKER,
|
|
60
|
-
"You are in plan mode: read-only exploration for safe code analysis.",
|
|
61
55
|
"",
|
|
62
56
|
`- Do not modify anything except the plan file: while planning, ${READ_ONLY_SUMMARY}.`,
|
|
63
|
-
"- bash is git-only: read-only
|
|
57
|
+
"- 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.",
|
|
64
58
|
"- Investigate and design a concrete plan, then save it with write_plan and call exit_plan_mode with the returned plan_path for approval.",
|
|
65
|
-
"- Delegate broad exploration with subagent
|
|
59
|
+
"- Delegate broad exploration with subagent.",
|
|
66
60
|
`- To choose between approaches, ask with ${ASK_USER_QUESTION_TOOL} before finalizing.`,
|
|
67
61
|
];
|
|
68
|
-
if (goal
|
|
62
|
+
if (goal && isPursuedGoal(goal)) {
|
|
69
63
|
lines.push(`- Current session goal: ${clip(sanitizeLine(goal.objective), GOAL_CONTEXT_CHARS)}`);
|
|
70
64
|
}
|
|
71
65
|
return lines.join("\n");
|
|
@@ -109,10 +103,32 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
109
103
|
},
|
|
110
104
|
});
|
|
111
105
|
|
|
112
|
-
|
|
106
|
+
// Another extension that offers plan mode (the goal workflow) asks on this
|
|
107
|
+
// channel instead of calling a tool it cannot reach from a command context. It
|
|
108
|
+
// owns the user's answer; enabling here must therefore not ask again.
|
|
109
|
+
const unsubscribes: Array<() => void> = [];
|
|
110
|
+
const listenForEntryRequests = (): void => {
|
|
111
|
+
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
112
|
+
unsubscribes.length = 0;
|
|
113
|
+
unsubscribes.push(
|
|
114
|
+
pi.events.on(PLAN_ENTER_REQUEST, (data) => {
|
|
115
|
+
const request = data as Partial<PlanEnterRequest> | undefined;
|
|
116
|
+
if (!request?.ctx || typeof request.respond !== "function") return;
|
|
117
|
+
if (!runtime.isEnabled()) runtime.enable(request.ctx);
|
|
118
|
+
request.respond(true);
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
pi.on("session_start", (_event, ctx) => {
|
|
124
|
+
listenForEntryRequests();
|
|
125
|
+
runtime.restore(ctx);
|
|
126
|
+
});
|
|
113
127
|
pi.on("session_tree", (_event, ctx) => runtime.restore(ctx));
|
|
114
128
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
115
129
|
await web.closeAll();
|
|
130
|
+
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
131
|
+
unsubscribes.length = 0;
|
|
116
132
|
ctx.ui.setStatus(STATUS_KEYS.planMode, undefined);
|
|
117
133
|
});
|
|
118
134
|
|
package/extensions/plan/plans.ts
CHANGED
|
@@ -292,10 +292,9 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
292
292
|
const range = total === 0 ? "it has no steps" : `valid steps are 1 to ${total}`;
|
|
293
293
|
throw new Error(`Step ${outOfRange.step} is out of range; ${range}.`);
|
|
294
294
|
}
|
|
295
|
-
//
|
|
296
|
-
//
|
|
295
|
+
// The expected text is the guard against a reordered plan: refuse the whole
|
|
296
|
+
// batch rather than tick the step that moved into that number.
|
|
297
297
|
const stale = updates.find((update) => {
|
|
298
|
-
if (update.expected === undefined) return false;
|
|
299
298
|
const step = steps[update.step - 1];
|
|
300
299
|
return step === undefined || !matchesStepText(step.content, update.expected);
|
|
301
300
|
});
|
|
@@ -46,12 +46,6 @@ export const PLAN_ALLOWED_TOOLS = [...PLAN_SAFE_TOOLS, SUBAGENT_TOOL];
|
|
|
46
46
|
/** Tools always blocked while planning, even when annotated read-only. */
|
|
47
47
|
const ALWAYS_BLOCKED_TOOLS = ["write", "edit", ENTER_TOOL];
|
|
48
48
|
|
|
49
|
-
/** One-clause description of the read-only guarantee, reused across prompts. */
|
|
50
|
-
export const READ_ONLY_SUMMARY =
|
|
51
|
-
"write and edit are disabled, bash is limited to read-only git commands, and other mutating tools are blocked; " +
|
|
52
|
-
"subagent delegation is forced read-only, and the only file write is write_plan (under .pi/plans, or the agent " +
|
|
53
|
-
"directory outside a repository)";
|
|
54
|
-
|
|
55
49
|
/** Shown when a tool call is refused while planning. */
|
|
56
50
|
export const BLOCKED_GUIDANCE = "Call exit_plan_mode and get approval before using it.";
|
|
57
51
|
|
|
@@ -21,7 +21,9 @@ import { realpathSync } from "node:fs";
|
|
|
21
21
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
22
22
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
23
23
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
24
|
+
import { PLAN_MODE_ENTRY_TYPE } from "../../lib/plan-state.ts";
|
|
24
25
|
import type { ReadOnlyPolicy } from "../../lib/policy.ts";
|
|
26
|
+
import { PLAN_ENTER_TOOL } from "../../lib/tool-names.ts";
|
|
25
27
|
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
26
28
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
27
29
|
import { createPlanStore, type PlanStore } from "./plans.ts";
|
|
@@ -31,13 +33,13 @@ import type { PlanModeEntry, ReviewBaselineEntry } from "./types.ts";
|
|
|
31
33
|
/** Tool the model calls to leave plan mode after approval. */
|
|
32
34
|
export const EXIT_TOOL = "exit_plan_mode";
|
|
33
35
|
/** Tool the model calls to ask to enter plan mode. */
|
|
34
|
-
export const ENTER_TOOL =
|
|
36
|
+
export const ENTER_TOOL = PLAN_ENTER_TOOL;
|
|
35
37
|
/** Tool the model calls to save the plan artifact. */
|
|
36
38
|
export const WRITE_PLAN_TOOL = "write_plan";
|
|
37
39
|
/** Tool the model calls to keep an approved plan's steps current. */
|
|
38
40
|
export const SET_PLAN_STEPS_TOOL = "set_plan_steps";
|
|
39
41
|
/** Custom-entry type used for plan-mode state. */
|
|
40
|
-
export const STATE_TYPE =
|
|
42
|
+
export const STATE_TYPE = PLAN_MODE_ENTRY_TYPE;
|
|
41
43
|
/** Custom-entry type used for a plan's review baseline. */
|
|
42
44
|
export const BASELINE_TYPE = "plan-baseline";
|
|
43
45
|
|