@garaje/base 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/README.md +10 -0
- package/agents/doc-writer.md +28 -0
- package/agents/implementer.md +31 -0
- package/agents/planner.md +33 -0
- package/agents/reviewer.md +36 -0
- package/extensions/.gitkeep +0 -0
- package/extensions/README.md +96 -0
- package/extensions/agent-rules.ts +29 -0
- package/extensions/command-guard.ts +94 -0
- package/extensions/confirm-destructive.ts +59 -0
- package/extensions/custom-compaction.ts +127 -0
- package/extensions/dirty-repo-guard.ts +56 -0
- package/extensions/git-checkpoint.ts +53 -0
- package/extensions/model-status.ts +31 -0
- package/extensions/notify.ts +55 -0
- package/extensions/plan-mode/README.md +72 -0
- package/extensions/plan-mode/index.ts +396 -0
- package/extensions/plan-mode/utils.ts +168 -0
- package/extensions/preset.ts +412 -0
- package/extensions/protected-paths.ts +69 -0
- package/extensions/role-commands.ts +69 -0
- package/extensions/session-name.ts +27 -0
- package/extensions/standalone-commands.ts +101 -0
- package/extensions/status-line.ts +32 -0
- package/extensions/subagent/agents.ts +125 -0
- package/extensions/subagent/index.ts +1015 -0
- package/extensions/usage-status.ts +75 -0
- package/lib/agents.ts +5 -0
- package/lib/base-paths.ts +8 -0
- package/lib/presets.ts +23 -0
- package/lib/rules.ts +37 -0
- package/package.json +42 -0
- package/presets.json +58 -0
- package/prompts/roles/doc-writer.md +26 -0
- package/prompts/roles/implementer.md +29 -0
- package/prompts/roles/planner.md +31 -0
- package/prompts/roles/reviewer.md +34 -0
- package/rules/00-workspace.md +23 -0
- package/rules/10-safety.md +20 -0
- package/rules/20-self-modification.md +32 -0
- package/skills/park/SKILL.md +113 -0
- package/skills/upgrade-bay/SKILL.md +35 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Checkpoint Extension
|
|
3
|
+
*
|
|
4
|
+
* Creates git stash checkpoints at each turn so /fork can restore code state.
|
|
5
|
+
* When forking, offers to restore code to that point in history.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
export default function (pi: ExtensionAPI) {
|
|
11
|
+
const checkpoints = new Map<string, string>();
|
|
12
|
+
let currentEntryId: string | undefined;
|
|
13
|
+
|
|
14
|
+
// Track the current entry ID when user messages are saved
|
|
15
|
+
pi.on("tool_result", async (_event, ctx) => {
|
|
16
|
+
const leaf = ctx.sessionManager.getLeafEntry();
|
|
17
|
+
if (leaf) currentEntryId = leaf.id;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
pi.on("turn_start", async () => {
|
|
21
|
+
// Create a git stash entry before LLM makes changes
|
|
22
|
+
const { stdout } = await pi.exec("git", ["stash", "create"]);
|
|
23
|
+
const ref = stdout.trim();
|
|
24
|
+
if (ref && currentEntryId) {
|
|
25
|
+
checkpoints.set(currentEntryId, ref);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
pi.on("session_before_fork", async (event, ctx) => {
|
|
30
|
+
const ref = checkpoints.get(event.entryId);
|
|
31
|
+
if (!ref) return;
|
|
32
|
+
|
|
33
|
+
if (!ctx.hasUI) {
|
|
34
|
+
// In non-interactive mode, don't restore automatically
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const choice = await ctx.ui.select("Restore code state?", [
|
|
39
|
+
"Yes, restore code to that point",
|
|
40
|
+
"No, keep current code",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
if (choice?.startsWith("Yes")) {
|
|
44
|
+
await pi.exec("git", ["stash", "apply", ref]);
|
|
45
|
+
ctx.ui.notify("Code restored to checkpoint", "info");
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
pi.on("agent_end", async () => {
|
|
50
|
+
// Clear checkpoints after agent completes
|
|
51
|
+
checkpoints.clear();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model status extension - shows model changes in the status bar.
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the `model_select` hook which fires when the model changes
|
|
5
|
+
* via /model command, Ctrl+P cycling, or session restore.
|
|
6
|
+
*
|
|
7
|
+
* Usage: pi -e ./model-status.ts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
export default function (pi: ExtensionAPI) {
|
|
13
|
+
pi.on("model_select", async (event, ctx) => {
|
|
14
|
+
const { model, previousModel, source } = event;
|
|
15
|
+
|
|
16
|
+
// Format model identifiers
|
|
17
|
+
const next = `${model.provider}/${model.id}`;
|
|
18
|
+
const prev = previousModel ? `${previousModel.provider}/${previousModel.id}` : "none";
|
|
19
|
+
|
|
20
|
+
// Show notification on change
|
|
21
|
+
if (source !== "restore") {
|
|
22
|
+
ctx.ui.notify(`Model: ${next}`, "info");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Update status bar with current model
|
|
26
|
+
ctx.ui.setStatus("model", `🤖 ${model.id}`);
|
|
27
|
+
|
|
28
|
+
// Log change details (visible in debug output)
|
|
29
|
+
console.log(`[model_select] ${prev} → ${next} (${source})`);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Notify Extension
|
|
3
|
+
*
|
|
4
|
+
* Sends a native terminal notification when Pi agent is done and waiting for input.
|
|
5
|
+
* Supports multiple terminal protocols:
|
|
6
|
+
* - OSC 777: Ghostty, iTerm2, WezTerm, rxvt-unicode
|
|
7
|
+
* - OSC 99: Kitty
|
|
8
|
+
* - Windows toast: Windows Terminal (WSL)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
function windowsToastScript(title: string, body: string): string {
|
|
14
|
+
const type = "Windows.UI.Notifications";
|
|
15
|
+
const mgr = `[${type}.ToastNotificationManager, ${type}, ContentType = WindowsRuntime]`;
|
|
16
|
+
const template = `[${type}.ToastTemplateType]::ToastText01`;
|
|
17
|
+
const toast = `[${type}.ToastNotification]::new($xml)`;
|
|
18
|
+
return [
|
|
19
|
+
`${mgr} > $null`,
|
|
20
|
+
`$xml = [${type}.ToastNotificationManager]::GetTemplateContent(${template})`,
|
|
21
|
+
`$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${body}')) > $null`,
|
|
22
|
+
`[${type}.ToastNotificationManager]::CreateToastNotifier('${title}').Show(${toast})`,
|
|
23
|
+
].join("; ");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function notifyOSC777(title: string, body: string): void {
|
|
27
|
+
process.stdout.write(`\x1b]777;notify;${title};${body}\x07`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function notifyOSC99(title: string, body: string): void {
|
|
31
|
+
// Kitty OSC 99: i=notification id, d=0 means not done yet, p=body for second part
|
|
32
|
+
process.stdout.write(`\x1b]99;i=1:d=0;${title}\x1b\\`);
|
|
33
|
+
process.stdout.write(`\x1b]99;i=1:p=body;${body}\x1b\\`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function notifyWindows(title: string, body: string): void {
|
|
37
|
+
const { execFile } = require("child_process");
|
|
38
|
+
execFile("powershell.exe", ["-NoProfile", "-Command", windowsToastScript(title, body)]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function notify(title: string, body: string): void {
|
|
42
|
+
if (process.env.WT_SESSION) {
|
|
43
|
+
notifyWindows(title, body);
|
|
44
|
+
} else if (process.env.KITTY_WINDOW_ID) {
|
|
45
|
+
notifyOSC99(title, body);
|
|
46
|
+
} else {
|
|
47
|
+
notifyOSC777(title, body);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function (pi: ExtensionAPI) {
|
|
52
|
+
pi.on("agent_end", async () => {
|
|
53
|
+
notify("Pi", "Ready for input");
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Plan Mode Extension
|
|
2
|
+
|
|
3
|
+
Read-only exploration mode for safe code analysis.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Built-in write tools disabled**: Disables edit/write while preserving other active tools
|
|
8
|
+
- **Bash allowlist**: Only read-only bash commands are allowed
|
|
9
|
+
- **Plan extraction**: Extracts numbered steps from `Plan:` sections
|
|
10
|
+
- **Progress tracking**: Widget shows completion status during execution
|
|
11
|
+
- **[DONE:n] markers**: Explicit step completion tracking
|
|
12
|
+
- **Session persistence**: State survives session resume
|
|
13
|
+
|
|
14
|
+
> Forked from the upstream `plan-mode` example: the slash command was
|
|
15
|
+
> renamed from `/plan` to `/plan-mode` so the `role-commands` extension can
|
|
16
|
+
> own `/plan` as a wrapper around the `planner` preset. See
|
|
17
|
+
> [`docs/extensions.md`](../../../docs/extensions.md#plan-mode-vs-plan) for
|
|
18
|
+
> when to use which.
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
- `/plan-mode` - Toggle plan mode
|
|
23
|
+
- `/todos` - Show current plan progress
|
|
24
|
+
- `Ctrl+Alt+P` - Toggle plan mode (shortcut)
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
1. Enable plan mode with `/plan-mode` or `--plan` flag
|
|
29
|
+
2. Ask the agent to analyze code and create a plan
|
|
30
|
+
3. The agent should output a numbered plan under a `Plan:` header:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Plan:
|
|
34
|
+
1. First step description
|
|
35
|
+
2. Second step description
|
|
36
|
+
3. Third step description
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
4. Choose "Execute the plan" when prompted
|
|
40
|
+
5. During execution, the agent marks steps complete with `[DONE:n]` tags
|
|
41
|
+
6. Progress widget shows completion status
|
|
42
|
+
|
|
43
|
+
## How It Works
|
|
44
|
+
|
|
45
|
+
### Plan Mode (Read-Only)
|
|
46
|
+
- Built-in edit/write tools disabled
|
|
47
|
+
- Other active tools remain available
|
|
48
|
+
- Bash commands filtered through allowlist
|
|
49
|
+
- Agent creates a plan without making changes
|
|
50
|
+
|
|
51
|
+
### Execution Mode
|
|
52
|
+
- Full tool access restored
|
|
53
|
+
- Agent executes steps in order
|
|
54
|
+
- `[DONE:n]` markers track completion
|
|
55
|
+
- Widget shows progress
|
|
56
|
+
|
|
57
|
+
### Command Allowlist
|
|
58
|
+
|
|
59
|
+
Safe commands (allowed):
|
|
60
|
+
- File inspection: `cat`, `head`, `tail`, `less`, `more`
|
|
61
|
+
- Search: `grep`, `find`, `rg`, `fd`
|
|
62
|
+
- Directory: `ls`, `pwd`, `tree`
|
|
63
|
+
- Git read: `git status`, `git log`, `git diff`, `git branch`
|
|
64
|
+
- Package info: `npm list`, `npm outdated`, `yarn info`
|
|
65
|
+
- System info: `uname`, `whoami`, `date`, `uptime`
|
|
66
|
+
|
|
67
|
+
Blocked commands:
|
|
68
|
+
- File modification: `rm`, `mv`, `cp`, `mkdir`, `touch`
|
|
69
|
+
- Git write: `git add`, `git commit`, `git push`
|
|
70
|
+
- Package install: `npm install`, `yarn add`, `pip install`
|
|
71
|
+
- System: `sudo`, `kill`, `reboot`
|
|
72
|
+
- Editors: `vim`, `nano`, `code`
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Mode Extension
|
|
3
|
+
*
|
|
4
|
+
* Read-only exploration mode for safe code analysis.
|
|
5
|
+
* When enabled, built-in write tools are disabled.
|
|
6
|
+
*
|
|
7
|
+
* Forked from upstream: the slash command was renamed `/plan` → `/plan-mode`
|
|
8
|
+
* so the `role-commands` extension can own `/plan` as a wrapper around the
|
|
9
|
+
* `planner` preset. See docs/extensions.md § "Plan mode vs /plan".
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - /plan-mode command or Ctrl+Alt+P to toggle
|
|
13
|
+
* - Bash restricted to allowlisted read-only commands
|
|
14
|
+
* - Extracts numbered plan steps from "Plan:" sections
|
|
15
|
+
* - [DONE:n] markers to complete steps during execution
|
|
16
|
+
* - Progress tracking widget during execution
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
20
|
+
import type { AssistantMessage, TextContent } from "@earendil-works/pi-ai";
|
|
21
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { Key } from "@earendil-works/pi-tui";
|
|
23
|
+
import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.ts";
|
|
24
|
+
|
|
25
|
+
// Tools
|
|
26
|
+
const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
|
|
27
|
+
const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"];
|
|
28
|
+
const PLAN_MODE_DISABLED_TOOLS = new Set<string>(["edit", "write"]);
|
|
29
|
+
const PLAN_MANAGED_TOOLS = new Set<string>([...PLAN_MODE_TOOLS, ...NORMAL_MODE_TOOLS]);
|
|
30
|
+
|
|
31
|
+
interface PlanModeState {
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
todos?: TodoItem[];
|
|
34
|
+
executing?: boolean;
|
|
35
|
+
toolsBeforePlanMode?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Type guard for assistant messages
|
|
39
|
+
function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
|
|
40
|
+
return m.role === "assistant" && Array.isArray(m.content);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Extract text content from an assistant message
|
|
44
|
+
function getTextContent(message: AssistantMessage): string {
|
|
45
|
+
return message.content
|
|
46
|
+
.filter((block): block is TextContent => block.type === "text")
|
|
47
|
+
.map((block) => block.text)
|
|
48
|
+
.join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function planModeExtension(pi: ExtensionAPI): void {
|
|
52
|
+
let planModeEnabled = false;
|
|
53
|
+
let executionMode = false;
|
|
54
|
+
let todoItems: TodoItem[] = [];
|
|
55
|
+
let toolsBeforePlanMode: string[] | undefined;
|
|
56
|
+
|
|
57
|
+
pi.registerFlag("plan", {
|
|
58
|
+
description: "Start in plan mode (read-only exploration)",
|
|
59
|
+
type: "boolean",
|
|
60
|
+
default: false,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function updateStatus(ctx: ExtensionContext): void {
|
|
64
|
+
// Footer status
|
|
65
|
+
if (executionMode && todoItems.length > 0) {
|
|
66
|
+
const completed = todoItems.filter((t) => t.completed).length;
|
|
67
|
+
ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("accent", `📋 ${completed}/${todoItems.length}`));
|
|
68
|
+
} else if (planModeEnabled) {
|
|
69
|
+
ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("warning", "⏸ plan"));
|
|
70
|
+
} else {
|
|
71
|
+
ctx.ui.setStatus("plan-mode", undefined);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Widget showing todo list
|
|
75
|
+
if (executionMode && todoItems.length > 0) {
|
|
76
|
+
const lines = todoItems.map((item) => {
|
|
77
|
+
if (item.completed) {
|
|
78
|
+
return (
|
|
79
|
+
ctx.ui.theme.fg("success", "☑ ") + ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(item.text))
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return `${ctx.ui.theme.fg("muted", "☐ ")}${item.text}`;
|
|
83
|
+
});
|
|
84
|
+
ctx.ui.setWidget("plan-todos", lines);
|
|
85
|
+
} else {
|
|
86
|
+
ctx.ui.setWidget("plan-todos", undefined);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function uniqueToolNames(toolNames: string[]): string[] {
|
|
91
|
+
return [...new Set(toolNames)];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getPlanModeTools(activeToolNames: string[]): string[] {
|
|
95
|
+
return uniqueToolNames([
|
|
96
|
+
...activeToolNames.filter((name) => !PLAN_MODE_DISABLED_TOOLS.has(name)),
|
|
97
|
+
...PLAN_MODE_TOOLS,
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getNormalModeTools(activeToolNames: string[]): string[] {
|
|
102
|
+
return uniqueToolNames([
|
|
103
|
+
...NORMAL_MODE_TOOLS,
|
|
104
|
+
...activeToolNames.filter((name) => !PLAN_MANAGED_TOOLS.has(name)),
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function enablePlanModeTools(): void {
|
|
109
|
+
if (toolsBeforePlanMode === undefined) {
|
|
110
|
+
toolsBeforePlanMode = pi.getActiveTools();
|
|
111
|
+
}
|
|
112
|
+
pi.setActiveTools(getPlanModeTools(toolsBeforePlanMode));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function restoreNormalModeTools(): void {
|
|
116
|
+
pi.setActiveTools(toolsBeforePlanMode ?? getNormalModeTools(pi.getActiveTools()));
|
|
117
|
+
toolsBeforePlanMode = undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function persistState(): void {
|
|
121
|
+
pi.appendEntry("plan-mode", {
|
|
122
|
+
enabled: planModeEnabled,
|
|
123
|
+
todos: todoItems,
|
|
124
|
+
executing: executionMode,
|
|
125
|
+
toolsBeforePlanMode,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function togglePlanMode(ctx: ExtensionContext): void {
|
|
130
|
+
planModeEnabled = !planModeEnabled;
|
|
131
|
+
executionMode = false;
|
|
132
|
+
todoItems = [];
|
|
133
|
+
|
|
134
|
+
if (planModeEnabled) {
|
|
135
|
+
enablePlanModeTools();
|
|
136
|
+
ctx.ui.notify("Plan mode enabled. Built-in write tools disabled.");
|
|
137
|
+
} else {
|
|
138
|
+
restoreNormalModeTools();
|
|
139
|
+
ctx.ui.notify("Plan mode disabled. Full access restored.");
|
|
140
|
+
}
|
|
141
|
+
updateStatus(ctx);
|
|
142
|
+
persistState();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// NOTE: renamed from /plan → /plan-mode so the role-commands extension can
|
|
146
|
+
// own /plan as a wrapper around the `planner` preset.
|
|
147
|
+
pi.registerCommand("plan-mode", {
|
|
148
|
+
description: "Toggle plan mode (read-only exploration)",
|
|
149
|
+
handler: async (_args, ctx) => togglePlanMode(ctx),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
pi.registerCommand("todos", {
|
|
153
|
+
description: "Show current plan todo list",
|
|
154
|
+
handler: async (_args, ctx) => {
|
|
155
|
+
if (todoItems.length === 0) {
|
|
156
|
+
ctx.ui.notify("No todos. Create a plan first with /plan-mode", "info");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const list = todoItems.map((item, i) => `${i + 1}. ${item.completed ? "✓" : "○"} ${item.text}`).join("\n");
|
|
160
|
+
ctx.ui.notify(`Plan Progress:\n${list}`, "info");
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
pi.registerShortcut(Key.ctrlAlt("p"), {
|
|
165
|
+
description: "Toggle plan mode",
|
|
166
|
+
handler: async (ctx) => togglePlanMode(ctx),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Block destructive bash commands in plan mode
|
|
170
|
+
pi.on("tool_call", async (event) => {
|
|
171
|
+
if (!planModeEnabled || event.toolName !== "bash") return;
|
|
172
|
+
|
|
173
|
+
const command = event.input.command as string;
|
|
174
|
+
if (!isSafeCommand(command)) {
|
|
175
|
+
return {
|
|
176
|
+
block: true,
|
|
177
|
+
reason: `Plan mode: command blocked (not allowlisted). Use /plan-mode to disable plan mode first.\nCommand: ${command}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Filter out stale plan mode context when not in plan mode
|
|
183
|
+
pi.on("context", async (event) => {
|
|
184
|
+
if (planModeEnabled) return;
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
messages: event.messages.filter((m) => {
|
|
188
|
+
const msg = m as AgentMessage & { customType?: string };
|
|
189
|
+
if (msg.customType === "plan-mode-context") return false;
|
|
190
|
+
if (msg.role !== "user") return true;
|
|
191
|
+
|
|
192
|
+
const content = msg.content;
|
|
193
|
+
if (typeof content === "string") {
|
|
194
|
+
return !content.includes("[PLAN MODE ACTIVE]");
|
|
195
|
+
}
|
|
196
|
+
if (Array.isArray(content)) {
|
|
197
|
+
return !content.some(
|
|
198
|
+
(c) => c.type === "text" && (c as TextContent).text?.includes("[PLAN MODE ACTIVE]"),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}),
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Inject plan/execution context before agent starts
|
|
207
|
+
pi.on("before_agent_start", async () => {
|
|
208
|
+
if (planModeEnabled) {
|
|
209
|
+
return {
|
|
210
|
+
message: {
|
|
211
|
+
customType: "plan-mode-context",
|
|
212
|
+
content: `[PLAN MODE ACTIVE]
|
|
213
|
+
You are in plan mode - a read-only exploration mode for safe code analysis.
|
|
214
|
+
|
|
215
|
+
Restrictions:
|
|
216
|
+
- Built-in edit and write tools are disabled
|
|
217
|
+
- Other currently active tools remain available
|
|
218
|
+
- Bash is restricted to an allowlist of read-only commands
|
|
219
|
+
|
|
220
|
+
Ask clarifying questions using the questionnaire tool.
|
|
221
|
+
Use brave-search skill via bash for web research.
|
|
222
|
+
|
|
223
|
+
Create a detailed numbered plan under a "Plan:" header:
|
|
224
|
+
|
|
225
|
+
Plan:
|
|
226
|
+
1. First step description
|
|
227
|
+
2. Second step description
|
|
228
|
+
...
|
|
229
|
+
|
|
230
|
+
Do NOT attempt to make changes - just describe what you would do.`,
|
|
231
|
+
display: false,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (executionMode && todoItems.length > 0) {
|
|
237
|
+
const remaining = todoItems.filter((t) => !t.completed);
|
|
238
|
+
const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join("\n");
|
|
239
|
+
return {
|
|
240
|
+
message: {
|
|
241
|
+
customType: "plan-execution-context",
|
|
242
|
+
content: `[EXECUTING PLAN - Full tool access enabled]
|
|
243
|
+
|
|
244
|
+
Remaining steps:
|
|
245
|
+
${todoList}
|
|
246
|
+
|
|
247
|
+
Execute each step in order.
|
|
248
|
+
After completing a step, include a [DONE:n] tag in your response.`,
|
|
249
|
+
display: false,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Track progress after each turn
|
|
256
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
257
|
+
if (!executionMode || todoItems.length === 0) return;
|
|
258
|
+
if (!isAssistantMessage(event.message)) return;
|
|
259
|
+
|
|
260
|
+
const text = getTextContent(event.message);
|
|
261
|
+
if (markCompletedSteps(text, todoItems) > 0) {
|
|
262
|
+
updateStatus(ctx);
|
|
263
|
+
}
|
|
264
|
+
persistState();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Handle plan completion and plan mode UI
|
|
268
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
269
|
+
// Check if execution is complete
|
|
270
|
+
if (executionMode && todoItems.length > 0) {
|
|
271
|
+
if (todoItems.every((t) => t.completed)) {
|
|
272
|
+
const completedList = todoItems.map((t) => `~~${t.text}~~`).join("\n");
|
|
273
|
+
pi.sendMessage(
|
|
274
|
+
{ customType: "plan-complete", content: `**Plan Complete!** ✓\n\n${completedList}`, display: true },
|
|
275
|
+
{ triggerTurn: false },
|
|
276
|
+
);
|
|
277
|
+
executionMode = false;
|
|
278
|
+
todoItems = [];
|
|
279
|
+
updateStatus(ctx);
|
|
280
|
+
persistState(); // Save cleared state so resume doesn't restore old execution mode
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!planModeEnabled || !ctx.hasUI) return;
|
|
286
|
+
|
|
287
|
+
// Extract todos from last assistant message
|
|
288
|
+
const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
|
|
289
|
+
if (lastAssistant) {
|
|
290
|
+
const extracted = extractTodoItems(getTextContent(lastAssistant));
|
|
291
|
+
if (extracted.length > 0) {
|
|
292
|
+
todoItems = extracted;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (todoItems.length === 0) return;
|
|
297
|
+
persistState();
|
|
298
|
+
|
|
299
|
+
// Show plan steps and prompt for next action
|
|
300
|
+
const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n");
|
|
301
|
+
const planTodoListMessage = {
|
|
302
|
+
customType: "plan-todo-list",
|
|
303
|
+
content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`,
|
|
304
|
+
display: true,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const choice = await ctx.ui.select("Plan mode - what next?", [
|
|
308
|
+
"Execute the plan (track progress)",
|
|
309
|
+
"Stay in plan mode",
|
|
310
|
+
"Refine the plan",
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
if (choice?.startsWith("Execute")) {
|
|
314
|
+
const firstTodoItem = todoItems[0];
|
|
315
|
+
if (!firstTodoItem) return;
|
|
316
|
+
|
|
317
|
+
planModeEnabled = false;
|
|
318
|
+
executionMode = true;
|
|
319
|
+
restoreNormalModeTools();
|
|
320
|
+
updateStatus(ctx);
|
|
321
|
+
persistState();
|
|
322
|
+
|
|
323
|
+
const remainingList = todoItems.map((t) => `${t.step}. ${t.text}`).join("\n");
|
|
324
|
+
const execMessage = `Execute the plan.
|
|
325
|
+
|
|
326
|
+
Remaining steps:
|
|
327
|
+
${remainingList}
|
|
328
|
+
|
|
329
|
+
Start with: ${firstTodoItem.text}
|
|
330
|
+
After completing a step, include a [DONE:n] tag in your response.`;
|
|
331
|
+
pi.sendMessage(planTodoListMessage, { deliverAs: "followUp" });
|
|
332
|
+
pi.sendMessage(
|
|
333
|
+
{ customType: "plan-mode-execute", content: execMessage, display: true },
|
|
334
|
+
{ triggerTurn: true, deliverAs: "followUp" },
|
|
335
|
+
);
|
|
336
|
+
} else if (choice === "Refine the plan") {
|
|
337
|
+
const refinement = await ctx.ui.editor("Refine the plan:", "");
|
|
338
|
+
if (refinement?.trim()) {
|
|
339
|
+
pi.sendMessage(planTodoListMessage, { deliverAs: "followUp" });
|
|
340
|
+
pi.sendUserMessage(refinement.trim(), { deliverAs: "followUp" });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Restore state on session start/resume
|
|
346
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
347
|
+
if (pi.getFlag("plan") === true) {
|
|
348
|
+
planModeEnabled = true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const entries = ctx.sessionManager.getEntries();
|
|
352
|
+
|
|
353
|
+
// Restore persisted state
|
|
354
|
+
const planModeEntry = entries
|
|
355
|
+
.filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode")
|
|
356
|
+
.pop() as { data?: PlanModeState } | undefined;
|
|
357
|
+
|
|
358
|
+
if (planModeEntry?.data) {
|
|
359
|
+
planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled;
|
|
360
|
+
todoItems = planModeEntry.data.todos ?? todoItems;
|
|
361
|
+
executionMode = planModeEntry.data.executing ?? executionMode;
|
|
362
|
+
toolsBeforePlanMode = planModeEntry.data.toolsBeforePlanMode ?? toolsBeforePlanMode;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// On resume: re-scan messages to rebuild completion state
|
|
366
|
+
// Only scan messages AFTER the last "plan-mode-execute" to avoid picking up [DONE:n] from previous plans
|
|
367
|
+
const isResume = planModeEntry !== undefined;
|
|
368
|
+
if (isResume && executionMode && todoItems.length > 0) {
|
|
369
|
+
// Find the index of the last plan-mode-execute entry (marks when current execution started)
|
|
370
|
+
let executeIndex = -1;
|
|
371
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
372
|
+
const entry = entries[i] as { type: string; customType?: string };
|
|
373
|
+
if (entry.customType === "plan-mode-execute") {
|
|
374
|
+
executeIndex = i;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Only scan messages after the execute marker
|
|
380
|
+
const messages: AssistantMessage[] = [];
|
|
381
|
+
for (let i = executeIndex + 1; i < entries.length; i++) {
|
|
382
|
+
const entry = entries[i];
|
|
383
|
+
if (entry.type === "message" && "message" in entry && isAssistantMessage(entry.message as AgentMessage)) {
|
|
384
|
+
messages.push(entry.message as AssistantMessage);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const allText = messages.map(getTextContent).join("\n");
|
|
388
|
+
markCompletedSteps(allText, todoItems);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (planModeEnabled) {
|
|
392
|
+
enablePlanModeTools();
|
|
393
|
+
}
|
|
394
|
+
updateStatus(ctx);
|
|
395
|
+
});
|
|
396
|
+
}
|