@dungle-scrubs/tallow 0.8.28 → 0.9.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/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +2 -9
- package/dist/install.js.map +1 -1
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +20 -9
- package/dist/interactive-mode-patch.js.map +1 -1
- package/extensions/_icons/__tests__/icons.test.ts +0 -1
- package/extensions/_icons/index.ts +0 -2
- package/extensions/context-fork/__tests__/context-fork.test.ts +9 -0
- package/extensions/health/index.ts +1 -1
- package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +42 -0
- package/extensions/render-stabilizer/extension.json +5 -0
- package/extensions/render-stabilizer/index.ts +66 -0
- package/extensions/subagent-tool/__tests__/auto-cheap-model.test.ts +66 -6
- package/extensions/subagent-tool/__tests__/model-router-explicit-resolution.test.ts +79 -5
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +47 -0
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +139 -5
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +142 -5
- package/package.json +1 -1
- package/schemas/settings.schema.json +0 -5
- package/extensions/plan-mode-tool/__tests__/e2e.mjs +0 -350
- package/extensions/plan-mode-tool/__tests__/index.test.ts +0 -213
- package/extensions/plan-mode-tool/__tests__/utils.test.ts +0 -381
- package/extensions/plan-mode-tool/extension.json +0 -22
- package/extensions/plan-mode-tool/index.ts +0 -583
- package/extensions/plan-mode-tool/utils.ts +0 -257
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import { Type } from "@sinclair/typebox";
|
|
4
|
-
import { ExtensionHarness } from "../../../test-utils/extension-harness.js";
|
|
5
|
-
import planModeExtension from "../index.js";
|
|
6
|
-
import { PLAN_MODE_ALLOWED_TOOLS } from "../utils.js";
|
|
7
|
-
|
|
8
|
-
const BASELINE_TOOLS = [
|
|
9
|
-
"read",
|
|
10
|
-
"bash",
|
|
11
|
-
"grep",
|
|
12
|
-
"find",
|
|
13
|
-
"ls",
|
|
14
|
-
"edit",
|
|
15
|
-
"write",
|
|
16
|
-
"subagent",
|
|
17
|
-
"bg_bash",
|
|
18
|
-
"mcp__mock__ping",
|
|
19
|
-
"questionnaire",
|
|
20
|
-
"plan_mode",
|
|
21
|
-
] as const;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Register mock tools used to test plan-mode gating and restoration.
|
|
25
|
-
*
|
|
26
|
-
* @param pi - Extension API test double
|
|
27
|
-
* @returns void
|
|
28
|
-
*/
|
|
29
|
-
function registerMockTools(pi: ExtensionAPI): void {
|
|
30
|
-
const names = [
|
|
31
|
-
"read",
|
|
32
|
-
"bash",
|
|
33
|
-
"grep",
|
|
34
|
-
"find",
|
|
35
|
-
"ls",
|
|
36
|
-
"edit",
|
|
37
|
-
"write",
|
|
38
|
-
"subagent",
|
|
39
|
-
"bg_bash",
|
|
40
|
-
"mcp__mock__ping",
|
|
41
|
-
"questionnaire",
|
|
42
|
-
] as const;
|
|
43
|
-
|
|
44
|
-
for (const name of names) {
|
|
45
|
-
pi.registerTool({
|
|
46
|
-
name,
|
|
47
|
-
label: name,
|
|
48
|
-
description: `Mock ${name}`,
|
|
49
|
-
parameters: Type.Object({}),
|
|
50
|
-
async execute() {
|
|
51
|
-
return {
|
|
52
|
-
content: [{ type: "text", text: `${name}-ok` }],
|
|
53
|
-
details: {},
|
|
54
|
-
};
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Create an extension context with optional persisted session entries.
|
|
62
|
-
*
|
|
63
|
-
* @param entries - Session entries returned by sessionManager.getEntries
|
|
64
|
-
* @returns Context object compatible with extension handlers
|
|
65
|
-
*/
|
|
66
|
-
function createContext(entries: unknown[] = [], hasUI = true): ExtensionContext {
|
|
67
|
-
return {
|
|
68
|
-
cwd: process.cwd(),
|
|
69
|
-
hasUI,
|
|
70
|
-
ui: {
|
|
71
|
-
notify() {},
|
|
72
|
-
setStatus() {},
|
|
73
|
-
setEditorComponent() {},
|
|
74
|
-
setWidget() {},
|
|
75
|
-
theme: {
|
|
76
|
-
fg(_token: string, value: string) {
|
|
77
|
-
return value;
|
|
78
|
-
},
|
|
79
|
-
strikethrough(value: string) {
|
|
80
|
-
return value;
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
} as never,
|
|
84
|
-
sessionManager: {
|
|
85
|
-
getEntries() {
|
|
86
|
-
return entries;
|
|
87
|
-
},
|
|
88
|
-
} as never,
|
|
89
|
-
} as unknown as ExtensionContext;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Resolve a registered tool from the test harness.
|
|
94
|
-
*
|
|
95
|
-
* @param harness - Extension harness
|
|
96
|
-
* @param name - Tool name
|
|
97
|
-
* @returns Tool definition
|
|
98
|
-
*/
|
|
99
|
-
function getTool(harness: ExtensionHarness, name: string): ToolDefinition {
|
|
100
|
-
const tool = harness.tools.get(name);
|
|
101
|
-
if (!tool) throw new Error(`Tool not registered: ${name}`);
|
|
102
|
-
return tool;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
describe("plan-mode strict readonly enforcement", () => {
|
|
106
|
-
let harness: ExtensionHarness;
|
|
107
|
-
|
|
108
|
-
beforeEach(async () => {
|
|
109
|
-
harness = ExtensionHarness.create();
|
|
110
|
-
await harness.loadExtension(registerMockTools);
|
|
111
|
-
await harness.loadExtension(planModeExtension);
|
|
112
|
-
harness.api.setActiveTools([...BASELINE_TOOLS]);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("enable applies strict allowlist and disable restores previous tools", async () => {
|
|
116
|
-
const tool = getTool(harness, "plan_mode");
|
|
117
|
-
const ctx = createContext();
|
|
118
|
-
|
|
119
|
-
await tool.execute("tc-enable", { action: "enable" }, undefined, () => {}, ctx);
|
|
120
|
-
expect(harness.api.getActiveTools()).toEqual(
|
|
121
|
-
PLAN_MODE_ALLOWED_TOOLS.filter((name) => BASELINE_TOOLS.includes(name))
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
await tool.execute("tc-disable", { action: "disable" }, undefined, () => {}, ctx);
|
|
125
|
-
expect(harness.api.getActiveTools()).toEqual([...BASELINE_TOOLS]);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("tool_call blocks non-allowlisted tools and unsafe bash", async () => {
|
|
129
|
-
const tool = getTool(harness, "plan_mode");
|
|
130
|
-
const ctx = createContext();
|
|
131
|
-
await tool.execute("tc-enable", { action: "enable" }, undefined, () => {}, ctx);
|
|
132
|
-
|
|
133
|
-
const [blockedToolResult] = await harness.fireEvent(
|
|
134
|
-
"tool_call",
|
|
135
|
-
{ toolName: "subagent", input: { task: "x" } },
|
|
136
|
-
ctx
|
|
137
|
-
);
|
|
138
|
-
expect(blockedToolResult).toMatchObject({ block: true });
|
|
139
|
-
expect((blockedToolResult as { reason: string }).reason).toContain('tool "subagent" blocked');
|
|
140
|
-
|
|
141
|
-
const [safeBashResult] = await harness.fireEvent(
|
|
142
|
-
"tool_call",
|
|
143
|
-
{ toolName: "bash", input: { command: "ls -la" } },
|
|
144
|
-
ctx
|
|
145
|
-
);
|
|
146
|
-
expect(safeBashResult).toBeUndefined();
|
|
147
|
-
|
|
148
|
-
const [unsafeBashResult] = await harness.fireEvent(
|
|
149
|
-
"tool_call",
|
|
150
|
-
{ toolName: "bash", input: { command: "rm -rf /tmp/nope" } },
|
|
151
|
-
ctx
|
|
152
|
-
);
|
|
153
|
-
expect(unsafeBashResult).toMatchObject({ block: true });
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("resumed plan mode re-applies strict policy", async () => {
|
|
157
|
-
const persistedEntries = [
|
|
158
|
-
{
|
|
159
|
-
type: "custom",
|
|
160
|
-
customType: "plan-mode",
|
|
161
|
-
data: {
|
|
162
|
-
enabled: true,
|
|
163
|
-
normalTools: [...BASELINE_TOOLS],
|
|
164
|
-
todos: [],
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
];
|
|
168
|
-
const ctx = createContext(persistedEntries);
|
|
169
|
-
|
|
170
|
-
await harness.fireEvent("session_start", { type: "session_start" }, ctx);
|
|
171
|
-
|
|
172
|
-
expect(harness.api.getActiveTools()).toEqual(
|
|
173
|
-
PLAN_MODE_ALLOWED_TOOLS.filter((name) => BASELINE_TOOLS.includes(name))
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const [blockedResult] = await harness.fireEvent(
|
|
177
|
-
"tool_call",
|
|
178
|
-
{ toolName: "bg_bash", input: { command: "echo hi" } },
|
|
179
|
-
ctx
|
|
180
|
-
);
|
|
181
|
-
expect(blockedResult).toMatchObject({ block: true });
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test("auto-enable only triggers for interactive UI input", async () => {
|
|
185
|
-
const [result] = await harness.fireEvent(
|
|
186
|
-
"input",
|
|
187
|
-
{ source: "interactive", text: "plan only fix auth" },
|
|
188
|
-
createContext([], true)
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
expect(result).toEqual({ action: "transform", text: "fix auth" });
|
|
192
|
-
expect(harness.api.getActiveTools()).toEqual(
|
|
193
|
-
PLAN_MODE_ALLOWED_TOOLS.filter((name) => BASELINE_TOOLS.includes(name))
|
|
194
|
-
);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("auto-enable ignores headless or non-interactive input", async () => {
|
|
198
|
-
const [headlessResult] = await harness.fireEvent(
|
|
199
|
-
"input",
|
|
200
|
-
{ source: "interactive", text: "plan only fix auth" },
|
|
201
|
-
createContext([], false)
|
|
202
|
-
);
|
|
203
|
-
const [rpcResult] = await harness.fireEvent(
|
|
204
|
-
"input",
|
|
205
|
-
{ source: "rpc", text: "plan only fix auth" },
|
|
206
|
-
createContext([], true)
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
expect(headlessResult).toEqual({ action: "continue" });
|
|
210
|
-
expect(rpcResult).toEqual({ action: "continue" });
|
|
211
|
-
expect(harness.api.getActiveTools()).toEqual([...BASELINE_TOOLS]);
|
|
212
|
-
});
|
|
213
|
-
});
|
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
cleanStepText,
|
|
4
|
-
detectPlanIntent,
|
|
5
|
-
extractTodoItems,
|
|
6
|
-
isPlanModeToolAllowed,
|
|
7
|
-
isSafeCommand,
|
|
8
|
-
PLAN_MODE_ALLOWED_TOOLS,
|
|
9
|
-
stripPlanIntent,
|
|
10
|
-
} from "../utils.js";
|
|
11
|
-
|
|
12
|
-
describe("isPlanModeToolAllowed", () => {
|
|
13
|
-
test("allows explicitly allowlisted tools", () => {
|
|
14
|
-
expect(PLAN_MODE_ALLOWED_TOOLS.length).toBeGreaterThan(0);
|
|
15
|
-
expect(isPlanModeToolAllowed("read")).toBe(true);
|
|
16
|
-
expect(isPlanModeToolAllowed("bash")).toBe(true);
|
|
17
|
-
expect(isPlanModeToolAllowed("plan_mode")).toBe(true);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test("blocks non-allowlisted tools fail-closed", () => {
|
|
21
|
-
expect(isPlanModeToolAllowed("edit")).toBe(false);
|
|
22
|
-
expect(isPlanModeToolAllowed("write")).toBe(false);
|
|
23
|
-
expect(isPlanModeToolAllowed("bg_bash")).toBe(false);
|
|
24
|
-
expect(isPlanModeToolAllowed("subagent")).toBe(false);
|
|
25
|
-
expect(isPlanModeToolAllowed("mcp__github__create_issue")).toBe(false);
|
|
26
|
-
expect(isPlanModeToolAllowed("totally_unknown_tool")).toBe(false);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("isSafeCommand", () => {
|
|
31
|
-
test("allows read-only file inspection commands", () => {
|
|
32
|
-
expect(isSafeCommand("cat README.md")).toBe(true);
|
|
33
|
-
expect(isSafeCommand("head -n 20 file.ts")).toBe(true);
|
|
34
|
-
expect(isSafeCommand("tail -f log.txt")).toBe(true);
|
|
35
|
-
expect(isSafeCommand("less config.json")).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("allows search commands", () => {
|
|
39
|
-
expect(isSafeCommand("grep -r 'TODO' src/")).toBe(true);
|
|
40
|
-
expect(isSafeCommand("find . -name '*.ts'")).toBe(true);
|
|
41
|
-
expect(isSafeCommand("rg 'pattern' --type ts")).toBe(true);
|
|
42
|
-
expect(isSafeCommand("fd '*.json'")).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("allows directory listing commands", () => {
|
|
46
|
-
expect(isSafeCommand("ls -la")).toBe(true);
|
|
47
|
-
expect(isSafeCommand("pwd")).toBe(true);
|
|
48
|
-
expect(isSafeCommand("tree src/")).toBe(true);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("allows git read commands", () => {
|
|
52
|
-
expect(isSafeCommand("git status")).toBe(true);
|
|
53
|
-
expect(isSafeCommand("git log --oneline -10")).toBe(true);
|
|
54
|
-
expect(isSafeCommand("git diff HEAD~1")).toBe(true);
|
|
55
|
-
expect(isSafeCommand("git branch -a")).toBe(true);
|
|
56
|
-
expect(isSafeCommand("git show HEAD")).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("allows system info commands", () => {
|
|
60
|
-
expect(isSafeCommand("uname -a")).toBe(true);
|
|
61
|
-
expect(isSafeCommand("whoami")).toBe(true);
|
|
62
|
-
expect(isSafeCommand("date")).toBe(true);
|
|
63
|
-
expect(isSafeCommand("uptime")).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("allows package info commands", () => {
|
|
67
|
-
expect(isSafeCommand("npm list")).toBe(true);
|
|
68
|
-
expect(isSafeCommand("npm outdated")).toBe(true);
|
|
69
|
-
expect(isSafeCommand("yarn info react")).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("blocks file modification commands", () => {
|
|
73
|
-
expect(isSafeCommand("rm -rf node_modules")).toBe(false);
|
|
74
|
-
expect(isSafeCommand("mv file.ts other.ts")).toBe(false);
|
|
75
|
-
expect(isSafeCommand("cp src/ dst/")).toBe(false);
|
|
76
|
-
expect(isSafeCommand("mkdir new-dir")).toBe(false);
|
|
77
|
-
expect(isSafeCommand("touch new-file.ts")).toBe(false);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("blocks git write commands", () => {
|
|
81
|
-
expect(isSafeCommand("git add .")).toBe(false);
|
|
82
|
-
expect(isSafeCommand("git commit -m 'msg'")).toBe(false);
|
|
83
|
-
expect(isSafeCommand("git push origin main")).toBe(false);
|
|
84
|
-
expect(isSafeCommand("git pull")).toBe(false);
|
|
85
|
-
expect(isSafeCommand("git rebase main")).toBe(false);
|
|
86
|
-
expect(isSafeCommand("git reset --hard")).toBe(false);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("blocks package install commands", () => {
|
|
90
|
-
expect(isSafeCommand("npm install lodash")).toBe(false);
|
|
91
|
-
expect(isSafeCommand("yarn add react")).toBe(false);
|
|
92
|
-
expect(isSafeCommand("pnpm add zod")).toBe(false);
|
|
93
|
-
expect(isSafeCommand("pip install requests")).toBe(false);
|
|
94
|
-
expect(isSafeCommand("brew install ripgrep")).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("blocks shell redirections", () => {
|
|
98
|
-
expect(isSafeCommand("echo hello > file.txt")).toBe(false);
|
|
99
|
-
expect(isSafeCommand("cat a >> b")).toBe(false);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("blocks privilege escalation and system commands", () => {
|
|
103
|
-
expect(isSafeCommand("sudo rm -rf /")).toBe(false);
|
|
104
|
-
expect(isSafeCommand("kill -9 1234")).toBe(false);
|
|
105
|
-
expect(isSafeCommand("reboot")).toBe(false);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("blocks editors", () => {
|
|
109
|
-
expect(isSafeCommand("vim file.ts")).toBe(false);
|
|
110
|
-
expect(isSafeCommand("nano config.json")).toBe(false);
|
|
111
|
-
expect(isSafeCommand("code .")).toBe(false);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("rejects unknown commands", () => {
|
|
115
|
-
expect(isSafeCommand("some-random-binary --flag")).toBe(false);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe("cleanStepText", () => {
|
|
120
|
-
test("removes bold markdown", () => {
|
|
121
|
-
expect(cleanStepText("**Important step**")).toBe("Important step");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("removes inline code", () => {
|
|
125
|
-
expect(cleanStepText("Update `config.json` file")).toBe("Config.json file");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("strips action verb prefixes", () => {
|
|
129
|
-
expect(cleanStepText("Create the database schema")).toBe("Database schema");
|
|
130
|
-
expect(cleanStepText("Read the configuration file")).toBe("Configuration file");
|
|
131
|
-
expect(cleanStepText("Install the dependencies")).toBe("Dependencies");
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("capitalizes first character", () => {
|
|
135
|
-
expect(cleanStepText("some lowercase text here")).toBe("Some lowercase text here");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("truncates long text to 50 chars", () => {
|
|
139
|
-
const long =
|
|
140
|
-
"This is a very long step description that should be truncated at fifty characters";
|
|
141
|
-
const result = cleanStepText(long);
|
|
142
|
-
expect(result.length).toBeLessThanOrEqual(50);
|
|
143
|
-
expect(result).toEndWith("...");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("collapses whitespace", () => {
|
|
147
|
-
expect(cleanStepText("too many spaces")).toBe("Too many spaces");
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe("extractTodoItems", () => {
|
|
152
|
-
test("extracts numbered steps after Plan: header", () => {
|
|
153
|
-
const message = `Here's what I'll do:
|
|
154
|
-
|
|
155
|
-
Plan:
|
|
156
|
-
1. Analyze the codebase structure
|
|
157
|
-
2. Review the configuration files
|
|
158
|
-
3. Check the test coverage
|
|
159
|
-
|
|
160
|
-
Let me know if this works.`;
|
|
161
|
-
|
|
162
|
-
const items = extractTodoItems(message);
|
|
163
|
-
expect(items).toHaveLength(3);
|
|
164
|
-
expect(items[0].step).toBe(1);
|
|
165
|
-
expect(items[0].completed).toBe(false);
|
|
166
|
-
expect(items[1].step).toBe(2);
|
|
167
|
-
expect(items[2].step).toBe(3);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("handles bold Plan: header", () => {
|
|
171
|
-
const message = `**Plan:**
|
|
172
|
-
1. First step description here
|
|
173
|
-
2. Second step description here`;
|
|
174
|
-
|
|
175
|
-
const items = extractTodoItems(message);
|
|
176
|
-
expect(items).toHaveLength(2);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test("returns empty for messages without Plan: header", () => {
|
|
180
|
-
const message = "Here are some numbered things:\n1. Item one\n2. Item two";
|
|
181
|
-
expect(extractTodoItems(message)).toHaveLength(0);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test("filters out short steps", () => {
|
|
185
|
-
const message = `Plan:
|
|
186
|
-
1. Do X
|
|
187
|
-
2. Analyze the complete project structure thoroughly`;
|
|
188
|
-
|
|
189
|
-
const items = extractTodoItems(message);
|
|
190
|
-
// "Do X" is too short (<=5 chars), should be filtered
|
|
191
|
-
expect(items).toHaveLength(1);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("steps are numbered sequentially from 1", () => {
|
|
195
|
-
const message = `Plan:
|
|
196
|
-
1. First real step here please
|
|
197
|
-
5. Fifth but actually second
|
|
198
|
-
10. Tenth but actually third`;
|
|
199
|
-
|
|
200
|
-
const items = extractTodoItems(message);
|
|
201
|
-
expect(items.map((i) => i.step)).toEqual([1, 2, 3]);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe("detectPlanIntent", () => {
|
|
206
|
-
// ── True positives ──────────────────────────────────────────────
|
|
207
|
-
test("detects 'plan only'", () => {
|
|
208
|
-
expect(detectPlanIntent("plan only")).toBe(true);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test("detects 'plan-only' (hyphenated)", () => {
|
|
212
|
-
expect(detectPlanIntent("this is plan-only")).toBe(true);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("detects 'just plan'", () => {
|
|
216
|
-
expect(detectPlanIntent("just plan for now")).toBe(true);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test("detects 'only plan'", () => {
|
|
220
|
-
expect(detectPlanIntent("only plan, don't execute")).toBe(true);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test("detects 'plan mode' as directive", () => {
|
|
224
|
-
expect(detectPlanIntent("plan mode please")).toBe(true);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test("detects 'planning mode'", () => {
|
|
228
|
-
expect(detectPlanIntent("planning mode please")).toBe(true);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("detects 'don't implement'", () => {
|
|
232
|
-
expect(detectPlanIntent("don't implement yet")).toBe(true);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test("detects curly apostrophe 'don\u2019t implement'", () => {
|
|
236
|
-
expect(detectPlanIntent("don\u2019t implement yet")).toBe(true);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test("detects 'do not implement'", () => {
|
|
240
|
-
expect(detectPlanIntent("do not implement")).toBe(true);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test("detects 'don't code yet'", () => {
|
|
244
|
-
expect(detectPlanIntent("don't code yet")).toBe(true);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test("detects 'don't make changes'", () => {
|
|
248
|
-
expect(detectPlanIntent("don't make changes")).toBe(true);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test("detects 'do not make changes'", () => {
|
|
252
|
-
expect(detectPlanIntent("do not make changes")).toBe(true);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
test("detects 'no implementation yet'", () => {
|
|
256
|
-
expect(detectPlanIntent("no implementation yet")).toBe(true);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test("detects 'no changes first'", () => {
|
|
260
|
-
expect(detectPlanIntent("no changes first")).toBe(true);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test("detects 'read-only mode'", () => {
|
|
264
|
-
expect(detectPlanIntent("read-only mode")).toBe(true);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
test("detects 'read only mode' (no hyphen)", () => {
|
|
268
|
-
expect(detectPlanIntent("read only mode")).toBe(true);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test("detects 'this is plan'", () => {
|
|
272
|
-
expect(detectPlanIntent("this is plan")).toBe(true);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test("detects 'this is planning'", () => {
|
|
276
|
-
expect(detectPlanIntent("this is planning")).toBe(true);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("detects 'plan first'", () => {
|
|
280
|
-
expect(detectPlanIntent("plan first")).toBe(true);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
test("detects 'plan before'", () => {
|
|
284
|
-
expect(detectPlanIntent("plan before implementing")).toBe(true);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test("detects the exact user complaint: 'not yet, this is plan only'", () => {
|
|
288
|
-
expect(detectPlanIntent("not yet, this is plan only")).toBe(true);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test("is case-insensitive", () => {
|
|
292
|
-
expect(detectPlanIntent("Plan Only")).toBe(true);
|
|
293
|
-
expect(detectPlanIntent("PLAN MODE")).toBe(true);
|
|
294
|
-
expect(detectPlanIntent("DON'T IMPLEMENT")).toBe(true);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test("detects intent mixed with a request", () => {
|
|
298
|
-
expect(detectPlanIntent("don't implement, just review the auth flow")).toBe(true);
|
|
299
|
-
expect(detectPlanIntent("analyze the database schema, plan only")).toBe(true);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// ── True negatives ──────────────────────────────────────────────
|
|
303
|
-
test("does NOT match 'make a plan for the API' (noun usage)", () => {
|
|
304
|
-
expect(detectPlanIntent("make a plan for the API")).toBe(false);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test("does NOT match 'what does plan mode do?' (question about plan mode)", () => {
|
|
308
|
-
expect(detectPlanIntent("what does plan mode do?")).toBe(false);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
test("does NOT match 'how does plan mode work?' (question)", () => {
|
|
312
|
-
expect(detectPlanIntent("how does plan mode work?")).toBe(false);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
test("does NOT match 'execute the plan' (opposite intent)", () => {
|
|
316
|
-
expect(detectPlanIntent("execute the plan")).toBe(false);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test("does NOT match 'the implementation plan looks good' (plan as noun)", () => {
|
|
320
|
-
expect(detectPlanIntent("the implementation plan looks good")).toBe(false);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
test("does NOT match 'plan' alone (too ambiguous)", () => {
|
|
324
|
-
expect(detectPlanIntent("plan")).toBe(false);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
test("does NOT match empty string", () => {
|
|
328
|
-
expect(detectPlanIntent("")).toBe(false);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
test("does NOT match 'the plan is to refactor auth' (noun usage)", () => {
|
|
332
|
-
expect(detectPlanIntent("the plan is to refactor auth")).toBe(false);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
test("does NOT match 'I planned the migration' (past tense)", () => {
|
|
336
|
-
expect(detectPlanIntent("I planned the migration")).toBe(false);
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
describe("stripPlanIntent", () => {
|
|
341
|
-
test("strips 'don't implement' and keeps the request", () => {
|
|
342
|
-
expect(stripPlanIntent("don't implement, just review the auth flow")).toBe(
|
|
343
|
-
"just review the auth flow"
|
|
344
|
-
);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
test("returns original when stripping leaves empty string", () => {
|
|
348
|
-
expect(stripPlanIntent("plan only")).toBe("plan only");
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
test("strips 'this is plan only' prefix from mixed input", () => {
|
|
352
|
-
expect(stripPlanIntent("this is plan only, analyze the database schema")).toBe(
|
|
353
|
-
"analyze the database schema"
|
|
354
|
-
);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
test("strips 'plan mode' from mixed input", () => {
|
|
358
|
-
expect(stripPlanIntent("plan mode — review the auth module")).toBe("review the auth module");
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
test("strips 'do not make changes' and cleans punctuation", () => {
|
|
362
|
-
expect(stripPlanIntent("do not make changes, review the config")).toBe("review the config");
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
test("cleans up double spaces after stripping", () => {
|
|
366
|
-
expect(stripPlanIntent("please plan only review auth")).toBe("please review auth");
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
test("handles multiple intent phrases in one message", () => {
|
|
370
|
-
const result = stripPlanIntent("plan only, don't implement, analyze the code");
|
|
371
|
-
expect(result).toBe("analyze the code");
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
test("returns original when entire message is intent", () => {
|
|
375
|
-
expect(stripPlanIntent("just plan")).toBe("just plan");
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
test("returns original for empty string", () => {
|
|
379
|
-
expect(stripPlanIntent("")).toBe("");
|
|
380
|
-
});
|
|
381
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "plan-mode-tool",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"description": "Plan mode — structured planning before execution",
|
|
5
|
-
"whenToUse": "Use when you want a planning-first, read-only workflow before execution.",
|
|
6
|
-
"capabilities": {
|
|
7
|
-
"tools": ["plan_mode"],
|
|
8
|
-
"commands": ["plan-mode", "todos"],
|
|
9
|
-
"events": ["agent_end", "before_agent_start", "context", "input", "session_start", "tool_call"]
|
|
10
|
-
},
|
|
11
|
-
"permissionSurface": {
|
|
12
|
-
"filesystem": "none",
|
|
13
|
-
"shell": false,
|
|
14
|
-
"network": false,
|
|
15
|
-
"subprocess": false
|
|
16
|
-
},
|
|
17
|
-
"category": "tool",
|
|
18
|
-
"tags": ["planning", "workflow"],
|
|
19
|
-
"files": ["index.ts", "utils.ts"],
|
|
20
|
-
"relationships": [],
|
|
21
|
-
"npmDependencies": {}
|
|
22
|
-
}
|