@hank-warren/pi-plan-mode 0.1.0 → 1.0.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/NOTICE.md +4 -0
- package/README.md +60 -246
- package/package.json +3 -4
- package/src/active-implementation-menu.ts +4 -3
- package/src/command.ts +4 -10
- package/src/completion-tool.ts +3 -1
- package/src/fresh-implementation.ts +19 -26
- package/src/interactive-ui.ts +0 -1
- package/src/plan-action-controller.ts +5 -29
- package/src/plan-action-menus.ts +16 -23
- package/src/plan-export.ts +9 -7
- package/src/plan-file.ts +85 -0
- package/src/plan-launch-menu.ts +18 -78
- package/src/plan-mode.ts +126 -505
- package/src/presentation.ts +21 -40
- package/src/prompt.ts +15 -12
- package/src/settings-menu.ts +4 -181
- package/src/settings.ts +6 -105
- package/src/state.ts +22 -118
- package/src/auto-permissions-delegation.ts +0 -122
- package/src/implementation-retention.ts +0 -122
- package/src/message-transform.ts +0 -232
- package/src/required-tools.ts +0 -22
- package/src/saved-plan-menu.ts +0 -93
- package/src/saved-plan-preflight.ts +0 -39
- package/src/tool-policy.ts +0 -563
- package/src/tool-selection.ts +0 -98
package/src/presentation.ts
CHANGED
|
@@ -1,32 +1,23 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { readPlanFile } from "./plan-file.js";
|
|
2
3
|
import type { PlanModeState } from "./state.js";
|
|
3
4
|
|
|
4
5
|
const STATUS_KEY = "plan-mode";
|
|
5
6
|
const PLAN_WIDGET_KEY = "plan-mode-plan";
|
|
6
7
|
|
|
7
|
-
export function updatePlanModeUi(
|
|
8
|
-
ctx: ExtensionContext,
|
|
9
|
-
state: PlanModeState,
|
|
10
|
-
toolSummary: () => string,
|
|
11
|
-
) {
|
|
8
|
+
export function updatePlanModeUi(ctx: ExtensionContext, state: PlanModeState) {
|
|
12
9
|
ctx.ui.setStatus(STATUS_KEY, formatStatus(state));
|
|
13
|
-
if (state.enabled && state.
|
|
10
|
+
if (state.enabled && state.awaitingAction) {
|
|
14
11
|
ctx.ui.setWidget(PLAN_WIDGET_KEY, [
|
|
15
12
|
"Proposed plan ready",
|
|
16
|
-
"Use /plan to implement,
|
|
13
|
+
"Use /plan to implement, export, revise, or exit Plan mode.",
|
|
17
14
|
]);
|
|
18
15
|
} else if (state.enabled) {
|
|
19
16
|
ctx.ui.setWidget(PLAN_WIDGET_KEY, [
|
|
20
17
|
"Plan mode: planning",
|
|
21
|
-
toolSummary(),
|
|
22
18
|
"Finish with plan_mode_complete when decision-ready.",
|
|
23
19
|
]);
|
|
24
|
-
} else if (state.
|
|
25
|
-
ctx.ui.setWidget(PLAN_WIDGET_KEY, [
|
|
26
|
-
"Plan saved for later",
|
|
27
|
-
"Use /plan to show, implement, or clear it.",
|
|
28
|
-
]);
|
|
29
|
-
} else if (state.activeImplementation) {
|
|
20
|
+
} else if (state.planPath) {
|
|
30
21
|
ctx.ui.setWidget(PLAN_WIDGET_KEY, [
|
|
31
22
|
"Implementation plan active",
|
|
32
23
|
"Use /plan to show, replace, or clear it.",
|
|
@@ -41,14 +32,15 @@ export function clearPlanModeUi(ctx: ExtensionContext) {
|
|
|
41
32
|
ctx.ui.setWidget(PLAN_WIDGET_KEY, undefined);
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Always reads the file so a hand-edited plan is what the user sees.
|
|
37
|
+
*/
|
|
38
|
+
export async function showStoredPlan(
|
|
39
|
+
pi: ExtensionAPI,
|
|
40
|
+
ctx: ExtensionContext,
|
|
41
|
+
state: PlanModeState,
|
|
42
|
+
) {
|
|
43
|
+
const plan = state.planPath ? await readPlanFile(state.planPath) : undefined;
|
|
52
44
|
if (!plan) {
|
|
53
45
|
ctx.ui.notify(
|
|
54
46
|
"No completed plan is available. Use /plan finalize when planning is complete.",
|
|
@@ -56,11 +48,7 @@ export function showStoredPlan(pi: ExtensionAPI, ctx: ExtensionContext, state: P
|
|
|
56
48
|
);
|
|
57
49
|
return;
|
|
58
50
|
}
|
|
59
|
-
const title =
|
|
60
|
-
? "Proposed Plan"
|
|
61
|
-
: savedPlan
|
|
62
|
-
? "Saved Plan"
|
|
63
|
-
: "Active Implementation Plan";
|
|
51
|
+
const title = state.enabled ? "Proposed Plan" : "Active Implementation Plan";
|
|
64
52
|
showPlanModePlan(pi, ctx, title, plan);
|
|
65
53
|
}
|
|
66
54
|
|
|
@@ -85,24 +73,17 @@ export function showPlanModePlan(
|
|
|
85
73
|
}
|
|
86
74
|
}
|
|
87
75
|
|
|
88
|
-
export function planModeStatusText(state: PlanModeState
|
|
76
|
+
export function planModeStatusText(state: PlanModeState) {
|
|
89
77
|
if (state.enabled) {
|
|
90
|
-
if (state.
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
return `Plan mode is active. ${toolSummary()} Explore, ask, and finish with plan_mode_complete when decision-ready.`;
|
|
78
|
+
if (state.awaitingAction) return "Plan mode is active and a proposed plan is ready.";
|
|
79
|
+
return "Plan mode is active. Explore, ask, and finish with plan_mode_complete when decision-ready.";
|
|
94
80
|
}
|
|
95
|
-
if (state.
|
|
96
|
-
if (state.activeImplementation) return "An implementation plan is active.";
|
|
81
|
+
if (state.planPath) return "An implementation plan is active.";
|
|
97
82
|
return "Plan mode is off.";
|
|
98
83
|
}
|
|
99
84
|
|
|
100
85
|
function formatStatus(state: PlanModeState) {
|
|
101
|
-
if (state.enabled)
|
|
102
|
-
|
|
103
|
-
return "plan active";
|
|
104
|
-
}
|
|
105
|
-
if (state.savedPlan) return "plan saved";
|
|
106
|
-
if (state.activeImplementation) return "plan implementing";
|
|
86
|
+
if (state.enabled) return state.awaitingAction ? "plan ready" : "plan active";
|
|
87
|
+
if (state.planPath) return "plan implementing";
|
|
107
88
|
return undefined;
|
|
108
89
|
}
|
package/src/prompt.ts
CHANGED
|
@@ -1,26 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
const PLAN_CONTEXT_MARKER = "[PLAN MODE ACTIVE]";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export function buildPlanModePrompt(options: { bashPolicy?: PlanModeBashPolicy } = {}) {
|
|
6
|
-
const bashPolicy = options.bashPolicy ?? "limited";
|
|
7
|
-
const bashRule =
|
|
8
|
-
bashPolicy === "auto-permissions"
|
|
9
|
-
? "- Bash calls outside Plan Mode's inspection allowlist may be delegated to Auto Permissions when a matching guarded rule is active. Its approval permits information gathering, not implementation: keep every Bash action non-mutating and within the planning request."
|
|
10
|
-
: "- Bash is limited by Plan Mode's reviewed inspection policy. Use only accepted non-mutating inspection commands.";
|
|
3
|
+
export function buildPlanModePrompt() {
|
|
11
4
|
return `${PLAN_CONTEXT_MARKER}
|
|
12
5
|
# Plan Mode (Conversational)
|
|
13
6
|
|
|
14
|
-
You are in Plan Mode, a
|
|
7
|
+
You are in Plan Mode, a collaboration mode for producing a decision-complete implementation plan. Chat your way to the plan before finalizing it. A final plan must leave no implementation decisions unresolved.
|
|
15
8
|
|
|
16
9
|
## Mode rules
|
|
17
10
|
|
|
18
11
|
- Stay in Plan Mode until a developer or extension explicitly exits it.
|
|
19
12
|
- Treat requests to implement as requests to plan the implementation; do not edit files or carry out the plan.
|
|
20
13
|
- Do not use update_plan/TODO tooling in Plan Mode; Plan Mode is conversational planning, not execution progress tracking.
|
|
21
|
-
- 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.
|
|
22
|
-
${bashRule}
|
|
23
14
|
- Do not perform mutating actions: no edit/write tools, no patching, no formatting that rewrites files, no dependency installation, no commits, no migrations.
|
|
15
|
+
- Gather information freely: read files, search, inspect configuration, and run read-only commands.
|
|
24
16
|
|
|
25
17
|
## Phase 1 — Ground in the environment
|
|
26
18
|
|
|
@@ -63,5 +55,16 @@ Only call plan_mode_complete when the plan leaves no implementation decisions un
|
|
|
63
55
|
|
|
64
56
|
Keep the plan concise, human and agent digestible, and free of open decisions. Prefer grouped behavior-level changes over file-by-file or symbol-by-symbol inventories. Do not ask "should I proceed?"; plan_mode_complete opens the Plan-mode ready flow.
|
|
65
57
|
|
|
58
|
+
The plan is saved to a durable file, so it survives compaction and can be re-read at any time.
|
|
59
|
+
|
|
66
60
|
If the user requests revisions after a completed plan, the next plan_mode_complete call must contain a complete replacement, not a delta. If there is not enough information for a complete replacement, continue planning with plan_mode_question instead of calling plan_mode_complete.`;
|
|
67
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The plan is never injected into context as a payload. While a plan is active
|
|
65
|
+
* the model gets only this pointer and reads the file on demand, which keeps
|
|
66
|
+
* compaction survival to a single line regardless of plan size.
|
|
67
|
+
*/
|
|
68
|
+
export function buildActivePlanPointer(planPath: string) {
|
|
69
|
+
return `[PLAN MODE] An approved implementation plan for this session is stored at ${planPath}. Read that file before implementing, and re-read it if you need the plan again after compaction. The file is the source of truth and the user may have edited it.`;
|
|
70
|
+
}
|
package/src/settings-menu.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import type { ExtensionContext
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { defineMenu, type RunMenuResult, runMenu } from "@narumitw/pi-tui-kit";
|
|
3
|
-
import { PLAN_MODE_COMPLETE_TOOL_NAME } from "./completion-tool.js";
|
|
4
|
-
import { retentionLabel } from "./implementation-retention.js";
|
|
5
3
|
import { planExportDestination } from "./plan-export.js";
|
|
6
|
-
import { PLAN_MODE_QUESTION_TOOL_NAME } from "./question-tool.js";
|
|
7
4
|
import {
|
|
8
|
-
configuredBashPolicy,
|
|
9
|
-
configuredImplementationPlanRetention,
|
|
10
5
|
configuredPlanExportPath,
|
|
11
|
-
IMPLEMENTATION_PLAN_RETENTIONS,
|
|
12
|
-
PLAN_MODE_BASH_POLICIES,
|
|
13
6
|
PLAN_MODE_THINKING_LEVELS,
|
|
14
|
-
type PlanModeBashPolicy,
|
|
15
7
|
type PlanModeSettings,
|
|
16
8
|
type PlanModeSettingsLoadResult,
|
|
17
9
|
type PlanModeSettingsPatch,
|
|
@@ -20,8 +12,6 @@ import {
|
|
|
20
12
|
type UpdatePlanModeSettingsOptions,
|
|
21
13
|
updatePlanModeSettings,
|
|
22
14
|
} from "./settings.js";
|
|
23
|
-
import { canSelectToolInPlanMode } from "./tool-policy.js";
|
|
24
|
-
import { defaultPlanModeToolNames, toolPolicyLabel } from "./tool-selection.js";
|
|
25
15
|
|
|
26
16
|
interface SettingsMenuState {
|
|
27
17
|
kind: "valid" | "invalid";
|
|
@@ -31,7 +21,6 @@ interface SettingsMenuState {
|
|
|
31
21
|
}
|
|
32
22
|
|
|
33
23
|
export interface PlanModeSettingsMenuOptions {
|
|
34
|
-
tools: readonly ToolInfo[];
|
|
35
24
|
signal: AbortSignal;
|
|
36
25
|
isCurrent(): boolean;
|
|
37
26
|
settingsPath?: string;
|
|
@@ -44,16 +33,8 @@ export interface PlanModeSettingsMenuOptions {
|
|
|
44
33
|
onSaved(settings: PlanModeSettings): void;
|
|
45
34
|
}
|
|
46
35
|
|
|
47
|
-
type Screen = "settings" | "
|
|
48
|
-
type Action =
|
|
49
|
-
| "set-thinking"
|
|
50
|
-
| "open-tools"
|
|
51
|
-
| "toggle-tool"
|
|
52
|
-
| "reset-tools"
|
|
53
|
-
| "set-bash-policy"
|
|
54
|
-
| "set-retention"
|
|
55
|
-
| "open-export"
|
|
56
|
-
| "set-export";
|
|
36
|
+
type Screen = "settings" | "export";
|
|
37
|
+
type Action = "set-thinking" | "open-export" | "set-export";
|
|
57
38
|
|
|
58
39
|
export async function showPlanModeSettings(
|
|
59
40
|
ctx: ExtensionContext,
|
|
@@ -62,10 +43,6 @@ export async function showPlanModeSettings(
|
|
|
62
43
|
const settingsPath = options.settingsPath ?? planModeSettingsPath();
|
|
63
44
|
const readSettings = options.readSettings ?? readPlanModeSettings;
|
|
64
45
|
const updateSettings = options.updateSettings ?? updatePlanModeSettings;
|
|
65
|
-
const tools = options.tools.filter(
|
|
66
|
-
(tool) =>
|
|
67
|
-
tool.name !== PLAN_MODE_QUESTION_TOOL_NAME && tool.name !== PLAN_MODE_COMPLETE_TOOL_NAME,
|
|
68
|
-
);
|
|
69
46
|
|
|
70
47
|
const loadState = async (): Promise<SettingsMenuState> => {
|
|
71
48
|
const loaded = await readSettings(options.settingsPath);
|
|
@@ -103,33 +80,6 @@ export async function showPlanModeSettings(
|
|
|
103
80
|
values: PLAN_MODE_THINKING_LEVELS,
|
|
104
81
|
action: "set-thinking",
|
|
105
82
|
},
|
|
106
|
-
{
|
|
107
|
-
id: "defaultPlanTools",
|
|
108
|
-
label: "Plan tools",
|
|
109
|
-
description:
|
|
110
|
-
"Choose persistent defaults; a launch-time selection still overrides them for that session.",
|
|
111
|
-
currentValue: defaultToolsValue(state.settings.defaultPlanTools),
|
|
112
|
-
action: "open-tools",
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
id: "bashPolicy",
|
|
116
|
-
label: "Bash policy",
|
|
117
|
-
description:
|
|
118
|
-
"Use Plan mode's allowlist or selectively defer matching guarded commands to Auto Permissions.",
|
|
119
|
-
currentValue: bashPolicyLabel(configuredBashPolicy(state.settings)),
|
|
120
|
-
values: PLAN_MODE_BASH_POLICIES.map(bashPolicyLabel),
|
|
121
|
-
action: "set-bash-policy",
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
id: "implementationPlanRetention",
|
|
125
|
-
label: "After Implement",
|
|
126
|
-
description: "Choose how long the accepted plan keeps guiding implementation.",
|
|
127
|
-
currentValue: retentionLabel(
|
|
128
|
-
configuredImplementationPlanRetention(state.settings),
|
|
129
|
-
),
|
|
130
|
-
values: IMPLEMENTATION_PLAN_RETENTIONS.map(retentionLabel),
|
|
131
|
-
action: "set-retention",
|
|
132
|
-
},
|
|
133
83
|
{
|
|
134
84
|
id: "defaultPlanExportPath",
|
|
135
85
|
label: "Export destination",
|
|
@@ -139,26 +89,6 @@ export async function showPlanModeSettings(
|
|
|
139
89
|
},
|
|
140
90
|
],
|
|
141
91
|
},
|
|
142
|
-
tools: ({ state }) => ({
|
|
143
|
-
kind: "multiSelect",
|
|
144
|
-
title: "Default Plan-mode tools",
|
|
145
|
-
lines: [
|
|
146
|
-
"Changes apply when a later Plan workflow starts; required Plan tools stay enabled.",
|
|
147
|
-
"Non-built-in tools run at user risk.",
|
|
148
|
-
],
|
|
149
|
-
enableSearch: true,
|
|
150
|
-
viewportSize: 10,
|
|
151
|
-
items: defaultToolItems(tools, state.settings.defaultPlanTools),
|
|
152
|
-
action: "toggle-tool",
|
|
153
|
-
actions: [
|
|
154
|
-
{
|
|
155
|
-
id: "reset-tools",
|
|
156
|
-
label: "Use automatic safe built-ins",
|
|
157
|
-
action: "reset-tools",
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
hint: "back",
|
|
161
|
-
}),
|
|
162
92
|
export: ({ state }) => {
|
|
163
93
|
const configured = configuredPlanExportPath(state.settings);
|
|
164
94
|
const destination = planExportDestination(configured, ctx.cwd);
|
|
@@ -190,29 +120,6 @@ export async function showPlanModeSettings(
|
|
|
190
120
|
`Plan mode thinking level: ${value}. Applies to the next Plan workflow.`,
|
|
191
121
|
);
|
|
192
122
|
},
|
|
193
|
-
"open-tools": async () => ({ kind: "to", screen: "tools" }),
|
|
194
|
-
"set-bash-policy": async ({ ctx: actionCtx, value, signal }) => {
|
|
195
|
-
const bashPolicy = bashPolicyFromLabel(value);
|
|
196
|
-
if (!bashPolicy) return { kind: "rejected" };
|
|
197
|
-
return savePatch(
|
|
198
|
-
actionCtx,
|
|
199
|
-
{ bashPolicy },
|
|
200
|
-
signal,
|
|
201
|
-
bashPolicy === "auto-permissions"
|
|
202
|
-
? "Bash policy: selectively delegated to Auto Permissions guarded rules."
|
|
203
|
-
: "Bash policy: Plan-mode inspection allowlist.",
|
|
204
|
-
);
|
|
205
|
-
},
|
|
206
|
-
"set-retention": async ({ ctx: actionCtx, value, signal }) => {
|
|
207
|
-
const implementationPlanRetention = retentionFromLabel(value);
|
|
208
|
-
if (!implementationPlanRetention) return { kind: "rejected" };
|
|
209
|
-
return savePatch(
|
|
210
|
-
actionCtx,
|
|
211
|
-
{ implementationPlanRetention },
|
|
212
|
-
signal,
|
|
213
|
-
`After Implement: ${retentionLabel(implementationPlanRetention)}. Applies to the next Implement action.`,
|
|
214
|
-
);
|
|
215
|
-
},
|
|
216
123
|
"open-export": async () => ({ kind: "to", screen: "export" }),
|
|
217
124
|
"set-export": async ({ ctx: actionCtx, value, signal }) => {
|
|
218
125
|
const defaultPlanExportPath = value?.trim() || null;
|
|
@@ -226,29 +133,6 @@ export async function showPlanModeSettings(
|
|
|
226
133
|
);
|
|
227
134
|
return result.kind === "stay" ? { kind: "to", screen: "settings" } : result;
|
|
228
135
|
},
|
|
229
|
-
"toggle-tool": async ({ ctx: actionCtx, state, itemId, selected, signal }) => {
|
|
230
|
-
const tool = tools.find((candidate) => candidate.name === itemId);
|
|
231
|
-
if (!tool || !canSelectToolInPlanMode(tool)) return { kind: "rejected" };
|
|
232
|
-
const names = explicitToolNames(tools, state.settings.defaultPlanTools);
|
|
233
|
-
const next = selected
|
|
234
|
-
? Array.from(new Set([...names, tool.name]))
|
|
235
|
-
: names.filter((name) => name !== tool.name);
|
|
236
|
-
return savePatch(
|
|
237
|
-
actionCtx,
|
|
238
|
-
{ defaultPlanTools: next },
|
|
239
|
-
signal,
|
|
240
|
-
`Default Plan-mode tools: ${next.length === 0 ? "required tools only" : `${next.length} selected`}.`,
|
|
241
|
-
);
|
|
242
|
-
},
|
|
243
|
-
"reset-tools": async ({ ctx: actionCtx, state, signal }) => {
|
|
244
|
-
if (state.settings.defaultPlanTools === undefined) return { kind: "stay" };
|
|
245
|
-
return savePatch(
|
|
246
|
-
actionCtx,
|
|
247
|
-
{ defaultPlanTools: null },
|
|
248
|
-
signal,
|
|
249
|
-
"Default Plan-mode tools: automatic safe built-ins.",
|
|
250
|
-
);
|
|
251
|
-
},
|
|
252
136
|
},
|
|
253
137
|
});
|
|
254
138
|
|
|
@@ -290,7 +174,7 @@ export async function showPlanModeSettings(
|
|
|
290
174
|
function settingsLines(settingsPath: string, notice: string | undefined) {
|
|
291
175
|
return [
|
|
292
176
|
`User settings · ${safeTerminalText(settingsPath)}`,
|
|
293
|
-
"Plan
|
|
177
|
+
"Plan thinking applies to the next workflow; the export destination applies to its next action.",
|
|
294
178
|
...(notice ? [safeTerminalText(notice)] : []),
|
|
295
179
|
];
|
|
296
180
|
}
|
|
@@ -308,67 +192,6 @@ function invalidScreen(settingsPath: string, state: SettingsMenuState) {
|
|
|
308
192
|
};
|
|
309
193
|
}
|
|
310
194
|
|
|
311
|
-
const BASH_POLICY_LABELS: Record<PlanModeBashPolicy, string> = {
|
|
312
|
-
limited: "Plan-mode allowlist",
|
|
313
|
-
"auto-permissions": "Auto Permissions guarded rules",
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
function bashPolicyLabel(policy: PlanModeBashPolicy) {
|
|
317
|
-
return BASH_POLICY_LABELS[policy];
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function bashPolicyFromLabel(value: string | undefined) {
|
|
321
|
-
return PLAN_MODE_BASH_POLICIES.find((policy) => bashPolicyLabel(policy) === value);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function retentionFromLabel(value: string | undefined) {
|
|
325
|
-
return IMPLEMENTATION_PLAN_RETENTIONS.find((retention) => retentionLabel(retention) === value);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function defaultToolsValue(configured: string[] | undefined) {
|
|
329
|
-
if (configured === undefined) return "Automatic safe built-ins";
|
|
330
|
-
if (configured.length === 0) return "Required tools only";
|
|
331
|
-
return `${configured.length} selected`;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function defaultToolItems(tools: readonly ToolInfo[], configured: string[] | undefined) {
|
|
335
|
-
const selected = new Set(explicitToolNames(tools, configured));
|
|
336
|
-
const availableNames = new Set(tools.map((tool) => tool.name));
|
|
337
|
-
const items = tools.map((tool) => {
|
|
338
|
-
const selectable = canSelectToolInPlanMode(tool);
|
|
339
|
-
const policy = toolPolicyLabel(tool);
|
|
340
|
-
const description = tool.description ?? "No description available";
|
|
341
|
-
return {
|
|
342
|
-
id: tool.name,
|
|
343
|
-
label: tool.name,
|
|
344
|
-
description: `${policy} · ${description}`,
|
|
345
|
-
searchText: `${policy} ${description}`,
|
|
346
|
-
selected: selected.has(tool.name),
|
|
347
|
-
disabled: !selectable,
|
|
348
|
-
disabledReason: selectable ? undefined : "Blocked by Plan-mode policy",
|
|
349
|
-
};
|
|
350
|
-
});
|
|
351
|
-
for (const name of configured ?? []) {
|
|
352
|
-
if (availableNames.has(name)) continue;
|
|
353
|
-
items.push({
|
|
354
|
-
id: name,
|
|
355
|
-
label: name,
|
|
356
|
-
description: "unavailable · Retained in settings but unavailable in this session",
|
|
357
|
-
searchText: "unavailable retained settings",
|
|
358
|
-
selected: true,
|
|
359
|
-
disabled: true,
|
|
360
|
-
disabledReason: "Unavailable in this session; reset defaults to remove unavailable names",
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
return items;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function explicitToolNames(tools: readonly ToolInfo[], configured: string[] | undefined) {
|
|
367
|
-
return configured === undefined
|
|
368
|
-
? defaultPlanModeToolNames([...tools], undefined)
|
|
369
|
-
: [...configured];
|
|
370
|
-
}
|
|
371
|
-
|
|
372
195
|
function safeTerminalText(value: string) {
|
|
373
196
|
return [...value]
|
|
374
197
|
.map((character) => {
|
package/src/settings.ts
CHANGED
|
@@ -3,13 +3,6 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { mkdir, open, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
SAFE_GH_SUBCOMMAND_PATHS,
|
|
8
|
-
SAFE_GIT_SUBCOMMANDS,
|
|
9
|
-
type SafeGhSubcommandPath,
|
|
10
|
-
type SafeGitSubcommand,
|
|
11
|
-
type SafeSubcommands,
|
|
12
|
-
} from "./tool-policy.js";
|
|
13
6
|
|
|
14
7
|
export const PLAN_MODE_SETTINGS_FILE = "pi-plan-mode.json";
|
|
15
8
|
const LEGACY_PLAN_MODE_SETTINGS_FILE = "plan-mode.json";
|
|
@@ -24,33 +17,18 @@ export const PLAN_MODE_THINKING_LEVELS = [
|
|
|
24
17
|
"xhigh",
|
|
25
18
|
"max",
|
|
26
19
|
] as const;
|
|
27
|
-
export const IMPLEMENTATION_PLAN_RETENTIONS = [
|
|
28
|
-
"keep",
|
|
29
|
-
"clear-on-start",
|
|
30
|
-
"clear-after-first-run",
|
|
31
|
-
] as const;
|
|
32
|
-
export const PLAN_MODE_BASH_POLICIES = ["limited", "auto-permissions"] as const;
|
|
33
20
|
export const DEFAULT_PLAN_EXPORT_PATH = "PLAN.md";
|
|
34
21
|
const MAX_PLAN_EXPORT_PATH_LENGTH = 4096;
|
|
35
22
|
|
|
36
23
|
export type PlanModeThinkingLevel = (typeof PLAN_MODE_THINKING_LEVELS)[number];
|
|
37
|
-
export type ImplementationPlanRetention = (typeof IMPLEMENTATION_PLAN_RETENTIONS)[number];
|
|
38
|
-
export type PlanModeBashPolicy = (typeof PLAN_MODE_BASH_POLICIES)[number];
|
|
39
24
|
export type PlanModeFixedThinkingLevel = Exclude<PlanModeThinkingLevel, "inherit">;
|
|
40
25
|
export interface PlanModeSettings {
|
|
41
26
|
thinkingLevel: PlanModeThinkingLevel;
|
|
42
|
-
defaultPlanTools?: string[];
|
|
43
|
-
implementationPlanRetention?: ImplementationPlanRetention;
|
|
44
27
|
defaultPlanExportPath?: string;
|
|
45
|
-
bashPolicy?: PlanModeBashPolicy;
|
|
46
|
-
safeSubcommands?: SafeSubcommands;
|
|
47
28
|
}
|
|
48
29
|
export interface PlanModeSettingsPatch {
|
|
49
30
|
thinkingLevel?: PlanModeThinkingLevel;
|
|
50
|
-
defaultPlanTools?: readonly string[] | null;
|
|
51
|
-
implementationPlanRetention?: ImplementationPlanRetention;
|
|
52
31
|
defaultPlanExportPath?: string | null;
|
|
53
|
-
bashPolicy?: PlanModeBashPolicy;
|
|
54
32
|
}
|
|
55
33
|
export interface UpdatePlanModeSettingsOptions {
|
|
56
34
|
settingsPath?: string;
|
|
@@ -79,6 +57,12 @@ function legacyPlanModeSettingsPath() {
|
|
|
79
57
|
return join(getAgentDir(), LEGACY_PLAN_MODE_SETTINGS_FILE);
|
|
80
58
|
}
|
|
81
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Unknown top-level keys are tolerated and preserved on save. Settings removed
|
|
62
|
+
* in the plan-file rewrite (defaultPlanTools, bashPolicy, safeSubcommands,
|
|
63
|
+
* implementationPlanRetention) therefore keep an existing file valid instead of
|
|
64
|
+
* failing it closed on upgrade.
|
|
65
|
+
*/
|
|
82
66
|
export function normalizePlanModeSettings(value: unknown): PlanModeSettings | undefined {
|
|
83
67
|
if (!isSettingsDocument(value)) return undefined;
|
|
84
68
|
const thinkingLevel = Object.hasOwn(value, "thinkingLevel")
|
|
@@ -90,23 +74,6 @@ export function normalizePlanModeSettings(value: unknown): PlanModeSettings | un
|
|
|
90
74
|
const settings: PlanModeSettings = {
|
|
91
75
|
thinkingLevel: thinkingLevel as PlanModeThinkingLevel,
|
|
92
76
|
};
|
|
93
|
-
if (Object.hasOwn(value, "defaultPlanTools")) {
|
|
94
|
-
const defaultPlanTools = normalizeToolNames(Reflect.get(value, "defaultPlanTools"));
|
|
95
|
-
if (!defaultPlanTools) return undefined;
|
|
96
|
-
settings.defaultPlanTools = defaultPlanTools;
|
|
97
|
-
}
|
|
98
|
-
if (Object.hasOwn(value, "implementationPlanRetention")) {
|
|
99
|
-
const implementationPlanRetention = Reflect.get(value, "implementationPlanRetention");
|
|
100
|
-
if (
|
|
101
|
-
!IMPLEMENTATION_PLAN_RETENTIONS.includes(
|
|
102
|
-
implementationPlanRetention as ImplementationPlanRetention,
|
|
103
|
-
)
|
|
104
|
-
) {
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
settings.implementationPlanRetention =
|
|
108
|
-
implementationPlanRetention as ImplementationPlanRetention;
|
|
109
|
-
}
|
|
110
77
|
if (Object.hasOwn(value, "defaultPlanExportPath")) {
|
|
111
78
|
const defaultPlanExportPath = normalizePlanExportPath(
|
|
112
79
|
Reflect.get(value, "defaultPlanExportPath"),
|
|
@@ -114,29 +81,9 @@ export function normalizePlanModeSettings(value: unknown): PlanModeSettings | un
|
|
|
114
81
|
if (!defaultPlanExportPath) return undefined;
|
|
115
82
|
settings.defaultPlanExportPath = defaultPlanExportPath;
|
|
116
83
|
}
|
|
117
|
-
if (Object.hasOwn(value, "bashPolicy")) {
|
|
118
|
-
const bashPolicy = Reflect.get(value, "bashPolicy");
|
|
119
|
-
if (!PLAN_MODE_BASH_POLICIES.includes(bashPolicy as PlanModeBashPolicy)) return undefined;
|
|
120
|
-
settings.bashPolicy = bashPolicy as PlanModeBashPolicy;
|
|
121
|
-
}
|
|
122
|
-
if (Object.hasOwn(value, "safeSubcommands")) {
|
|
123
|
-
const safeSubcommands = normalizeSafeSubcommands(Reflect.get(value, "safeSubcommands"));
|
|
124
|
-
if (!safeSubcommands) return undefined;
|
|
125
|
-
settings.safeSubcommands = safeSubcommands;
|
|
126
|
-
}
|
|
127
84
|
return settings;
|
|
128
85
|
}
|
|
129
86
|
|
|
130
|
-
function normalizeToolNames(value: unknown) {
|
|
131
|
-
if (
|
|
132
|
-
!Array.isArray(value) ||
|
|
133
|
-
!value.every((item): item is string => typeof item === "string" && item.trim().length > 0)
|
|
134
|
-
) {
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
return Array.from(new Set(value));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
87
|
function normalizePlanExportPath(value: unknown) {
|
|
141
88
|
if (typeof value !== "string") return undefined;
|
|
142
89
|
const normalized = value.trim();
|
|
@@ -154,34 +101,6 @@ function normalizePlanExportPath(value: unknown) {
|
|
|
154
101
|
return normalized;
|
|
155
102
|
}
|
|
156
103
|
|
|
157
|
-
function normalizeSafeSubcommands(value: unknown): SafeSubcommands | undefined {
|
|
158
|
-
if (!isSettingsDocument(value)) return undefined;
|
|
159
|
-
if (Object.keys(value).some((key) => key !== "git" && key !== "gh")) return undefined;
|
|
160
|
-
|
|
161
|
-
const safeSubcommands: SafeSubcommands = {};
|
|
162
|
-
if (Object.hasOwn(value, "git")) {
|
|
163
|
-
const git = normalizeKnownValues(Reflect.get(value, "git"), SAFE_GIT_SUBCOMMANDS);
|
|
164
|
-
if (!git) return undefined;
|
|
165
|
-
safeSubcommands.git = git as SafeGitSubcommand[];
|
|
166
|
-
}
|
|
167
|
-
if (Object.hasOwn(value, "gh")) {
|
|
168
|
-
const gh = normalizeKnownValues(Reflect.get(value, "gh"), SAFE_GH_SUBCOMMAND_PATHS);
|
|
169
|
-
if (!gh) return undefined;
|
|
170
|
-
safeSubcommands.gh = gh as SafeGhSubcommandPath[];
|
|
171
|
-
}
|
|
172
|
-
return safeSubcommands;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function normalizeKnownValues(value: unknown, supported: readonly string[]) {
|
|
176
|
-
if (
|
|
177
|
-
!Array.isArray(value) ||
|
|
178
|
-
!value.every((item): item is string => typeof item === "string" && supported.includes(item))
|
|
179
|
-
) {
|
|
180
|
-
return undefined;
|
|
181
|
-
}
|
|
182
|
-
return Array.from(new Set(value));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
104
|
export async function readPlanModeSettings(
|
|
186
105
|
settingsPath?: string,
|
|
187
106
|
): Promise<PlanModeSettingsLoadResult> {
|
|
@@ -225,18 +144,10 @@ export function updatePlanModeSettings(
|
|
|
225
144
|
const current = await readSettingsDocumentForUpdate(settingsPath, legacySettingsPath);
|
|
226
145
|
const updated: SettingsDocument = { ...current };
|
|
227
146
|
if (patch.thinkingLevel !== undefined) updated.thinkingLevel = patch.thinkingLevel;
|
|
228
|
-
if (patch.defaultPlanTools === null) delete updated.defaultPlanTools;
|
|
229
|
-
else if (patch.defaultPlanTools !== undefined) {
|
|
230
|
-
updated.defaultPlanTools = [...patch.defaultPlanTools];
|
|
231
|
-
}
|
|
232
|
-
if (patch.implementationPlanRetention !== undefined) {
|
|
233
|
-
updated.implementationPlanRetention = patch.implementationPlanRetention;
|
|
234
|
-
}
|
|
235
147
|
if (patch.defaultPlanExportPath === null) delete updated.defaultPlanExportPath;
|
|
236
148
|
else if (patch.defaultPlanExportPath !== undefined) {
|
|
237
149
|
updated.defaultPlanExportPath = patch.defaultPlanExportPath;
|
|
238
150
|
}
|
|
239
|
-
if (patch.bashPolicy !== undefined) updated.bashPolicy = patch.bashPolicy;
|
|
240
151
|
const settings = normalizePlanModeSettings(updated);
|
|
241
152
|
if (!settings) throw invalidSettingsError(settingsPath, "invalid settings shape");
|
|
242
153
|
await publishSettings(settingsPath, updated, options.signal, options.beforeRename);
|
|
@@ -405,16 +316,6 @@ export function configuredThinkingLevel(
|
|
|
405
316
|
return settings.thinkingLevel === "inherit" ? undefined : settings.thinkingLevel;
|
|
406
317
|
}
|
|
407
318
|
|
|
408
|
-
export function configuredImplementationPlanRetention(
|
|
409
|
-
settings: PlanModeSettings,
|
|
410
|
-
): ImplementationPlanRetention {
|
|
411
|
-
return settings.implementationPlanRetention ?? "keep";
|
|
412
|
-
}
|
|
413
|
-
|
|
414
319
|
export function configuredPlanExportPath(settings: PlanModeSettings) {
|
|
415
320
|
return settings.defaultPlanExportPath ?? DEFAULT_PLAN_EXPORT_PATH;
|
|
416
321
|
}
|
|
417
|
-
|
|
418
|
-
export function configuredBashPolicy(settings: PlanModeSettings): PlanModeBashPolicy {
|
|
419
|
-
return settings.bashPolicy ?? "limited";
|
|
420
|
-
}
|