@hank-warren/pi-plan-mode 0.1.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/LICENSE +21 -0
- package/NOTICE.md +7 -0
- package/README.md +313 -0
- package/index.ts +1 -0
- package/package.json +47 -0
- package/src/active-implementation-menu.ts +68 -0
- package/src/auto-permissions-delegation.ts +122 -0
- package/src/command.ts +30 -0
- package/src/completion-tool.ts +91 -0
- package/src/extension-runtime.ts +24 -0
- package/src/fresh-implementation.ts +213 -0
- package/src/implementation-retention.ts +122 -0
- package/src/index.ts +1 -0
- package/src/interactive-ui.ts +5 -0
- package/src/message-transform.ts +232 -0
- package/src/plan-action-controller.ts +103 -0
- package/src/plan-action-menus.ts +197 -0
- package/src/plan-export-controller.ts +38 -0
- package/src/plan-export-screen.ts +19 -0
- package/src/plan-export.ts +145 -0
- package/src/plan-launch-menu.ts +122 -0
- package/src/plan-mode.ts +1037 -0
- package/src/presentation.ts +108 -0
- package/src/prompt.ts +67 -0
- package/src/question-tool.ts +273 -0
- package/src/required-tools.ts +22 -0
- package/src/saved-plan-menu.ts +93 -0
- package/src/saved-plan-preflight.ts +39 -0
- package/src/settings-menu.ts +384 -0
- package/src/settings.ts +420 -0
- package/src/state.ts +167 -0
- package/src/tool-policy.ts +563 -0
- package/src/tool-selection.ts +98 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 narumiruna
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Attribution
|
|
2
|
+
|
|
3
|
+
This package is a fork of [`@narumitw/pi-plan-mode`](https://github.com/narumiruna/pi-extensions/tree/main/extensions/pi-plan-mode), version 0.49.3, from upstream commit `4c2c2e8c4b6c3d21659110ea1966810b1d15e045`.
|
|
4
|
+
|
|
5
|
+
The original work is Copyright (c) 2026 narumiruna and is used under the MIT License included in [`LICENSE`](LICENSE).
|
|
6
|
+
|
|
7
|
+
Fork-specific changes are Copyright (c) 2026 Hank Warren and are released under the same MIT License.
|
package/README.md
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# 🧠pi-plan-mode — Codex-like Plan Mode for Pi
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@hank-warren/pi-plan-mode) [](https://pi.dev) [](./LICENSE)
|
|
4
|
+
|
|
5
|
+
`@hank-warren/pi-plan-mode` adds a Codex-like `/plan` collaboration mode to Pi. Plan mode is for read-only exploration, clarifying questions, and a structured implementation-ready plan before any code mutation happens.
|
|
6
|
+
|
|
7
|
+
This is a fork of `@narumitw/pi-plan-mode` 0.49.3. It preserves the upstream Plan workflow and adds an opt-in, fail-closed integration with `@ogulcancelik/pi-auto-permissions` so guarded planning-time information gathering can run without a second Plan-mode veto. See [NOTICE.md](NOTICE.md) for attribution.
|
|
8
|
+
|
|
9
|
+
Pi core intentionally does not ship a built-in plan mode; this package provides one as an independently installable extension.
|
|
10
|
+
|
|
11
|
+
## ✨ Features
|
|
12
|
+
|
|
13
|
+
- Adds a state-aware `/plan` launch and management menu, plus `/plan start` for direct activation.
|
|
14
|
+
- Adds `--plan` to start a session in Plan mode.
|
|
15
|
+
- Enables built-in read-only tools by default while Plan mode is active.
|
|
16
|
+
- Disables extension and custom tools by default, with persistent pre-start Settings and a staged `/plan tools` compatibility shortcut for explicit user-risk opt-in.
|
|
17
|
+
- Blocks `update_plan`, mutating built-in tools, and—by default—unsafe `bash` forms such as writes, substitutions, background jobs, dependency installs, and mutating Git commands.
|
|
18
|
+
- Optionally delegates only Bash commands covered by active Auto Permissions guarded rules, while keeping `edit`, `write`, and `update_plan` blocked and preserving the model-level non-mutation instructions.
|
|
19
|
+
- Injects Codex-like Plan mode instructions: explore first, ask decision questions for high-impact ambiguity, do not mutate files, and finalize only when decision-complete.
|
|
20
|
+
- Adds required `plan_mode_question` and `plan_mode_complete` tools for structured questions and completion.
|
|
21
|
+
- Presents the complete plan and lets you implement with the planning conversation or start a fresh linked session carrying only the approved plan, as well as export, save, stay, or discard.
|
|
22
|
+
- Exports ready, saved, or active implementation plans with `/plan export [path]` without overwriting existing paths; the omitted-path destination is configurable, and exporting a ready plan completes and exits Plan mode.
|
|
23
|
+
- Lets each accepted plan remain active, serve only as the first implementation handoff, or clear after the first implementation run; the current retained-plan behavior remains the default.
|
|
24
|
+
- Keeps legacy `<proposed_plan>` responses compatible without advertising XML as the primary workflow.
|
|
25
|
+
- Shows Plan mode state in Pi's statusline as `plan active`, `plan ready`, `plan saved`, or `plan implementing`; `@narumitw/pi-statusline` adds the default `📝` icon unless configured otherwise (this repo's `@hank-warren/pi-statusline` does not render extension statuses).
|
|
26
|
+
- Persists Plan mode, one session-local saved plan, and active implementation state so resume and compaction retain the exact accepted plan.
|
|
27
|
+
|
|
28
|
+
## 📦 Install
|
|
29
|
+
|
|
30
|
+
This release requires Pi 0.80.6 or newer.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pi install npm:@hank-warren/pi-plan-mode
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Try without installing permanently:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pi -e npm:@hank-warren/pi-plan-mode
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Try this package locally from the repository root:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pi -e ./packages/pi-plan-mode
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 🚀 Usage
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
/plan
|
|
52
|
+
/plan start
|
|
53
|
+
/plan <prompt>
|
|
54
|
+
/plan tools
|
|
55
|
+
/plan show
|
|
56
|
+
/plan finalize
|
|
57
|
+
/plan implement
|
|
58
|
+
/plan save
|
|
59
|
+
/plan export [path]
|
|
60
|
+
/plan exit
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
In TUI and RPC, use bare `/plan` to open the menu for the current Plan state. When Plan mode is off
|
|
64
|
+
and no plan is stored, the launch menu shows the effective next-start tools and offers **Start Plan
|
|
65
|
+
mode**, **Choose tools, then start…**, **Settings**, and **How Plan mode works**. Settings edits the
|
|
66
|
+
persistent defaults for later workflows. Launch-menu tool changes remain a draft until **Done — start
|
|
67
|
+
Plan mode** is selected; Back, Escape, Ctrl+C, disposal, session replacement, and shutdown discard
|
|
68
|
+
the draft without changing Plan state, active tools, thinking, or the stored selection.
|
|
69
|
+
|
|
70
|
+
Use `/plan start` when you want to enter Plan mode directly without sending a model message. Use
|
|
71
|
+
`/plan <prompt>` to enter Plan mode and immediately submit `<prompt>` as the first Plan-mode user
|
|
72
|
+
message. The exact argument `start` is reserved for direct activation; longer text such as `/plan
|
|
73
|
+
start a migration` remains an inline planning prompt. `--plan` also remains a direct activation path.
|
|
74
|
+
|
|
75
|
+
Use **Choose tools, then start…** or the `/plan tools` compatibility shortcut to choose a
|
|
76
|
+
session-specific override before Planning starts. Both routes use the same draft selector: **Done —
|
|
77
|
+
start Plan mode** stores the selection and starts the workflow, while cancellation leaves Plan mode
|
|
78
|
+
off and changes nothing. The bounded multi-select shows 10 rows at a time, supports viewport paging,
|
|
79
|
+
descriptions, and explicit unavailable rows for blocked tools. In TUI mode, type to fuzzy-search tool
|
|
80
|
+
names, descriptions, policy, and source metadata; RPC keeps the complete unfiltered list. Once Plan
|
|
81
|
+
mode is active, tools are locked: `/plan` no longer offers tool or Settings actions, and `/plan tools`
|
|
82
|
+
rejects the request. Exit and start a new workflow if a different tool set is required. The
|
|
83
|
+
`plan_mode_question` tool keeps its one-off question/choice dialog because it is a model-requested
|
|
84
|
+
planning interaction, not command-menu navigation. `/plan show` displays the stored
|
|
85
|
+
plan without starting a model turn, including the accepted plan while implementation is active.
|
|
86
|
+
`/plan finalize` explicitly asks the agent to complete the plan or ask one remaining material
|
|
87
|
+
question, `/plan save` stores a completed ready plan for later and leaves Plan mode, and `/plan
|
|
88
|
+
export [path]` writes a ready, saved, or active implementation plan to Markdown. Completed and saved
|
|
89
|
+
plan menus offer **Implement here**, which continues with the planning conversation, and **Start
|
|
90
|
+
fresh and implement**, which opens a new session and transfers only the approved plan. The direct
|
|
91
|
+
`/plan implement` compatibility route remains equivalent to **Implement here** and never opens a
|
|
92
|
+
selector. A successful ready-plan export also leaves Plan mode; saved and active implementation
|
|
93
|
+
exports retain their existing state. `show`, `save`, `export`, and `implement` fail closed when no
|
|
94
|
+
applicable plan is stored; `finalize` requires active Plan mode.
|
|
95
|
+
|
|
96
|
+
`/plan export` uses the configured **Export destination**, which defaults to `PLAN.md`. Supply a path
|
|
97
|
+
to override that default for one export. Relative paths resolve from the command's current `ctx.cwd`
|
|
98
|
+
at export time, absolute paths remain absolute, a leading `@` is accepted for Pi path compatibility,
|
|
99
|
+
and missing parent directories are created. Explicit `/plan export <path>` input always wins over the
|
|
100
|
+
setting. Export never overwrites an existing file, directory, or symbolic
|
|
101
|
+
link: choose another path or remove the existing target first. A successful export adds one trailing
|
|
102
|
+
newline but otherwise preserves the accepted Markdown exactly. After a ready plan is written, Plan
|
|
103
|
+
mode ends, its tools and thinking level are restored, and the ready state is cleared without starting
|
|
104
|
+
a model turn. Exporting a saved or active implementation plan leaves that state unchanged. Failed or
|
|
105
|
+
cancelled exports leave every Plan-mode state unchanged. The resulting file is available to the agent
|
|
106
|
+
through its normal file-reading tools. Export is an explicit user-requested file mutation;
|
|
107
|
+
model-initiated Plan-mode writes remain blocked.
|
|
108
|
+
|
|
109
|
+
In TUI and RPC, **Export plan…** opens a single-line path input from every ready, saved, or active
|
|
110
|
+
plan menu. The input shows both the configured value and its currently resolved path. Submit an empty
|
|
111
|
+
value to use the configured destination, or enter a relative or absolute one-off path. A failed TUI
|
|
112
|
+
export retains the draft for correction; RPC reopens its input dialog. Escape returns to
|
|
113
|
+
the owning menu without writing a file. A successful ready-plan export closes the menu and ends Plan
|
|
114
|
+
mode; saved and active implementation menus close without changing their stored state.
|
|
115
|
+
|
|
116
|
+
When Plan mode is active, ask the agent to design the change. The agent may inspect files and run read-only commands, but it should not edit files or execute the implementation. It should explore first, then use structured questions when your preference or a tradeoff materially changes the plan. Configure persistent defaults or a one-workflow tool override before activation; Planning and ready menus deliberately keep those controls locked.
|
|
117
|
+
|
|
118
|
+
By default, Plan mode manages only Pi's built-in tools: `read`, limited `bash`, available read-only built-ins such as `grep`, `find`, and `ls`, plus the required `plan_mode_question` and `plan_mode_complete` tools. Built-in `edit` and `write` are blocked. `update_plan` is also blocked because it tracks execution progress rather than conversational planning. Extension and custom tools are disabled by default because Pi tools do not expose standardized mutability metadata; enable them before starting from Settings or the staged workflow selector only when you accept the risk. For example, you can opt into `firecrawl_scrape`, `firecrawl_search`, or `lsp_diagnostics` if those extensions are loaded and you want to use them during planning.
|
|
119
|
+
|
|
120
|
+
With the default `limited` Bash policy, Plan mode uses a fail-closed inspection allowlist, including when an extension overrides the canonical `bash` tool name. It accepts common inspection commands, read-only Git and npm queries, pipelines and command lists composed entirely of accepted commands, plus selected checks such as `npm test`, `npm run typecheck`, and `cargo test`. It rejects output/input redirects, shell expansion, substitutions, subshells, background jobs, mutating flags, dependency changes, editors, and unknown commands. A rejected parsed command list or pipeline identifies its first blocked command segment; malformed or unsupported shell syntax reports the complete submitted input instead. Tests and builds may still write ignored caches or build artifacts and may execute project-defined hooks; enable or invoke them only when the repository is trusted. This is extension-level risk reduction, not an OS sandbox.
|
|
121
|
+
|
|
122
|
+
Set `bashPolicy` to `auto-permissions` to integrate with `@ogulcancelik/pi-auto-permissions` 0.1.x. Plan mode first applies its normal inspection allowlist. For a command that allowlist would block, it stands down only when the Auto Permissions extension is loaded and its current configuration contains a matching, non-trusted `guarded` rule. Missing, disabled, malformed, unmatched, convention-only, or trusted-group-bypassed configurations keep the Plan-mode block. Auto Permissions then performs its normal guardian review or user approval flow. Plan mode still blocks built-in `edit`, `write`, and `update_plan`, and its model instructions still prohibit implementation and mutation.
|
|
123
|
+
|
|
124
|
+
`plan_mode_question` follows Codex's `request_user_input` pattern: the agent can ask 1-3 concise questions, each with meaningful options and a free-form Other path. If you cancel or no interactive UI is available, the agent should ask a concise plain-text question or proceed only with a clearly stated low-risk assumption instead of prematurely producing a final plan.
|
|
125
|
+
|
|
126
|
+
Pi activates tools by tool name. The pre-start selector stores accepted session selections by name
|
|
127
|
+
and shows each effective tool's source from Pi metadata, such as `built-in`, a user extension path,
|
|
128
|
+
or a project extension path. If an extension overrides a built-in tool with the same name, Pi exposes the effective tool for that name and the selector shows that source.
|
|
129
|
+
|
|
130
|
+
A complete Plan mode answer should appear only after the agent has resolved discoverable facts and high-impact user decisions. The agent must call `plan_mode_complete({ plan })` alone as its final action, passing the complete Markdown plan. The tool rejects empty or whitespace-only plans and plans longer than 50,000 JavaScript characters; it does not truncate. Its visible result contains the full plan, and versioned result details let the extension restore it safely from the active session branch.
|
|
131
|
+
|
|
132
|
+
`plan_mode_complete` uses Pi's `terminate: true` hint. Termination is best effort: if a model puts it in a parallel tool batch, Pi terminates the batch early only when every finalized sibling tool also terminates. The prompt therefore requires the completion call to be standalone and last. The extension deliberately does not infer completion from phrases such as “I will present the plan,” and it does not automatically retry a turn with no plan because research and clarification turns may legitimately remain unfinished. If a turn ends without a plan, Plan mode stays active; use `/plan finalize` for explicit recovery.
|
|
133
|
+
|
|
134
|
+
Legacy sessions and models may still submit one non-empty `<proposed_plan>` block with tags on their own lines. That compatibility path remains accepted, but it is not the primary workflow. Empty, malformed, unclosed, or multiple legacy blocks keep Plan mode active and produce a warning.
|
|
135
|
+
|
|
136
|
+
After completion, `/plan` opens the ready actions when interactive UI is available. The same flat menu shows **Implement here** and **Start fresh and implement**, explains which conversation context each choice uses, and previews how long the approved plan will remain active. **Implement here**—and the compatibility route `/plan implement`—disables Plan mode, restores full tool access, captures the current **After Implement** policy, and starts implementation in the current session with its planning conversation. **Start fresh and implement** waits for the source session to become idle, verifies the selected model and authentication, creates a new session linked to the persisted source as its parent, and transfers the exact approved plan without copying planning messages, tool results, or compaction/branch summaries. The destination still loads its normal `AGENTS.md`, skills, project resources, and extensions. Choosing **Export plan…** asks for a destination, writes the plan, restores normal tools and thinking, and leaves Plan mode without starting a model turn. Choosing **Save for later**—or running `/plan save`—instead stores one plan in the current Pi session before leaving Plan mode.
|
|
137
|
+
|
|
138
|
+
When a workflow was started only with `--plan` and no `/plan` command has run in that session, the automatic menu cannot obtain Pi's command-only session replacement capability; choosing fresh asks you to reopen `/plan`, where the same action is available. A successful fresh handoff does not delete or consume the source planning session. Resume it later to inspect or hand off the ready/saved plan again; this deliberate duplication is the recovery path if the destination work is abandoned. In-memory sessions create an unlinked fresh session because no parent file exists. Escape, Ctrl+C, menu disposal, source replacement/shutdown, model/auth failure, or cancellation by another extension before replacement leaves the source plan unchanged. Once replacement succeeds, the destination persists active-plan state before kickoff. If persistence fails, the complete request is placed in the destination editor and the source remains resumable. If kickoff fails, the destination retains the active plan; send a message to continue, use `/plan exit` to clear it, or resume the parent planning session.
|
|
139
|
+
|
|
140
|
+
A saved plan appears as `plan saved` and remains available after reload, resume, branch-local fork, and compaction in that session. It does not expire automatically, cross into a new session, or participate in ordinary model context. Open `/plan` to Show, Implement here, Start fresh and implement, Export, open Settings, or Clear it; `/plan show`, `/plan implement`, `/plan export [path]`, and `/plan exit`/`off` retain their direct routes in TUI and RPC. Fresh implementation checks idle state, the selected model, and authentication before session replacement; Implement here keeps its established preflight behavior. Starting another workflow with `/plan start`, `/plan <prompt>`, or `/plan tools` is blocked until the saved plan is implemented or cleared, so the single saved slot is never silently overwritten. Resuming that session with `--plan` moves the saved plan back to ready Plan mode. Cancellation or failed implementation preflight leaves it unchanged.
|
|
141
|
+
|
|
142
|
+
Text print and JSON modes cannot display the bare `/plan` menu and reject that route before changing
|
|
143
|
+
state; use `/plan start` for direct no-prompt activation or `/plan <prompt>` to start planning with a
|
|
144
|
+
prompt. `/plan tools` also rejects before changing state because its staged selector requires TUI or
|
|
145
|
+
RPC. These modes can export any stored plan with `/plan export [path]`, save a ready plan with
|
|
146
|
+
`/plan save`, and clear it with `/plan exit` or `/plan off`. Successful export is observable through
|
|
147
|
+
the created file; exporting a ready plan also leaves Plan mode, while saved and active implementation
|
|
148
|
+
state remains unchanged. An existing target or missing plan fails the command without changing state.
|
|
149
|
+
These modes reject saved-plan display and implementation before changing state because Pi provides
|
|
150
|
+
neither printable custom-message output nor acknowledged extension-triggered turns; resume the
|
|
151
|
+
session in TUI or RPC to show or implement it.
|
|
152
|
+
|
|
153
|
+
Both implementation paths apply the current **After Implement** policy in their destination; the fresh-session choice does not change retention semantics. With the default **Keep plan active** policy, the exact accepted plan remains active across later turns, session resume, and manual or automatic compaction without depending on the compaction summary. Plan mode avoids a duplicate context block while the original implementation handoff remains available and injects one hidden canonical copy after that handoff is compacted away. This can consume up to the existing 50,000-character plan limit in model context when reinjection is needed. **Use plan for handoff only** clears retained state immediately after the first implementation request receives the complete plan. **Clear after first implementation run** keeps the plan through that run, including retries, compaction retries, and queued continuation, then clears it at `agent_settled`. Cleanup is bound to the matching implementation, so an older run settling cannot clear a newer handoff.
|
|
154
|
+
|
|
155
|
+
While implementation is active, `/plan show` displays the accepted plan. Interactive `/plan` offers Show, Export plan…, Settings, Start a new plan, and Clear; `/plan exit` and `/plan off` are the direct clear routes. Settings changes never alter the policy already captured by the active implementation. Automatic cleanup has the same observable result as clearing: its active status and future injected plan context disappear, while the implementation request that triggered cleanup still receives the complete plan. Starting a new Plan-mode workflow or implementing a replacement plan supersedes the previous active plan. The extension deliberately does not infer completion from assistant prose or agent settlement, so clear the active plan when the implementation no longer applies. Choosing Stay before implementation keeps the plan ready. Revision feedback starts another Plan-mode turn and clears the previous implementable plan until an updated completion arrives. For clarification-only follow-ups, the agent answers and resubmits the complete unchanged plan so it remains implementable. Before saving or implementation, exit/off discards the ready plan and removes its completion result from later non-Plan model context.
|
|
156
|
+
|
|
157
|
+
While Plan mode is enabled, the extension also publishes a compact status for Pi statuslines. With a statusline that renders extension statuses (for example `@narumitw/pi-statusline`; this repo's `@hank-warren/pi-statusline` does not), this appears as:
|
|
158
|
+
|
|
159
|
+
- `plan active`: Plan mode is enabled and still gathering context or drafting a plan.
|
|
160
|
+
- `plan ready`: A completed plan is stored until you implement it, export it, save it, continue planning, or exit Plan mode.
|
|
161
|
+
- `plan saved`: One completed plan is stored outside model context in the current session until you implement or clear it.
|
|
162
|
+
- `plan implementing`: The exact accepted plan is currently retained under its captured **After Implement** policy.
|
|
163
|
+
|
|
164
|
+
You can also exit directly. Before implementation, direct exit discards the latest proposed plan; while a plan is saved, it clears that saved plan. During implementation, it removes both the original implementation handoff and the extension's canonical active-plan block from later model calls; an earlier Pi-generated compaction summary may still describe prior work:
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
/plan exit
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## ⚙️ Settings
|
|
171
|
+
|
|
172
|
+
Open **Settings** from an inactive `/plan` menu to edit one flat group of five workflow choices: **Plan thinking**, **Plan tools**, **Bash policy**, **After Implement**, and **Export destination**. You can also edit `$PI_CODING_AGENT_DIR/pi-plan-mode.json` (normally `~/.pi/agent/pi-plan-mode.json`) manually; `safeSubcommands` remains JSON-only. The file is optional, is read at session start, and is created only after an explicit Settings save or manual edit.
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"thinkingLevel": "inherit",
|
|
177
|
+
"defaultPlanTools": ["read", "bash", "grep", "find", "ls", "mcp"],
|
|
178
|
+
"bashPolicy": "auto-permissions",
|
|
179
|
+
"implementationPlanRetention": "keep",
|
|
180
|
+
"defaultPlanExportPath": "PLAN.md",
|
|
181
|
+
"safeSubcommands": {
|
|
182
|
+
"git": ["status", "log", "rev-parse", "blame"],
|
|
183
|
+
"gh": ["pr view", "pr list", "issue view", "issue list"]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Default Plan tools
|
|
189
|
+
|
|
190
|
+
`defaultPlanTools` defines the initial tool selection when a session has no stored pre-start selection. Omit it—or choose **Use automatic safe built-ins**—to keep the available safe built-ins as the default. An explicit empty array appears as **Required tools only** and enables only `plan_mode_question` and `plan_mode_complete`.
|
|
191
|
+
|
|
192
|
+
Tool names must be non-empty strings; duplicates are removed in first-seen order. Unknown, unavailable, and Plan-mode-blocked names are ignored when tools are activated. Settings retains configured unavailable names and shows them as unavailable; resetting to automatic removes the entire override. A tool registered after Plan mode is already active is not added automatically; exit and start a new Plan workflow to apply another set. Non-built-in tools named in this global setting are an explicit user-risk opt-in, just like selecting them in the pre-start workflow selector. Plan mode does not interpret their arguments or actions: enabling one trusts the whole effective tool. Pi resolves tools by name, so if an extension overrides a built-in name, the effective extension tool is selected instead. An effective tool named `bash` remains subject to the limited-shell policy regardless of its source metadata.
|
|
193
|
+
|
|
194
|
+
A selection accepted through **Choose tools, then start…** or `/plan tools` is stored in that Pi session and takes precedence over `defaultPlanTools` when the session resumes. The global setting remains the baseline for fresh sessions and sessions without an explicit selection. Settings saves immediately, but the saved tools and thinking level apply only when a later Plan workflow starts; they never mutate a workflow already in progress.
|
|
195
|
+
|
|
196
|
+
### Bash policy
|
|
197
|
+
|
|
198
|
+
`bashPolicy` controls which extension makes Bash authorization decisions while Plan mode is active:
|
|
199
|
+
|
|
200
|
+
- Omit it or use `limited` for the upstream-compatible, fail-closed Plan-mode inspection allowlist.
|
|
201
|
+
- Use `auto-permissions` to retain the Plan allowlist while selectively deferring blocked commands that match an active Auto Permissions guarded rule.
|
|
202
|
+
|
|
203
|
+
Delegation does not activate tools. Keep `bash` in `defaultPlanTools` or select it before starting the workflow. It also does not activate custom tools: add names such as `mcp`, `source_check`, or `ask_user_question` to `defaultPlanTools`, or select them at launch. Plan mode treats every selected non-built-in tool as a whole-tool user-risk opt-in because Pi does not publish standardized mutability metadata.
|
|
204
|
+
|
|
205
|
+
An Auto Permissions approval is permission to execute the exact call, not permission to implement the plan. The Plan prompt continues to require non-mutating information gathering. The compatibility preflight is deliberately fail-closed and currently mirrors the Auto Permissions 0.1.x configuration contract, including `PI_AUTO_PERMISSIONS_CONFIG` and trusted project groups. If that contract changes in a future Auto Permissions release, unmatched or unreadable policy falls back to the Plan-mode block.
|
|
206
|
+
|
|
207
|
+
### After Implement
|
|
208
|
+
|
|
209
|
+
`implementationPlanRetention` controls the result of the next Implement action. Omit it or use `keep` for **Keep plan active**, the backward-compatible behavior that retains and reinjects the exact plan until `/plan exit` or supersession. Use `clear-on-start` for **Use plan for handoff only**: the first matching implementation request receives the complete plan, then retained state clears before later requests. Use `clear-after-first-run` to retain the plan until that implementation's first fully settled run ends. A resumed cleanup policy re-arms against the first context in the replacement session. Failed handoff delivery restores the ready or saved plan and does not run automatic cleanup.
|
|
210
|
+
|
|
211
|
+
Changing this setting applies to the next Implement action only. Each active implementation stores its effective policy, so a later Settings save cannot shorten or extend an implementation already in progress. `/plan exit` remains available under every policy.
|
|
212
|
+
|
|
213
|
+
### Export destination
|
|
214
|
+
|
|
215
|
+
`defaultPlanExportPath` controls only exports that omit a path. Omit it—or submit an empty value in Settings—to use `PLAN.md`. The value must be a non-empty string of at most 4,096 characters without terminal control characters or NUL. Relative values are resolved against the current working directory at export time; the Settings detail and every export input preview the concrete resolved destination. An explicit `/plan export <path>` is a one-off override and does not edit Settings. Saving a new destination affects the next export immediately, including export of a currently active implementation.
|
|
216
|
+
|
|
217
|
+
The existing no-overwrite, cancellation, and atomic Plan-state behavior is unchanged. A failed save rolls the row back to its previous value; a failed or cancelled export preserves the plan and target. Long previews wrap or truncate to the available terminal width without changing the raw path used by the action.
|
|
218
|
+
|
|
219
|
+
### Safe shell subcommands
|
|
220
|
+
|
|
221
|
+
`safeSubcommands` adds reviewed command validators to limited `bash`; it is not a raw shell allowlist. Only the following exact values are accepted:
|
|
222
|
+
|
|
223
|
+
- `git`: `status`, `log`, `diff`, `show`, `branch`, `remote`, `ls-files`, `grep`, `rev-parse`, `blame`, `describe`, `merge-base`, `ls-tree`, and `cat-file`.
|
|
224
|
+
- `gh`: `pr view`, `pr list`, `issue view`, and `issue list`.
|
|
225
|
+
|
|
226
|
+
The first eight Git validators are built in and remain enabled when omitted, so listing them is valid but redundant. The other six Git validators and every `gh` path require an explicit opt-in. Git entries select one exact subcommand; `gh` entries select one exact two-word path, so `"pr view"` never enables `pr merge`, `pr close`, or `pr edit`. Omitted `safeSubcommands`, an empty object, and empty arrays preserve the default policy. Duplicate values are removed in first-seen order.
|
|
227
|
+
|
|
228
|
+
With the example configuration above, commands such as these are accepted:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
git rev-parse --show-toplevel
|
|
232
|
+
git blame -- src/plan-mode.ts
|
|
233
|
+
git diff --cached
|
|
234
|
+
git show --stat --oneline HEAD
|
|
235
|
+
git log -p -1 HEAD -- src/plan-mode.ts
|
|
236
|
+
gh pr view 218 --json number,title,state
|
|
237
|
+
gh issue list --state open --json number,title,state
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The command-specific validators still reject unsafe forms, including:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
git blame --textconv -- src/plan-mode.ts
|
|
244
|
+
git cat-file --filters HEAD
|
|
245
|
+
git diff --ext-diff
|
|
246
|
+
git log --show-signature -1
|
|
247
|
+
git remote show origin
|
|
248
|
+
git show --textconv HEAD
|
|
249
|
+
gh pr merge 218
|
|
250
|
+
gh pr view 218
|
|
251
|
+
gh pr view 218 --web
|
|
252
|
+
gh pr view 218 > pr.txt
|
|
253
|
+
gh pr list --json number,title && gh pr merge 218
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Redirects, shell expansion and substitution, explicit pager or browser requests, explicit external diff/textconv/filter/signature helpers, output flags, malformed command layouts, and any chain containing an unsafe segment fail closed. Read-dominant Git validators accept ordinary inspection flags without requiring `--no-textconv` or `--no-ext-diff`; Git may therefore invoke a helper configured by the user or trusted repository even when the command does not request one explicitly. Use the negative flags when you want to suppress those configured helpers. Mixed read/write surfaces remain narrower: use `git remote show -n` to avoid invoking a transport helper, while mutating `branch` and `remote` forms remain blocked. GitHub CLI read paths require `--json <fields>` output so Plan mode does not rely on `GH_PAGER`, `PAGER`, or gh pager configuration. Unknown `safeSubcommands` keys or values, non-array values, and non-string entries invalidate the entire settings file and trigger the normal warning/default fallback on session start.
|
|
257
|
+
|
|
258
|
+
Read-only does not mean private: Git inspection can expose repository history and tracked secrets, while `gh` queries can expose remote repository, pull request, and issue data available to your authenticated account. The policy reduces accidental mutation and explicit helper execution; it is not a sandbox or a confidentiality boundary.
|
|
259
|
+
|
|
260
|
+
### Thinking level
|
|
261
|
+
|
|
262
|
+
Plan mode inherits Pi's current thinking level by default. Set `thinkingLevel` to request a fixed level only while Plan mode is active. Supported values are `inherit`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. The extension snapshots the prior level and restores it on exit only if the level still matches the value it applied; a manual change made during Plan mode is preserved. A Settings save does not change Pi's current or default thinking level and takes effect only when the next Plan workflow starts.
|
|
263
|
+
|
|
264
|
+
Settings saves are serialized in invocation order inside one Pi process. Each save re-reads the latest valid document, preserves unknown top-level fields and unedited `safeSubcommands`, then publishes through a same-directory temporary file and rename. A missing file stays absent until an explicit save. Invalid JSON, invalid values, oversized content, non-regular files, and read failures make Settings read-only; the existing bytes and previous effective settings remain. This in-process queue is not a cross-process lock, so concurrent separate Pi processes can still race.
|
|
265
|
+
|
|
266
|
+
Invalid settings produce a warning and fall back to inherited thinking, available safe-built-in tool defaults, the `limited` Bash policy, `keep`, and `PLAN.md`. Compatibility: a valid legacy `plan-mode.json` remains readable with a warning and is never modified automatically. If Settings is explicitly saved while only that legacy file exists, the extension creates canonical `pi-plan-mode.json` from the complete legacy document, applies the selected change, preserves unknown fields, and leaves the legacy file untouched. If both files exist, the canonical filename takes precedence.
|
|
267
|
+
|
|
268
|
+
## đź§ Codex-like behavior
|
|
269
|
+
|
|
270
|
+
This extension maps Codex's `ModeKind::Plan` behavior onto Pi's extension API:
|
|
271
|
+
|
|
272
|
+
- Plan mode is a conversational collaboration mode, not TODO/progress tracking.
|
|
273
|
+
- `/plan <prompt>` follows Codex behavior by switching to Plan mode before submitting the inline prompt.
|
|
274
|
+
- The agent should use `plan_mode_question` for important non-discoverable preferences or tradeoffs before finalizing.
|
|
275
|
+
- The agent completes with a standalone `plan_mode_complete` tool call instead of relying on semantic prose detection.
|
|
276
|
+
- `update_plan` checklist use is blocked while Plan mode is active.
|
|
277
|
+
- The implementation boundary is explicit: Plan mode restores tools before saving or starting implementation, saving keeps the plan outside ordinary model context, choosing implementation immediately triggers a normal agent turn with full tool access, and the accepted plan follows the captured `keep`, `clear-on-start`, or `clear-after-first-run` lifecycle.
|
|
278
|
+
- Pi extension safety is approximated with tool classification and fail-closed filtering for every effective tool named `bash`; the optional Auto Permissions integration delegates only exact commands covered by active guarded rules. Other non-built-in tools remain user-selected at user risk because Pi does not expose standardized tool mutability metadata.
|
|
279
|
+
- Unlike native Codex, this extension uses a terminating Pi tool plus an `agent_settled` ready flow; Pi cannot provide sandbox-level enforcement.
|
|
280
|
+
|
|
281
|
+
## 🗂️ Package layout
|
|
282
|
+
|
|
283
|
+
```txt
|
|
284
|
+
packages/pi-plan-mode/
|
|
285
|
+
├── index.ts # Pi package entrypoint
|
|
286
|
+
├── src/
|
|
287
|
+
│ ├── plan-mode.ts # Extension registration, mode state, and UI loading boundary
|
|
288
|
+
│ ├── interactive-ui.ts # Lazily loaded interactive menu surface
|
|
289
|
+
│ └── *.ts # Package-local prompt, policy, question, and message modules
|
|
290
|
+
├── test/
|
|
291
|
+
├── README.md
|
|
292
|
+
├── NOTICE.md
|
|
293
|
+
├── LICENSE
|
|
294
|
+
└── package.json
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The root `index.ts` forwards to the internal source entrypoint. The package exposes its Pi extension through `package.json`:
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"pi": {
|
|
302
|
+
"extensions": ["./index.ts"]
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## 🔎 Keywords
|
|
308
|
+
|
|
309
|
+
Pi extension, Pi coding agent, plan mode, Codex-like plan mode, AI coding workflow, read-only planning, implementation plan.
|
|
310
|
+
|
|
311
|
+
## đź“„ License
|
|
312
|
+
|
|
313
|
+
MIT. See [`LICENSE`](./LICENSE).
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./src/index.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hank-warren/pi-plan-mode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Codex-like Plan mode for Pi with fail-closed integration for Auto Permissions Bash review.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"pi",
|
|
10
|
+
"plan-mode",
|
|
11
|
+
"planning",
|
|
12
|
+
"permissions"
|
|
13
|
+
],
|
|
14
|
+
"author": "Hank Warren",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/hank-warren/pi-extensions.git",
|
|
19
|
+
"directory": "packages/pi-plan-mode"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/hank-warren/pi-extensions/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/hank-warren/pi-extensions/tree/main/packages/pi-plan-mode#readme",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"pi": {
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./index.ts"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"index.ts",
|
|
35
|
+
"src",
|
|
36
|
+
"README.md",
|
|
37
|
+
"NOTICE.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
42
|
+
"@earendil-works/pi-tui": "*"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@narumitw/pi-tui-kit": "^0.49.1"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
3
|
+
import { type PlanExportDestinationProvider, planExportInputScreen } from "./plan-export-screen.js";
|
|
4
|
+
|
|
5
|
+
interface ActiveImplementationMenuOptions {
|
|
6
|
+
statusText: string;
|
|
7
|
+
getExportDestination: PlanExportDestinationProvider;
|
|
8
|
+
signal: AbortSignal;
|
|
9
|
+
isCurrent(): boolean;
|
|
10
|
+
show(): void;
|
|
11
|
+
exportPlan(path: string, signal: AbortSignal): Promise<boolean>;
|
|
12
|
+
settings(signal: AbortSignal): Promise<boolean>;
|
|
13
|
+
startNew(): void;
|
|
14
|
+
clear(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function showActiveImplementationMenu(
|
|
18
|
+
ctx: ExtensionContext,
|
|
19
|
+
options: ActiveImplementationMenuOptions,
|
|
20
|
+
) {
|
|
21
|
+
type Screen = "active" | "export";
|
|
22
|
+
type Action = "show" | "export" | "settings" | "start-new" | "clear";
|
|
23
|
+
const menu = defineMenu<undefined, Screen, Action, ExtensionContext>({
|
|
24
|
+
start: "active",
|
|
25
|
+
screens: {
|
|
26
|
+
active: () => ({
|
|
27
|
+
kind: "actions",
|
|
28
|
+
title: "Active implementation plan",
|
|
29
|
+
lines: [options.statusText],
|
|
30
|
+
items: [
|
|
31
|
+
{ id: "show", label: "Show active implementation plan", action: "show" },
|
|
32
|
+
{ id: "export", label: "Export plan…", to: "export" },
|
|
33
|
+
{ id: "settings", label: "Settings", action: "settings" },
|
|
34
|
+
{ id: "start-new", label: "Start a new plan", action: "start-new" },
|
|
35
|
+
{ id: "clear", label: "Clear active implementation plan", action: "clear" },
|
|
36
|
+
],
|
|
37
|
+
hint: "close",
|
|
38
|
+
}),
|
|
39
|
+
export: () => planExportInputScreen(options.getExportDestination),
|
|
40
|
+
},
|
|
41
|
+
actions: {
|
|
42
|
+
show: async () => {
|
|
43
|
+
options.show();
|
|
44
|
+
return { kind: "close" };
|
|
45
|
+
},
|
|
46
|
+
export: async ({ value, signal }) =>
|
|
47
|
+
(await options.exportPlan(value ?? "", signal)) ? { kind: "close" } : { kind: "rejected" },
|
|
48
|
+
settings: async ({ signal }) => {
|
|
49
|
+
const close = await options.settings(signal);
|
|
50
|
+
if (signal.aborted || !options.isCurrent()) return { kind: "rejected" };
|
|
51
|
+
return close ? { kind: "close" } : { kind: "stay" };
|
|
52
|
+
},
|
|
53
|
+
"start-new": async () => {
|
|
54
|
+
options.startNew();
|
|
55
|
+
return { kind: "close" };
|
|
56
|
+
},
|
|
57
|
+
clear: async () => {
|
|
58
|
+
options.clear();
|
|
59
|
+
return { kind: "close" };
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
await runMenu(ctx, menu, {
|
|
64
|
+
getState: () => undefined,
|
|
65
|
+
signal: options.signal,
|
|
66
|
+
isCurrent: options.isCurrent,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { CONFIG_DIR_NAME, getAgentDir, type ToolInfo } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolve, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const AUTO_PERMISSIONS_CONFIG = "pi-auto-permissions/config.json";
|
|
6
|
+
const AUTO_PERMISSIONS_TOOL = "request_override";
|
|
7
|
+
const AUTO_PERMISSIONS_SOURCE_MARKERS = [
|
|
8
|
+
"/@ogulcancelik/pi-auto-permissions/",
|
|
9
|
+
"/@hank-warren/pi-auto-permissions/",
|
|
10
|
+
"/packages/pi-auto-permissions/",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
interface DelegationContext {
|
|
14
|
+
tools: readonly ToolInfo[];
|
|
15
|
+
trustedGroups: ReadonlySet<string>;
|
|
16
|
+
configPath?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface GuardedRule {
|
|
20
|
+
pattern: RegExp;
|
|
21
|
+
group: string;
|
|
22
|
+
level: "guarded" | "convention";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fail-closed compatibility check for @ogulcancelik/pi-auto-permissions 0.1.x
|
|
27
|
+
* and compatible forks (@hank-warren/pi-auto-permissions). Plan mode stands
|
|
28
|
+
* down only when that extension is loaded and its current configuration will
|
|
29
|
+
* guard this exact command.
|
|
30
|
+
*/
|
|
31
|
+
export function shouldDelegateBashToAutoPermissions(
|
|
32
|
+
command: string,
|
|
33
|
+
context: DelegationContext,
|
|
34
|
+
): boolean {
|
|
35
|
+
if (!autoPermissionsIsLoaded(context.tools)) return false;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const config = readConfig(context.configPath);
|
|
39
|
+
if (!config.enabled) return false;
|
|
40
|
+
return config.rules.some((rule) => {
|
|
41
|
+
if (rule.level !== "guarded" || context.trustedGroups.has(rule.group)) return false;
|
|
42
|
+
rule.pattern.lastIndex = 0;
|
|
43
|
+
return rule.pattern.test(command);
|
|
44
|
+
});
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function autoPermissionsIsLoaded(tools: readonly ToolInfo[]): boolean {
|
|
51
|
+
const tool = tools.find((candidate) => candidate.name === AUTO_PERMISSIONS_TOOL);
|
|
52
|
+
const path = tool?.sourceInfo?.path?.replaceAll("\\", "/");
|
|
53
|
+
if (!path) return false;
|
|
54
|
+
return AUTO_PERMISSIONS_SOURCE_MARKERS.some((marker) => path.includes(marker));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readConfig(configPath?: string) {
|
|
58
|
+
const path = configPath
|
|
59
|
+
? resolve(configPath)
|
|
60
|
+
: process.env.PI_AUTO_PERMISSIONS_CONFIG
|
|
61
|
+
? resolve(process.env.PI_AUTO_PERMISSIONS_CONFIG)
|
|
62
|
+
: join(getAgentDir(), AUTO_PERMISSIONS_CONFIG);
|
|
63
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
64
|
+
if (!isRecord(raw)) throw new Error("Auto Permissions config must be an object");
|
|
65
|
+
if (raw.enabled !== undefined && typeof raw.enabled !== "boolean") {
|
|
66
|
+
throw new Error("Auto Permissions enabled must be boolean");
|
|
67
|
+
}
|
|
68
|
+
if (raw.rules !== undefined && !Array.isArray(raw.rules)) {
|
|
69
|
+
throw new Error("Auto Permissions rules must be an array");
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
enabled: raw.enabled !== false,
|
|
73
|
+
rules: (raw.rules ?? []).map(compileRule),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compileRule(value: unknown, index: number): GuardedRule {
|
|
78
|
+
if (!isRecord(value)) throw new Error(`Auto Permissions rules[${index}] must be an object`);
|
|
79
|
+
const pattern = nonEmptyString(value.pattern);
|
|
80
|
+
const group = nonEmptyString(value.group);
|
|
81
|
+
const label = nonEmptyString(value.label);
|
|
82
|
+
if (!pattern || !group || !label) {
|
|
83
|
+
throw new Error(`Auto Permissions rules[${index}] is incomplete`);
|
|
84
|
+
}
|
|
85
|
+
const flags = value.flags ?? "i";
|
|
86
|
+
if (typeof flags !== "string") throw new Error(`Auto Permissions rules[${index}] flags invalid`);
|
|
87
|
+
const level = value.level ?? "guarded";
|
|
88
|
+
if (level !== "guarded" && level !== "convention") {
|
|
89
|
+
throw new Error(`Auto Permissions rules[${index}] level invalid`);
|
|
90
|
+
}
|
|
91
|
+
const message = nonEmptyString(value.message);
|
|
92
|
+
if (value.message !== undefined && !message) {
|
|
93
|
+
throw new Error(`Auto Permissions rules[${index}] message invalid`);
|
|
94
|
+
}
|
|
95
|
+
if (level === "convention" && !message) {
|
|
96
|
+
throw new Error(`Auto Permissions rules[${index}] convention message required`);
|
|
97
|
+
}
|
|
98
|
+
return { pattern: new RegExp(pattern, flags), group, level };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function snapshotAutoPermissionsTrustedGroups(cwd: string, projectTrusted: boolean) {
|
|
102
|
+
const groups = new Set<string>();
|
|
103
|
+
if (!projectTrusted) return groups;
|
|
104
|
+
try {
|
|
105
|
+
const content = readFileSync(join(cwd, CONFIG_DIR_NAME, "trusted-ops"), "utf8");
|
|
106
|
+
for (const line of content.split("\n")) {
|
|
107
|
+
const value = line.trim();
|
|
108
|
+
if (value && !value.startsWith("#")) groups.add(value);
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Match Auto Permissions: missing or unreadable means no trusted groups.
|
|
112
|
+
}
|
|
113
|
+
return groups;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function nonEmptyString(value: unknown) {
|
|
117
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
121
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
122
|
+
}
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface CommandArgumentCompletion {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const PLAN_COMMAND_COMPLETIONS: readonly CommandArgumentCompletion[] = [
|
|
8
|
+
{ value: "start", label: "start", description: "Start Plan mode without sending a prompt" },
|
|
9
|
+
{ value: "show", label: "show", description: "Show the ready, saved, or active plan" },
|
|
10
|
+
{ value: "finalize", label: "finalize", description: "Request a completed plan" },
|
|
11
|
+
{ value: "implement", label: "implement", description: "Implement the completed or saved plan" },
|
|
12
|
+
{ value: "save", label: "save", description: "Save the completed plan for later" },
|
|
13
|
+
{ value: "export", label: "export", description: "Export the stored plan to a Markdown file" },
|
|
14
|
+
{ value: "exit", label: "exit", description: "Leave Plan mode or clear a saved/active plan" },
|
|
15
|
+
{ value: "off", label: "off", description: "Leave Plan mode or clear a saved/active plan" },
|
|
16
|
+
{
|
|
17
|
+
value: "tools",
|
|
18
|
+
label: "tools",
|
|
19
|
+
description: "Choose tools before starting this Plan workflow",
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function completePlanArguments(argumentPrefix: string): CommandArgumentCompletion[] | null {
|
|
24
|
+
const prefix = argumentPrefix.trimStart().toLowerCase();
|
|
25
|
+
if (prefix === "") return [...PLAN_COMMAND_COMPLETIONS];
|
|
26
|
+
if (/\s/.test(prefix)) return null;
|
|
27
|
+
|
|
28
|
+
const matches = PLAN_COMMAND_COMPLETIONS.filter((item) => item.value.startsWith(prefix));
|
|
29
|
+
return matches.length > 0 ? [...matches] : null;
|
|
30
|
+
}
|