@narumitw/pi-plan-mode 0.1.24 → 0.1.26
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 +10 -4
- package/package.json +1 -1
- package/src/plan-mode.ts +237 -15
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@ Pi core intentionally does not ship a built-in plan mode; this package provides
|
|
|
10
10
|
|
|
11
11
|
- Adds `/plan` to enter or manage Plan mode.
|
|
12
12
|
- Adds `--plan` to start a session in Plan mode.
|
|
13
|
-
-
|
|
14
|
-
-
|
|
13
|
+
- Enables built-in read-only tools by default while Plan mode is active.
|
|
14
|
+
- Disables extension and custom tools by default, with a `/plan tools` selector for explicit user-risk opt-in.
|
|
15
|
+
- Blocks mutating built-in tools and bash commands such as `rm`, `git commit`, dependency installs, redirects, and editor launches.
|
|
15
16
|
- Injects Codex-like Plan mode instructions: explore first, ask only non-discoverable questions, do not mutate files, and finish with `<proposed_plan>`.
|
|
16
17
|
- Detects proposed plan blocks and prompts you to implement, revise, or stay in Plan mode.
|
|
17
18
|
- Shows Plan mode state in Pi's statusline as `📝 plan active` or `📝 plan ready`.
|
|
@@ -40,12 +41,17 @@ pi -e ./extensions/pi-plan-mode
|
|
|
40
41
|
```text
|
|
41
42
|
/plan
|
|
42
43
|
/plan <prompt>
|
|
44
|
+
/plan tools
|
|
43
45
|
```
|
|
44
46
|
|
|
45
|
-
Use `/plan` to enter Plan mode before writing your planning prompt. Use `/plan <prompt>` to enter Plan mode and immediately submit `<prompt>` as the first Plan-mode user message.
|
|
47
|
+
Use `/plan` to enter Plan mode before writing your planning prompt. Use `/plan <prompt>` to enter Plan mode and immediately submit `<prompt>` as the first Plan-mode user message. Use `/plan tools` to choose which tools are active while Plan mode is enabled; the selector is paginated at 10 tools per page.
|
|
46
48
|
|
|
47
49
|
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.
|
|
48
50
|
|
|
51
|
+
By default, Plan mode manages only Pi's built-in tools: `read`, limited `bash`, and available read-only built-ins such as `grep`, `find`, and `ls`. Built-in `edit` and `write` are blocked. Extension and custom tools are disabled by default because Pi tools do not expose standardized mutability metadata; enable them from `/plan tools` only when you accept the risk for that session. For example, you can opt into `firecrawl_scrape`, `firecrawl_search`, or `biome_lsp_diagnostics` if those extensions are loaded and you want to use them during planning.
|
|
52
|
+
|
|
53
|
+
Pi activates tools by tool name. The `/plan tools` selector stores selections by name and shows each currently effective tool's source from Pi metadata, such as `built-in`, a user extension path, 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.
|
|
54
|
+
|
|
49
55
|
A complete Plan mode answer should include exactly one block like this:
|
|
50
56
|
|
|
51
57
|
```xml
|
|
@@ -87,7 +93,7 @@ This extension maps Codex's `ModeKind::Plan` behavior onto Pi's extension API:
|
|
|
87
93
|
- `/plan <prompt>` follows Codex behavior by switching to Plan mode before submitting the inline prompt.
|
|
88
94
|
- `update_plan`-style checklist use is discouraged while Plan mode is active.
|
|
89
95
|
- The implementation boundary is explicit: Plan mode restores tools before starting implementation, and choosing implementation immediately triggers a normal agent turn with full tool access.
|
|
90
|
-
- Pi extension safety is approximated with
|
|
96
|
+
- Pi extension safety is approximated with built-in tool restriction plus bash filtering; non-built-in tools are user-selected at user risk because Plan mode does not classify extension/custom tool behavior.
|
|
91
97
|
|
|
92
98
|
## 🗂️ Package layout
|
|
93
99
|
|
package/package.json
CHANGED
package/src/plan-mode.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ToolInfo } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
const STATE_ENTRY_TYPE = "plan-mode-state";
|
|
4
4
|
const STATUS_KEY = "plan-mode";
|
|
5
5
|
const PLAN_WIDGET_KEY = "plan-mode-plan";
|
|
6
6
|
const PLAN_CONTEXT_MARKER = "[CODEX-LIKE PLAN MODE ACTIVE]";
|
|
7
|
-
const
|
|
7
|
+
const SAFE_BUILTIN_PLAN_TOOLS = new Set(["read", "bash", "grep", "find", "ls"]);
|
|
8
|
+
const BLOCKED_BUILTIN_TOOLS = new Set(["edit", "write"]);
|
|
8
9
|
const DEFAULT_TOOLS = ["read", "bash", "edit", "write"];
|
|
9
|
-
const
|
|
10
|
+
const TOOL_SELECTOR_PAGE_SIZE = 10;
|
|
10
11
|
const PROPOSED_PLAN_PATTERN = /<proposed_plan>\s*([\s\S]*?)\s*<\/proposed_plan>/i;
|
|
11
12
|
|
|
12
13
|
interface PlanModeState {
|
|
13
14
|
enabled: boolean;
|
|
14
15
|
latestPlan?: string;
|
|
15
16
|
awaitingAction: boolean;
|
|
17
|
+
selectedToolNames?: string[];
|
|
18
|
+
selectedToolKeys?: string[];
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
type SessionEntry = {
|
|
@@ -95,6 +98,11 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
95
98
|
ctx.ui.notify("Plan mode disabled. Full tool access restored.", "info");
|
|
96
99
|
return;
|
|
97
100
|
}
|
|
101
|
+
if (command === "tools") {
|
|
102
|
+
if (!state.enabled) enterPlanMode(ctx);
|
|
103
|
+
await showToolSelector(ctx);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
98
106
|
if (prompt) {
|
|
99
107
|
enterPlanModeWithPrompt(prompt, ctx);
|
|
100
108
|
return;
|
|
@@ -111,7 +119,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
111
119
|
pi.on("session_start", (_event, ctx) => {
|
|
112
120
|
restoreState(ctx);
|
|
113
121
|
if (pi.getFlag("plan") === true) state.enabled = true;
|
|
114
|
-
if (state.enabled)
|
|
122
|
+
if (state.enabled) activatePlanModeTools();
|
|
115
123
|
updateUi(ctx);
|
|
116
124
|
});
|
|
117
125
|
|
|
@@ -122,13 +130,13 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
122
130
|
|
|
123
131
|
pi.on("tool_call", async (event) => {
|
|
124
132
|
if (!state.enabled) return;
|
|
125
|
-
if (
|
|
133
|
+
if (isBlockedBuiltinToolName(event.toolName)) {
|
|
126
134
|
return {
|
|
127
135
|
block: true,
|
|
128
|
-
reason: `Plan mode blocks mutating tool '${event.toolName}'. Use /plan and choose implementation when the plan is ready.`,
|
|
136
|
+
reason: `Plan mode blocks built-in mutating tool '${event.toolName}'. Use /plan and choose implementation when the plan is ready.`,
|
|
129
137
|
};
|
|
130
138
|
}
|
|
131
|
-
if (event.toolName !== "bash") return;
|
|
139
|
+
if (event.toolName !== "bash" || !isBuiltinToolName(event.toolName)) return;
|
|
132
140
|
|
|
133
141
|
const command = readCommand(event.input);
|
|
134
142
|
if (!isSafeCommand(command)) {
|
|
@@ -148,6 +156,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
148
156
|
|
|
149
157
|
pi.on("before_agent_start", () => {
|
|
150
158
|
if (!state.enabled) return;
|
|
159
|
+
applyPlanModeTools();
|
|
151
160
|
return {
|
|
152
161
|
message: {
|
|
153
162
|
customType: "plan-mode-context",
|
|
@@ -186,7 +195,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
186
195
|
function enterPlanMode(ctx: ExtensionContext) {
|
|
187
196
|
if (!state.enabled) previousTools = safeGetActiveTools();
|
|
188
197
|
state = { ...state, enabled: true, awaitingAction: false };
|
|
189
|
-
|
|
198
|
+
activatePlanModeTools();
|
|
190
199
|
persistState();
|
|
191
200
|
updateUi(ctx);
|
|
192
201
|
}
|
|
@@ -234,8 +243,14 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
234
243
|
}
|
|
235
244
|
|
|
236
245
|
const choices = state.latestPlan
|
|
237
|
-
? [
|
|
238
|
-
|
|
246
|
+
? [
|
|
247
|
+
"Show latest proposed plan",
|
|
248
|
+
"Implement this plan",
|
|
249
|
+
"Configure Plan-mode tools",
|
|
250
|
+
"Stay in Plan mode",
|
|
251
|
+
"Exit Plan mode",
|
|
252
|
+
]
|
|
253
|
+
: ["Configure Plan-mode tools", "Stay in Plan mode", "Exit Plan mode"];
|
|
239
254
|
const choice = await ctx.ui.select(planStatusText(), choices);
|
|
240
255
|
if (choice === "Show latest proposed plan") {
|
|
241
256
|
ctx.ui.notify(state.latestPlan ?? "No proposed plan yet.", "info");
|
|
@@ -245,6 +260,10 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
245
260
|
startImplementation(ctx);
|
|
246
261
|
return;
|
|
247
262
|
}
|
|
263
|
+
if (choice === "Configure Plan-mode tools") {
|
|
264
|
+
await showToolSelector(ctx);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
248
267
|
if (choice === "Exit Plan mode") {
|
|
249
268
|
exitPlanMode(ctx);
|
|
250
269
|
ctx.ui.notify("Plan mode disabled. Full tool access restored.", "info");
|
|
@@ -257,21 +276,151 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
257
276
|
const choice = await ctx.ui.select("Proposed plan ready. What next?", [
|
|
258
277
|
"Implement this plan",
|
|
259
278
|
"Revise plan",
|
|
279
|
+
"Configure Plan-mode tools",
|
|
260
280
|
"Stay in Plan mode",
|
|
261
281
|
]);
|
|
262
282
|
if (choice === "Implement this plan") {
|
|
263
283
|
startImplementation(ctx);
|
|
264
284
|
return;
|
|
265
285
|
}
|
|
286
|
+
if (choice === "Configure Plan-mode tools") {
|
|
287
|
+
await showToolSelector(ctx);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
266
290
|
if (choice === "Revise plan") {
|
|
267
291
|
const refinement = await ctx.ui.editor("Revise the plan", "");
|
|
268
292
|
if (refinement?.trim()) pi.sendUserMessage(refinement.trim());
|
|
269
293
|
}
|
|
270
294
|
}
|
|
271
295
|
|
|
272
|
-
function
|
|
296
|
+
async function showToolSelector(ctx: ExtensionContext) {
|
|
297
|
+
if (!ctx.hasUI) {
|
|
298
|
+
ctx.ui.notify(formatToolSummary(), "info");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let pageIndex = 0;
|
|
303
|
+
while (true) {
|
|
304
|
+
const tools = selectableTools();
|
|
305
|
+
const pageCount = toolSelectorPageCount(tools);
|
|
306
|
+
pageIndex = Math.min(pageIndex, pageCount - 1);
|
|
307
|
+
const pageStart = pageIndex * TOOL_SELECTOR_PAGE_SIZE;
|
|
308
|
+
const pageTools = tools.slice(pageStart, pageStart + TOOL_SELECTOR_PAGE_SIZE);
|
|
309
|
+
const selectedNames = planModeSelectedNames(tools);
|
|
310
|
+
const choices = pageTools.map((tool, index) =>
|
|
311
|
+
formatToolChoice(tool, selectedNames.has(tool.name), pageStart + index),
|
|
312
|
+
);
|
|
313
|
+
const previousChoice = "Previous page";
|
|
314
|
+
const nextChoice = "Next page";
|
|
315
|
+
const doneChoice = "Done";
|
|
316
|
+
const navigationChoices = [
|
|
317
|
+
...(pageIndex > 0 ? [previousChoice] : []),
|
|
318
|
+
...(pageIndex < pageCount - 1 ? [nextChoice] : []),
|
|
319
|
+
doneChoice,
|
|
320
|
+
];
|
|
321
|
+
const choice = await ctx.ui.select(
|
|
322
|
+
`Plan-mode tools (${pageIndex + 1}/${pageCount}). Non-built-in tools run at user risk.`,
|
|
323
|
+
[...choices, ...navigationChoices],
|
|
324
|
+
);
|
|
325
|
+
if (!choice || choice === doneChoice) break;
|
|
326
|
+
if (choice === previousChoice) {
|
|
327
|
+
pageIndex = Math.max(0, pageIndex - 1);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (choice === nextChoice) {
|
|
331
|
+
pageIndex = Math.min(pageCount - 1, pageIndex + 1);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const selectedIndex = choices.indexOf(choice);
|
|
336
|
+
const tool = pageTools[selectedIndex];
|
|
337
|
+
if (!tool) continue;
|
|
338
|
+
if (!canSelectToolInPlanMode(tool)) {
|
|
339
|
+
ctx.ui.notify(`${tool.name} is blocked in Plan mode.`, "warning");
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const nextSelectedNames = planModeSelectedNames(tools);
|
|
344
|
+
if (nextSelectedNames.has(tool.name)) nextSelectedNames.delete(tool.name);
|
|
345
|
+
else nextSelectedNames.add(tool.name);
|
|
346
|
+
|
|
347
|
+
state = {
|
|
348
|
+
...state,
|
|
349
|
+
selectedToolNames: filterAvailableSelectedNames(Array.from(nextSelectedNames), tools),
|
|
350
|
+
};
|
|
351
|
+
applyPlanModeTools();
|
|
352
|
+
persistState();
|
|
353
|
+
updateUi(ctx);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
applyPlanModeTools();
|
|
357
|
+
persistState();
|
|
358
|
+
updateUi(ctx);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function activatePlanModeTools() {
|
|
273
362
|
previousTools ??= safeGetActiveTools();
|
|
274
|
-
|
|
363
|
+
applyPlanModeTools();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function applyPlanModeTools() {
|
|
367
|
+
pi.setActiveTools(planModeToolNames());
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function planModeToolNames() {
|
|
371
|
+
const tools = selectableTools();
|
|
372
|
+
if (tools.length === 0) return ["read", "bash"];
|
|
373
|
+
|
|
374
|
+
const selectedNames = planModeSelectedNames(tools);
|
|
375
|
+
return tools
|
|
376
|
+
.filter((tool) => selectedNames.has(tool.name) && canSelectToolInPlanMode(tool))
|
|
377
|
+
.map((tool) => tool.name);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function planModeSelectedNames(tools: ToolInfo[]) {
|
|
381
|
+
const selectedToolNames = state.selectedToolNames ?? migrateSelectedToolKeys(tools);
|
|
382
|
+
if (selectedToolNames === undefined) return new Set(defaultPlanModeToolNames(tools));
|
|
383
|
+
|
|
384
|
+
state = {
|
|
385
|
+
...state,
|
|
386
|
+
selectedToolNames: filterAvailableSelectedNames(selectedToolNames, tools),
|
|
387
|
+
selectedToolKeys: undefined,
|
|
388
|
+
};
|
|
389
|
+
return new Set(state.selectedToolNames);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function defaultPlanModeToolNames(tools: ToolInfo[]) {
|
|
393
|
+
return tools
|
|
394
|
+
.filter((tool) => isBuiltinTool(tool) && SAFE_BUILTIN_PLAN_TOOLS.has(tool.name))
|
|
395
|
+
.map((tool) => tool.name);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function migrateSelectedToolKeys(tools: ToolInfo[]) {
|
|
399
|
+
if (state.selectedToolKeys === undefined) return undefined;
|
|
400
|
+
return state.selectedToolKeys
|
|
401
|
+
.map((key) => toolNameFromLegacyKey(key, tools))
|
|
402
|
+
.filter((name): name is string => name !== undefined);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function filterAvailableSelectedNames(names: string[], tools: ToolInfo[]) {
|
|
406
|
+
const availableNames = new Set(tools.filter(canSelectToolInPlanMode).map((tool) => tool.name));
|
|
407
|
+
return unique(names.filter((name) => availableNames.has(name)));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function selectableTools() {
|
|
411
|
+
return safeGetAllTools().sort(compareTools);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function toolSelectorPageCount(tools: ToolInfo[]) {
|
|
415
|
+
return Math.max(1, Math.ceil(tools.length / TOOL_SELECTOR_PAGE_SIZE));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function safeGetAllTools() {
|
|
419
|
+
try {
|
|
420
|
+
return pi.getAllTools();
|
|
421
|
+
} catch {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
275
424
|
}
|
|
276
425
|
|
|
277
426
|
function restoreTools() {
|
|
@@ -301,6 +450,8 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
301
450
|
enabled: entry.data.enabled ?? false,
|
|
302
451
|
latestPlan: entry.data.latestPlan,
|
|
303
452
|
awaitingAction: entry.data.awaitingAction ?? false,
|
|
453
|
+
selectedToolNames: entry.data.selectedToolNames,
|
|
454
|
+
selectedToolKeys: entry.data.selectedToolKeys,
|
|
304
455
|
};
|
|
305
456
|
}
|
|
306
457
|
|
|
@@ -312,7 +463,11 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
312
463
|
"Use /plan to implement, revise, or exit Plan mode.",
|
|
313
464
|
]);
|
|
314
465
|
} else if (state.enabled) {
|
|
315
|
-
ctx.ui.setWidget(PLAN_WIDGET_KEY, [
|
|
466
|
+
ctx.ui.setWidget(PLAN_WIDGET_KEY, [
|
|
467
|
+
"Plan mode: planning",
|
|
468
|
+
formatToolSummary(),
|
|
469
|
+
"Produce a <proposed_plan> block.",
|
|
470
|
+
]);
|
|
316
471
|
} else {
|
|
317
472
|
ctx.ui.setWidget(PLAN_WIDGET_KEY, undefined);
|
|
318
473
|
}
|
|
@@ -331,9 +486,75 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
331
486
|
|
|
332
487
|
function planStatusText() {
|
|
333
488
|
if (!state.enabled) return "Plan mode is off.";
|
|
334
|
-
if (state.latestPlan) return
|
|
335
|
-
return
|
|
489
|
+
if (state.latestPlan) return `Plan mode is active and a proposed plan is ready. ${formatToolSummary()}`;
|
|
490
|
+
return `Plan mode is active. ${formatToolSummary()} Explore, ask, and produce a <proposed_plan> block.`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function formatToolSummary() {
|
|
494
|
+
const names = planModeToolNames();
|
|
495
|
+
return `Tools: ${names.length > 0 ? names.join(", ") : "none"}`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function isBlockedBuiltinToolName(toolName: string) {
|
|
499
|
+
if (!BLOCKED_BUILTIN_TOOLS.has(toolName)) return false;
|
|
500
|
+
const tool = toolByName(toolName);
|
|
501
|
+
return tool ? isBuiltinTool(tool) : true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function isBuiltinToolName(toolName: string) {
|
|
505
|
+
const tool = toolByName(toolName);
|
|
506
|
+
return tool ? isBuiltinTool(tool) : toolName === "bash";
|
|
336
507
|
}
|
|
508
|
+
|
|
509
|
+
function toolByName(toolName: string) {
|
|
510
|
+
return safeGetAllTools().find((candidate) => candidate.name === toolName);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function isBuiltinTool(tool: ToolInfo) {
|
|
515
|
+
return tool.sourceInfo.source === "builtin";
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function canSelectToolInPlanMode(tool: ToolInfo) {
|
|
519
|
+
if (isBuiltinTool(tool)) return SAFE_BUILTIN_PLAN_TOOLS.has(tool.name);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function toolNameFromLegacyKey(key: string, tools: ToolInfo[]) {
|
|
524
|
+
const directName = tools.find((tool) => tool.name === key)?.name;
|
|
525
|
+
if (directName) return directName;
|
|
526
|
+
const [name] = key.split("\u001f");
|
|
527
|
+
return tools.find((tool) => tool.name === name) ? name : undefined;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function compareTools(left: ToolInfo, right: ToolInfo) {
|
|
531
|
+
const leftBuiltin = isBuiltinTool(left);
|
|
532
|
+
const rightBuiltin = isBuiltinTool(right);
|
|
533
|
+
if (leftBuiltin !== rightBuiltin) return leftBuiltin ? -1 : 1;
|
|
534
|
+
return left.name.localeCompare(right.name);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function formatToolChoice(tool: ToolInfo, selected: boolean, index: number) {
|
|
538
|
+
const marker = selected ? "[x]" : "[ ]";
|
|
539
|
+
return `${marker} ${index + 1}. ${tool.name} (${toolPolicyLabel(tool)})`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function toolPolicyLabel(tool: ToolInfo) {
|
|
543
|
+
if (isBuiltinTool(tool)) {
|
|
544
|
+
if (!SAFE_BUILTIN_PLAN_TOOLS.has(tool.name)) return "built-in blocked";
|
|
545
|
+
return tool.name === "bash" ? "built-in limited" : "built-in";
|
|
546
|
+
}
|
|
547
|
+
return `user risk: ${toolSourceLabel(tool)}`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function toolSourceLabel(tool: ToolInfo) {
|
|
551
|
+
const sourceInfo = tool.sourceInfo;
|
|
552
|
+
const source = `${sourceInfo.scope}/${sourceInfo.source}`;
|
|
553
|
+
return sourceInfo.path ? `${source} ${sourceInfo.path}` : source;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function unique(values: string[]) {
|
|
557
|
+
return Array.from(new Set(values));
|
|
337
558
|
}
|
|
338
559
|
|
|
339
560
|
function buildPlanModePrompt() {
|
|
@@ -346,6 +567,7 @@ Mode rules:
|
|
|
346
567
|
- Use non-mutating exploration first: read files, search, inspect configuration, run read-only checks, and resolve discoverable facts before asking the user.
|
|
347
568
|
- Ask the user only for preferences or tradeoffs that cannot be discovered from the repository.
|
|
348
569
|
- Do not use update_plan/TODO tooling in Plan Mode; Plan Mode is conversational planning, not execution progress tracking.
|
|
570
|
+
- Plan Mode manages built-in tool safety only. Non-built-in tools are disabled by default and may be enabled by the user at their own risk.
|
|
349
571
|
- Do not perform mutating actions: no edit/write tools, no patching, no formatting that rewrites files, no dependency installation, no commits, no migrations.
|
|
350
572
|
- When the plan is decision-complete, output exactly one proposed plan block using:
|
|
351
573
|
<proposed_plan>
|