gsd-pi 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/README.md +341 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +35 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +69 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/dist/modes/interactive/theme/theme.js +949 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -0
- package/dist/resource-loader.d.ts +22 -0
- package/dist/resource-loader.js +48 -0
- package/dist/wizard.d.ts +20 -0
- package/dist/wizard.js +132 -0
- package/package.json +39 -0
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +10 -0
- package/src/resources/AGENTS.md +204 -0
- package/src/resources/GSD-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2554 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/gsd/activity-log.ts +48 -0
- package/src/resources/extensions/gsd/auto.ts +2032 -0
- package/src/resources/extensions/gsd/commands.ts +292 -0
- package/src/resources/extensions/gsd/crash-recovery.ts +85 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/gsd/doctor.ts +683 -0
- package/src/resources/extensions/gsd/files.ts +730 -0
- package/src/resources/extensions/gsd/gitignore.ts +104 -0
- package/src/resources/extensions/gsd/guided-flow.ts +800 -0
- package/src/resources/extensions/gsd/index.ts +418 -0
- package/src/resources/extensions/gsd/metrics.ts +372 -0
- package/src/resources/extensions/gsd/observability-validator.ts +408 -0
- package/src/resources/extensions/gsd/package.json +11 -0
- package/src/resources/extensions/gsd/paths.ts +308 -0
- package/src/resources/extensions/gsd/preferences.ts +600 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +50 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +151 -0
- package/src/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +64 -0
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/gsd/prompts/queue.md +85 -0
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/gsd/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +28 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +109 -0
- package/src/resources/extensions/gsd/prompts/system.md +220 -0
- package/src/resources/extensions/gsd/session-forensics.ts +487 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +137 -0
- package/src/resources/extensions/gsd/state.ts +439 -0
- package/src/resources/extensions/gsd/templates/context.md +76 -0
- package/src/resources/extensions/gsd/templates/decisions.md +8 -0
- package/src/resources/extensions/gsd/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/gsd/templates/plan.md +133 -0
- package/src/resources/extensions/gsd/templates/preferences.md +15 -0
- package/src/resources/extensions/gsd/templates/project.md +31 -0
- package/src/resources/extensions/gsd/templates/reassessment.md +28 -0
- package/src/resources/extensions/gsd/templates/requirements.md +81 -0
- package/src/resources/extensions/gsd/templates/research.md +46 -0
- package/src/resources/extensions/gsd/templates/roadmap.md +118 -0
- package/src/resources/extensions/gsd/templates/slice-context.md +58 -0
- package/src/resources/extensions/gsd/templates/slice-summary.md +99 -0
- package/src/resources/extensions/gsd/templates/state.md +19 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +52 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +57 -0
- package/src/resources/extensions/gsd/templates/uat.md +54 -0
- package/src/resources/extensions/gsd/types.ts +159 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +162 -0
- package/src/resources/extensions/gsd/workspace-index.ts +203 -0
- package/src/resources/extensions/gsd/worktree.ts +182 -0
- package/src/resources/extensions/plan-mode/README.md +65 -0
- package/src/resources/extensions/plan-mode/index.ts +521 -0
- package/src/resources/extensions/plan-mode/utils.ts +168 -0
- package/src/resources/extensions/search-the-web/cache.ts +70 -0
- package/src/resources/extensions/search-the-web/format.ts +134 -0
- package/src/resources/extensions/search-the-web/http.ts +147 -0
- package/src/resources/extensions/search-the-web/index.ts +46 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +374 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +424 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/interview-ui.ts +613 -0
- package/src/resources/extensions/shared/next-action-ui.ts +197 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +88 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +297 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +234 -0
- package/src/resources/extensions/slash-commands/gsd-run.ts +34 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1021 -0
- package/src/resources/extensions/worktree/index.ts +420 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Mode Extension
|
|
3
|
+
*
|
|
4
|
+
* Read-only exploration mode for safe code analysis.
|
|
5
|
+
* When enabled, only read-only tools are available.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - /plan command or Ctrl+Alt+P to toggle
|
|
9
|
+
* - Bash restricted to allowlisted read-only commands
|
|
10
|
+
* - Extracts numbered plan steps from "Plan:" sections
|
|
11
|
+
* - [DONE:n] markers to complete steps during execution
|
|
12
|
+
* - Progress tracking widget during execution
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
16
|
+
import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
|
|
17
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
18
|
+
import { Key } from "@mariozechner/pi-tui";
|
|
19
|
+
import { Type } from "@sinclair/typebox";
|
|
20
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
21
|
+
import { showInterviewRound, type Question } from "../shared/interview-ui.js";
|
|
22
|
+
import {
|
|
23
|
+
createProgressPanel,
|
|
24
|
+
type ProgressPanel,
|
|
25
|
+
type ProgressPanelModel,
|
|
26
|
+
type ProgressItem,
|
|
27
|
+
type ProgressItemStatus,
|
|
28
|
+
} from "../shared/progress-widget.js";
|
|
29
|
+
import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.js";
|
|
30
|
+
|
|
31
|
+
// Tools
|
|
32
|
+
const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "plan_clarify"];
|
|
33
|
+
const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"];
|
|
34
|
+
|
|
35
|
+
// Type guard for assistant messages
|
|
36
|
+
function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
|
|
37
|
+
return m.role === "assistant" && Array.isArray(m.content);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Extract text content from an assistant message
|
|
41
|
+
function getTextContent(message: AssistantMessage): string {
|
|
42
|
+
return message.content
|
|
43
|
+
.filter((block): block is TextContent => block.type === "text")
|
|
44
|
+
.map((block) => block.text)
|
|
45
|
+
.join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function planModeExtension(pi: ExtensionAPI): void {
|
|
49
|
+
let planModeEnabled = false;
|
|
50
|
+
let executionMode = false;
|
|
51
|
+
let todoItems: TodoItem[] = [];
|
|
52
|
+
let panel: ProgressPanel | null = null;
|
|
53
|
+
let cmdCtx: ExtensionCommandContext | null = null;
|
|
54
|
+
|
|
55
|
+
// ── Progress panel model builder ──────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function buildPlanModel(): ProgressPanelModel {
|
|
58
|
+
const allDone = todoItems.length > 0 && todoItems.every((t) => t.completed);
|
|
59
|
+
|
|
60
|
+
// Derive per-item status: first non-completed item is active
|
|
61
|
+
let foundActive = false;
|
|
62
|
+
const items: ProgressItem[] = todoItems.map((t) => {
|
|
63
|
+
let status: ProgressItemStatus;
|
|
64
|
+
if (t.completed) {
|
|
65
|
+
status = "done";
|
|
66
|
+
} else if (!foundActive) {
|
|
67
|
+
status = "active";
|
|
68
|
+
foundActive = true;
|
|
69
|
+
} else {
|
|
70
|
+
status = "pending";
|
|
71
|
+
}
|
|
72
|
+
return { label: t.text, status };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const completed = todoItems.filter((t) => t.completed).length;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
title: "Plan",
|
|
79
|
+
badge: allDone ? "DONE" : "EXECUTING",
|
|
80
|
+
badgeStatus: allDone ? "done" : "active",
|
|
81
|
+
subtitle: [`Step ${completed}/${todoItems.length}`],
|
|
82
|
+
items,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pi.registerFlag("plan", {
|
|
87
|
+
description: "Start in plan mode (read-only exploration)",
|
|
88
|
+
type: "boolean",
|
|
89
|
+
default: false,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function ensurePanel(ctx: ExtensionContext): void {
|
|
93
|
+
if (!panel) {
|
|
94
|
+
panel = createProgressPanel(ctx.ui, {
|
|
95
|
+
widgetKey: "plan-todos",
|
|
96
|
+
statusKey: "plan-mode",
|
|
97
|
+
statusPrefix: "plan",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function updateStatus(ctx: ExtensionContext): void {
|
|
103
|
+
if (executionMode && todoItems.length > 0) {
|
|
104
|
+
// Execution-mode: delegate to the shared progress panel
|
|
105
|
+
ensurePanel(ctx);
|
|
106
|
+
panel!.update(buildPlanModel());
|
|
107
|
+
} else if (planModeEnabled) {
|
|
108
|
+
// Idle plan-mode: simple footer status, no widget
|
|
109
|
+
panel?.dispose();
|
|
110
|
+
panel = null;
|
|
111
|
+
ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("warning", "⏸ plan"));
|
|
112
|
+
} else {
|
|
113
|
+
// Neither mode: clean up everything
|
|
114
|
+
panel?.dispose();
|
|
115
|
+
panel = null;
|
|
116
|
+
ctx.ui.setStatus("plan-mode", undefined);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function togglePlanMode(ctx: ExtensionContext): void {
|
|
121
|
+
planModeEnabled = !planModeEnabled;
|
|
122
|
+
executionMode = false;
|
|
123
|
+
todoItems = [];
|
|
124
|
+
|
|
125
|
+
if (planModeEnabled) {
|
|
126
|
+
pi.setActiveTools(PLAN_MODE_TOOLS);
|
|
127
|
+
ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`);
|
|
128
|
+
} else {
|
|
129
|
+
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
130
|
+
ctx.ui.notify("Plan mode disabled. Full access restored.");
|
|
131
|
+
}
|
|
132
|
+
updateStatus(ctx);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function persistState(): void {
|
|
136
|
+
pi.appendEntry("plan-mode", {
|
|
137
|
+
enabled: planModeEnabled,
|
|
138
|
+
todos: todoItems,
|
|
139
|
+
executing: executionMode,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── plan_clarify tool — LLM-driven clarifying questions via interview UI ──
|
|
144
|
+
|
|
145
|
+
pi.registerTool({
|
|
146
|
+
name: "plan_clarify",
|
|
147
|
+
label: "Plan Clarify",
|
|
148
|
+
description: [
|
|
149
|
+
"Ask the user clarifying questions during plan mode exploration.",
|
|
150
|
+
"Use this to resolve ambiguities before committing to a plan.",
|
|
151
|
+
"Each question should have 2-4 concrete options. The user can also add notes.",
|
|
152
|
+
"Keep questions focused on intent, scope, and constraints — not implementation details.",
|
|
153
|
+
].join("\n"),
|
|
154
|
+
parameters: Type.Object({
|
|
155
|
+
questions: Type.Array(
|
|
156
|
+
Type.Object({
|
|
157
|
+
id: Type.String({ description: "Stable snake_case identifier, e.g. 'scope'" }),
|
|
158
|
+
header: Type.String({ description: "Short label, 12 chars or fewer, shown in tab bar" }),
|
|
159
|
+
question: Type.String({ description: "Single sentence question for the user" }),
|
|
160
|
+
options: Type.Array(
|
|
161
|
+
Type.Object({
|
|
162
|
+
label: Type.String({ description: "2-5 word label" }),
|
|
163
|
+
description: Type.String({ description: "One sentence explaining this choice" }),
|
|
164
|
+
}),
|
|
165
|
+
{ minItems: 2, maxItems: 4 },
|
|
166
|
+
),
|
|
167
|
+
allowMultiple: Type.Optional(Type.Boolean({ description: "Allow selecting multiple options (default: false)" })),
|
|
168
|
+
}),
|
|
169
|
+
{ description: "1-4 clarifying questions to present to the user" },
|
|
170
|
+
),
|
|
171
|
+
}),
|
|
172
|
+
|
|
173
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
174
|
+
if (!ctx.hasUI) {
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: "text", text: "Error: UI not available (running in non-interactive mode)" }],
|
|
177
|
+
details: { cancelled: true, answers: {} },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const questions = params.questions as Question[];
|
|
182
|
+
if (questions.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: "Error: No questions provided" }],
|
|
185
|
+
details: { cancelled: true, answers: {} },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = await showInterviewRound(
|
|
190
|
+
questions,
|
|
191
|
+
{ progress: "Plan Mode · Clarifying Questions" },
|
|
192
|
+
ctx,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (!result.answers || Object.keys(result.answers).length === 0) {
|
|
196
|
+
return {
|
|
197
|
+
content: [{ type: "text", text: "User cancelled the clarification." }],
|
|
198
|
+
details: { cancelled: true, answers: {} },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const lines = Object.entries(result.answers).map(([id, ans]) => {
|
|
203
|
+
const q = questions.find((q) => q.id === id);
|
|
204
|
+
const label = q?.question ?? id;
|
|
205
|
+
const selected = Array.isArray(ans.selected) ? ans.selected.join(", ") : ans.selected;
|
|
206
|
+
return ans.notes
|
|
207
|
+
? `${label}: ${selected} (note: ${ans.notes})`
|
|
208
|
+
: `${label}: ${selected}`;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
213
|
+
details: { cancelled: false, answers: result.answers },
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
renderCall(args, theme) {
|
|
218
|
+
const qs = (args.questions as Question[]) || [];
|
|
219
|
+
let text = theme.fg("toolTitle", theme.bold("plan_clarify "));
|
|
220
|
+
text += theme.fg("muted", `${qs.length} question${qs.length !== 1 ? "s" : ""}`);
|
|
221
|
+
return new Text(text, 0, 0);
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
renderResult(result, _options, theme) {
|
|
225
|
+
const details = result.details as { cancelled?: boolean; answers?: Record<string, { selected: string | string[]; notes?: string }> } | undefined;
|
|
226
|
+
if (!details || details.cancelled) {
|
|
227
|
+
return new Text(theme.fg("warning", "Cancelled"), 0, 0);
|
|
228
|
+
}
|
|
229
|
+
const lines = Object.entries(details.answers ?? {}).map(([id, ans]) => {
|
|
230
|
+
const selected = Array.isArray(ans.selected) ? ans.selected.join(", ") : ans.selected;
|
|
231
|
+
let line = `${theme.fg("success", "✓ ")}${theme.fg("accent", id)}: ${selected}`;
|
|
232
|
+
if (ans.notes) line += ` ${theme.fg("muted", `[${ans.notes}]`)}`;
|
|
233
|
+
return line;
|
|
234
|
+
});
|
|
235
|
+
return new Text(lines.join("\n") || theme.fg("dim", "(no answers)"), 0, 0);
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
pi.registerCommand("plan", {
|
|
240
|
+
description: "Toggle plan mode (read-only exploration)",
|
|
241
|
+
handler: async (_args, ctx) => {
|
|
242
|
+
cmdCtx = ctx;
|
|
243
|
+
togglePlanMode(ctx);
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
pi.registerCommand("todos", {
|
|
248
|
+
description: "Show current plan todo list",
|
|
249
|
+
handler: async (_args, ctx) => {
|
|
250
|
+
if (todoItems.length === 0) {
|
|
251
|
+
ctx.ui.notify("No todos. Create a plan first with /plan", "info");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const list = todoItems.map((item, i) => `${i + 1}. ${item.completed ? "✓" : "○"} ${item.text}`).join("\n");
|
|
255
|
+
ctx.ui.notify(`Plan Progress:\n${list}`, "info");
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
pi.registerShortcut(Key.ctrlAlt("p"), {
|
|
260
|
+
description: "Toggle plan mode",
|
|
261
|
+
handler: async (ctx) => togglePlanMode(ctx),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Block destructive bash commands in plan mode
|
|
265
|
+
pi.on("tool_call", async (event) => {
|
|
266
|
+
if (!planModeEnabled || event.toolName !== "bash") return;
|
|
267
|
+
|
|
268
|
+
const command = event.input.command as string;
|
|
269
|
+
if (!isSafeCommand(command)) {
|
|
270
|
+
return {
|
|
271
|
+
block: true,
|
|
272
|
+
reason: `Plan mode: command blocked (not allowlisted). Use /plan to disable plan mode first.\nCommand: ${command}`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Filter out stale plan mode context when not in plan mode
|
|
278
|
+
pi.on("context", async (event) => {
|
|
279
|
+
if (planModeEnabled) return;
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
messages: event.messages.filter((m) => {
|
|
283
|
+
const msg = m as AgentMessage & { customType?: string };
|
|
284
|
+
if (msg.customType === "plan-mode-context") return false;
|
|
285
|
+
if (msg.role !== "user") return true;
|
|
286
|
+
|
|
287
|
+
const content = msg.content;
|
|
288
|
+
if (typeof content === "string") {
|
|
289
|
+
return !content.includes("[PLAN MODE ACTIVE]");
|
|
290
|
+
}
|
|
291
|
+
if (Array.isArray(content)) {
|
|
292
|
+
return !content.some(
|
|
293
|
+
(c) => c.type === "text" && (c as TextContent).text?.includes("[PLAN MODE ACTIVE]"),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
297
|
+
}),
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Inject plan/execution context before agent starts
|
|
302
|
+
pi.on("before_agent_start", async () => {
|
|
303
|
+
if (planModeEnabled) {
|
|
304
|
+
return {
|
|
305
|
+
message: {
|
|
306
|
+
customType: "plan-mode-context",
|
|
307
|
+
content: `[PLAN MODE ACTIVE]
|
|
308
|
+
You are in plan mode - a read-only exploration mode for safe code analysis.
|
|
309
|
+
|
|
310
|
+
Restrictions:
|
|
311
|
+
- You can only use: read, bash, grep, find, ls, plan_clarify
|
|
312
|
+
- You CANNOT use: edit, write (file modifications are disabled)
|
|
313
|
+
- Bash is restricted to an allowlist of read-only commands
|
|
314
|
+
|
|
315
|
+
Use the plan_clarify tool to ask the user clarifying questions before committing to a plan.
|
|
316
|
+
|
|
317
|
+
Create a detailed numbered plan under a "Plan:" header:
|
|
318
|
+
|
|
319
|
+
Plan:
|
|
320
|
+
1. First step description
|
|
321
|
+
2. Second step description
|
|
322
|
+
...
|
|
323
|
+
|
|
324
|
+
Do NOT attempt to make changes - just describe what you would do.`,
|
|
325
|
+
display: false,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (executionMode && todoItems.length > 0) {
|
|
331
|
+
if (panel) panel.startPulse();
|
|
332
|
+
|
|
333
|
+
const remaining = todoItems.filter((t) => !t.completed);
|
|
334
|
+
const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join("\n");
|
|
335
|
+
return {
|
|
336
|
+
message: {
|
|
337
|
+
customType: "plan-execution-context",
|
|
338
|
+
content: `[EXECUTING PLAN - Full tool access enabled]
|
|
339
|
+
|
|
340
|
+
Remaining steps:
|
|
341
|
+
${todoList}
|
|
342
|
+
|
|
343
|
+
Execute each step in order.
|
|
344
|
+
After completing a step, include a [DONE:n] tag in your response.`,
|
|
345
|
+
display: false,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Track progress after each turn
|
|
352
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
353
|
+
if (!executionMode || todoItems.length === 0) return;
|
|
354
|
+
if (!isAssistantMessage(event.message)) return;
|
|
355
|
+
|
|
356
|
+
const text = getTextContent(event.message);
|
|
357
|
+
if (markCompletedSteps(text, todoItems) > 0) {
|
|
358
|
+
updateStatus(ctx);
|
|
359
|
+
}
|
|
360
|
+
persistState();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Handle plan completion and plan mode UI
|
|
364
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
365
|
+
// Stop pulse animation (safe to call even if not pulsing)
|
|
366
|
+
if (panel) panel.stopPulse();
|
|
367
|
+
|
|
368
|
+
// Check if execution is complete
|
|
369
|
+
if (executionMode && todoItems.length > 0) {
|
|
370
|
+
if (todoItems.every((t) => t.completed)) {
|
|
371
|
+
// Final update to show DONE state before disposing
|
|
372
|
+
panel?.update(buildPlanModel());
|
|
373
|
+
|
|
374
|
+
const completedList = todoItems.map((t) => `~~${t.text}~~`).join("\n");
|
|
375
|
+
pi.sendMessage(
|
|
376
|
+
{ customType: "plan-complete", content: `**Plan Complete!** ✓\n\n${completedList}`, display: true },
|
|
377
|
+
{ triggerTurn: false },
|
|
378
|
+
);
|
|
379
|
+
executionMode = false;
|
|
380
|
+
todoItems = [];
|
|
381
|
+
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
382
|
+
updateStatus(ctx);
|
|
383
|
+
persistState();
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!planModeEnabled || !ctx.hasUI) return;
|
|
389
|
+
|
|
390
|
+
// Extract todos from last assistant message
|
|
391
|
+
const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
|
|
392
|
+
if (lastAssistant) {
|
|
393
|
+
const extracted = extractTodoItems(getTextContent(lastAssistant));
|
|
394
|
+
if (extracted.length > 0) {
|
|
395
|
+
todoItems = extracted;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Show plan steps and prompt for next action
|
|
400
|
+
if (todoItems.length > 0) {
|
|
401
|
+
const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n");
|
|
402
|
+
pi.sendMessage(
|
|
403
|
+
{
|
|
404
|
+
customType: "plan-todo-list",
|
|
405
|
+
content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`,
|
|
406
|
+
display: true,
|
|
407
|
+
},
|
|
408
|
+
{ triggerTurn: false },
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const choice = await ctx.ui.select("Plan mode - what next?", [
|
|
413
|
+
todoItems.length > 0 ? "Execute the plan (track progress)" : "Execute the plan",
|
|
414
|
+
"Stay in plan mode",
|
|
415
|
+
"Refine the plan",
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
if (choice?.startsWith("Execute")) {
|
|
419
|
+
planModeEnabled = false;
|
|
420
|
+
executionMode = todoItems.length > 0;
|
|
421
|
+
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
422
|
+
updateStatus(ctx);
|
|
423
|
+
|
|
424
|
+
// Build the execution prompt — include the full plan so the fresh
|
|
425
|
+
// session has everything it needs without relying on prior context.
|
|
426
|
+
const todoListText =
|
|
427
|
+
todoItems.length > 0
|
|
428
|
+
? todoItems.map((t) => `${t.step}. ${t.text}`).join("\n")
|
|
429
|
+
: "";
|
|
430
|
+
const execMessage =
|
|
431
|
+
todoItems.length > 0
|
|
432
|
+
? [
|
|
433
|
+
`Execute the following plan. Start with step 1.`,
|
|
434
|
+
``,
|
|
435
|
+
`Plan:`,
|
|
436
|
+
todoListText,
|
|
437
|
+
``,
|
|
438
|
+
`After completing each step, include a [DONE:n] tag in your response.`,
|
|
439
|
+
].join("\n")
|
|
440
|
+
: "Execute the plan you just created.";
|
|
441
|
+
|
|
442
|
+
// Clear context before executing so the LLM starts fresh.
|
|
443
|
+
if (cmdCtx) {
|
|
444
|
+
const result = await cmdCtx.newSession();
|
|
445
|
+
if (result.cancelled) {
|
|
446
|
+
// User cancelled the session switch — restore plan mode.
|
|
447
|
+
planModeEnabled = true;
|
|
448
|
+
executionMode = false;
|
|
449
|
+
pi.setActiveTools(PLAN_MODE_TOOLS);
|
|
450
|
+
updateStatus(ctx);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
pi.sendMessage(
|
|
456
|
+
{ customType: "plan-mode-execute", content: execMessage, display: true },
|
|
457
|
+
{ triggerTurn: true },
|
|
458
|
+
);
|
|
459
|
+
} else if (choice === "Refine the plan") {
|
|
460
|
+
const refinement = await ctx.ui.editor("Refine the plan:", "");
|
|
461
|
+
if (refinement?.trim()) {
|
|
462
|
+
pi.sendUserMessage(refinement.trim());
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Restore state on session start/resume
|
|
468
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
469
|
+
if (pi.getFlag("plan") === true) {
|
|
470
|
+
planModeEnabled = true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const entries = ctx.sessionManager.getEntries();
|
|
474
|
+
|
|
475
|
+
// Restore persisted state
|
|
476
|
+
const planModeEntry = entries
|
|
477
|
+
.filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode")
|
|
478
|
+
.pop() as { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } } | undefined;
|
|
479
|
+
|
|
480
|
+
if (planModeEntry?.data) {
|
|
481
|
+
planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled;
|
|
482
|
+
todoItems = planModeEntry.data.todos ?? todoItems;
|
|
483
|
+
executionMode = planModeEntry.data.executing ?? executionMode;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// On resume: re-scan messages to rebuild completion state
|
|
487
|
+
const isResume = planModeEntry !== undefined;
|
|
488
|
+
if (isResume && executionMode && todoItems.length > 0) {
|
|
489
|
+
let executeIndex = -1;
|
|
490
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
491
|
+
const entry = entries[i] as { type: string; customType?: string };
|
|
492
|
+
if (entry.customType === "plan-mode-execute") {
|
|
493
|
+
executeIndex = i;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const messages: AssistantMessage[] = [];
|
|
499
|
+
for (let i = executeIndex + 1; i < entries.length; i++) {
|
|
500
|
+
const entry = entries[i];
|
|
501
|
+
if (entry.type === "message" && "message" in entry && isAssistantMessage(entry.message as AgentMessage)) {
|
|
502
|
+
messages.push(entry.message as AssistantMessage);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const allText = messages.map(getTextContent).join("\n");
|
|
506
|
+
markCompletedSteps(allText, todoItems);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (planModeEnabled) {
|
|
510
|
+
pi.setActiveTools(PLAN_MODE_TOOLS);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Re-initialize the progress panel if resuming an active execution
|
|
514
|
+
if (executionMode && todoItems.length > 0) {
|
|
515
|
+
ensurePanel(ctx);
|
|
516
|
+
panel!.update(buildPlanModel());
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
updateStatus(ctx);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utility functions for plan mode.
|
|
3
|
+
* Extracted for testability.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Destructive commands blocked in plan mode
|
|
7
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
8
|
+
/\brm\b/i,
|
|
9
|
+
/\brmdir\b/i,
|
|
10
|
+
/\bmv\b/i,
|
|
11
|
+
/\bcp\b/i,
|
|
12
|
+
/\bmkdir\b/i,
|
|
13
|
+
/\btouch\b/i,
|
|
14
|
+
/\bchmod\b/i,
|
|
15
|
+
/\bchown\b/i,
|
|
16
|
+
/\bchgrp\b/i,
|
|
17
|
+
/\bln\b/i,
|
|
18
|
+
/\btee\b/i,
|
|
19
|
+
/\btruncate\b/i,
|
|
20
|
+
/\bdd\b/i,
|
|
21
|
+
/\bshred\b/i,
|
|
22
|
+
/(^|[^<])>(?!>)/,
|
|
23
|
+
/>>/,
|
|
24
|
+
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
|
25
|
+
/\byarn\s+(add|remove|install|publish)/i,
|
|
26
|
+
/\bpnpm\s+(add|remove|install|publish)/i,
|
|
27
|
+
/\bpip\s+(install|uninstall)/i,
|
|
28
|
+
/\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
|
|
29
|
+
/\bbrew\s+(install|uninstall|upgrade)/i,
|
|
30
|
+
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
|
|
31
|
+
/\bsudo\b/i,
|
|
32
|
+
/\bsu\b/i,
|
|
33
|
+
/\bkill\b/i,
|
|
34
|
+
/\bpkill\b/i,
|
|
35
|
+
/\bkillall\b/i,
|
|
36
|
+
/\breboot\b/i,
|
|
37
|
+
/\bshutdown\b/i,
|
|
38
|
+
/\bsystemctl\s+(start|stop|restart|enable|disable)/i,
|
|
39
|
+
/\bservice\s+\S+\s+(start|stop|restart)/i,
|
|
40
|
+
/\b(vim?|nano|emacs|code|subl)\b/i,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Safe read-only commands allowed in plan mode
|
|
44
|
+
const SAFE_PATTERNS = [
|
|
45
|
+
/^\s*cat\b/,
|
|
46
|
+
/^\s*head\b/,
|
|
47
|
+
/^\s*tail\b/,
|
|
48
|
+
/^\s*less\b/,
|
|
49
|
+
/^\s*more\b/,
|
|
50
|
+
/^\s*grep\b/,
|
|
51
|
+
/^\s*find\b/,
|
|
52
|
+
/^\s*ls\b/,
|
|
53
|
+
/^\s*pwd\b/,
|
|
54
|
+
/^\s*echo\b/,
|
|
55
|
+
/^\s*printf\b/,
|
|
56
|
+
/^\s*wc\b/,
|
|
57
|
+
/^\s*sort\b/,
|
|
58
|
+
/^\s*uniq\b/,
|
|
59
|
+
/^\s*diff\b/,
|
|
60
|
+
/^\s*file\b/,
|
|
61
|
+
/^\s*stat\b/,
|
|
62
|
+
/^\s*du\b/,
|
|
63
|
+
/^\s*df\b/,
|
|
64
|
+
/^\s*tree\b/,
|
|
65
|
+
/^\s*which\b/,
|
|
66
|
+
/^\s*whereis\b/,
|
|
67
|
+
/^\s*type\b/,
|
|
68
|
+
/^\s*env\b/,
|
|
69
|
+
/^\s*printenv\b/,
|
|
70
|
+
/^\s*uname\b/,
|
|
71
|
+
/^\s*whoami\b/,
|
|
72
|
+
/^\s*id\b/,
|
|
73
|
+
/^\s*date\b/,
|
|
74
|
+
/^\s*cal\b/,
|
|
75
|
+
/^\s*uptime\b/,
|
|
76
|
+
/^\s*ps\b/,
|
|
77
|
+
/^\s*top\b/,
|
|
78
|
+
/^\s*htop\b/,
|
|
79
|
+
/^\s*free\b/,
|
|
80
|
+
/^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
|
|
81
|
+
/^\s*git\s+ls-/i,
|
|
82
|
+
/^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
|
|
83
|
+
/^\s*yarn\s+(list|info|why|audit)/i,
|
|
84
|
+
/^\s*node\s+--version/i,
|
|
85
|
+
/^\s*python\s+--version/i,
|
|
86
|
+
/^\s*curl\s/i,
|
|
87
|
+
/^\s*wget\s+-O\s*-/i,
|
|
88
|
+
/^\s*jq\b/,
|
|
89
|
+
/^\s*sed\s+-n/i,
|
|
90
|
+
/^\s*awk\b/,
|
|
91
|
+
/^\s*rg\b/,
|
|
92
|
+
/^\s*fd\b/,
|
|
93
|
+
/^\s*bat\b/,
|
|
94
|
+
/^\s*exa\b/,
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
export function isSafeCommand(command: string): boolean {
|
|
98
|
+
const isDestructive = DESTRUCTIVE_PATTERNS.some((p) => p.test(command));
|
|
99
|
+
const isSafe = SAFE_PATTERNS.some((p) => p.test(command));
|
|
100
|
+
return !isDestructive && isSafe;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface TodoItem {
|
|
104
|
+
step: number;
|
|
105
|
+
text: string;
|
|
106
|
+
completed: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function cleanStepText(text: string): string {
|
|
110
|
+
let cleaned = text
|
|
111
|
+
.replace(/\*{1,2}([^*]+)\*{1,2}/g, "$1") // Remove bold/italic
|
|
112
|
+
.replace(/`([^`]+)`/g, "$1") // Remove code
|
|
113
|
+
.replace(
|
|
114
|
+
/^(Use|Run|Execute|Create|Write|Read|Check|Verify|Update|Modify|Add|Remove|Delete|Install)\s+(the\s+)?/i,
|
|
115
|
+
"",
|
|
116
|
+
)
|
|
117
|
+
.replace(/\s+/g, " ")
|
|
118
|
+
.trim();
|
|
119
|
+
|
|
120
|
+
if (cleaned.length > 0) {
|
|
121
|
+
cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
122
|
+
}
|
|
123
|
+
if (cleaned.length > 50) {
|
|
124
|
+
cleaned = `${cleaned.slice(0, 47)}...`;
|
|
125
|
+
}
|
|
126
|
+
return cleaned;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function extractTodoItems(message: string): TodoItem[] {
|
|
130
|
+
const items: TodoItem[] = [];
|
|
131
|
+
const headerMatch = message.match(/\*{0,2}Plan:\*{0,2}\s*\n/i);
|
|
132
|
+
if (!headerMatch) return items;
|
|
133
|
+
|
|
134
|
+
const planSection = message.slice(message.indexOf(headerMatch[0]) + headerMatch[0].length);
|
|
135
|
+
const numberedPattern = /^\s*(\d+)[.)]\s+\*{0,2}([^*\n]+)/gm;
|
|
136
|
+
|
|
137
|
+
for (const match of planSection.matchAll(numberedPattern)) {
|
|
138
|
+
const text = match[2]
|
|
139
|
+
.trim()
|
|
140
|
+
.replace(/\*{1,2}$/, "")
|
|
141
|
+
.trim();
|
|
142
|
+
if (text.length > 5 && !text.startsWith("`") && !text.startsWith("/") && !text.startsWith("-")) {
|
|
143
|
+
const cleaned = cleanStepText(text);
|
|
144
|
+
if (cleaned.length > 3) {
|
|
145
|
+
items.push({ step: items.length + 1, text: cleaned, completed: false });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return items;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function extractDoneSteps(message: string): number[] {
|
|
153
|
+
const steps: number[] = [];
|
|
154
|
+
for (const match of message.matchAll(/\[DONE:(\d+)\]/gi)) {
|
|
155
|
+
const step = Number(match[1]);
|
|
156
|
+
if (Number.isFinite(step)) steps.push(step);
|
|
157
|
+
}
|
|
158
|
+
return steps;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function markCompletedSteps(text: string, items: TodoItem[]): number {
|
|
162
|
+
const doneSteps = extractDoneSteps(text);
|
|
163
|
+
for (const step of doneSteps) {
|
|
164
|
+
const item = items.find((t) => t.step === step);
|
|
165
|
+
if (item) item.completed = true;
|
|
166
|
+
}
|
|
167
|
+
return doneSteps.length;
|
|
168
|
+
}
|