mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
d26289060d
* fix(pi): preserve extension tool selections * fix(pi): release phase tools when the plan completes The agent_end plan-completion path dropped back to idle via restoreSavedState but never called releaseAddedPhaseTools(), so tools Plannotator activated for the executing phase stayed active in idle and a stale phaseAddedTools list was persisted. Mirror exitToIdle. Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS * fix(pi): keep grep/find/ls in the default planning tool set Dropping them from the planning phase profile silently removed native search from default planning sessions. Restore them as default planning phase tools so they now flow through the new tracking mechanism: they are recorded in phaseAddedTools and released on phase exit, and a discovery tool another extension already activated is neither claimed nor removed. A user's own planning.activeTools still overrides them. Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS * fix(pi): always keep the submit tool active during planning Planning tools now come purely from the resolved profile's activeTools, which uses replace semantics: a phases.planning.activeTools in ~/.pi/agent/plannotator.json or cwd/.pi/plannotator.json supersedes the built-in list wholesale. Pi's setActiveToolsByName then replaces the session's tool list outright, so such a config silently dropped plannotator_submit_plan while the planning system prompt kept telling the model to call it - a dead end with no way to submit a plan. Union PLAN_SUBMIT_TOOL into the configured list for the planning phase only (deduped if the config already lists it). It flows through phaseAddedTools like any other addition, so it is released normally on phase exit. Other tools are left to the user: overriding planning.activeTools is a deliberate choice of tool set, and only the submit tool is contractually required for the phase to function. Also corrects the README's "extra tools ... additive" wording, which described the old merge behavior. Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS --------- Co-authored-by: Michael Ramos <support@plannotator.ai> Co-authored-by: Michael Ramos <mdramos8@gmail.com>
81 lines
2.6 KiB
TypeScript
81 lines
2.6 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
applyPhaseTools,
|
|
isPlanWritePathAllowed,
|
|
releasePhaseTools,
|
|
} from "./tool-scope.ts";
|
|
|
|
describe("pi plan tool scoping", () => {
|
|
test("adds configured phase tools without replacing the active tools", () => {
|
|
expect(
|
|
applyPhaseTools(["inspect", "search"], [], ["search", "submit_plan"]),
|
|
).toEqual({
|
|
activeTools: ["inspect", "search", "submit_plan"],
|
|
addedTools: ["submit_plan"],
|
|
});
|
|
});
|
|
|
|
test("changes phases without restoring tools removed by another extension", () => {
|
|
expect(
|
|
applyPhaseTools(
|
|
["inspect", "external_new", "submit_plan"],
|
|
["submit_plan", "missing_phase_tool"],
|
|
["execution_progress"],
|
|
),
|
|
).toEqual({
|
|
activeTools: ["inspect", "external_new", "execution_progress"],
|
|
addedTools: ["execution_progress"],
|
|
});
|
|
});
|
|
|
|
test("releases only tools added by the phase", () => {
|
|
expect(
|
|
releasePhaseTools(
|
|
["inspect", "external_new", "execution_progress"],
|
|
["execution_progress", "already_removed"],
|
|
),
|
|
).toEqual(["inspect", "external_new"]);
|
|
});
|
|
});
|
|
|
|
describe("plan write path gate", () => {
|
|
const cwd = "/r";
|
|
|
|
test("allows markdown files anywhere inside cwd", () => {
|
|
expect(isPlanWritePathAllowed("PLAN.md", cwd)).toBe(true);
|
|
expect(isPlanWritePathAllowed("plans/auth.md", cwd)).toBe(true);
|
|
expect(isPlanWritePathAllowed("deeply/nested/dir/notes.mdx", cwd)).toBe(true);
|
|
});
|
|
|
|
test("rejects non-markdown extensions", () => {
|
|
expect(isPlanWritePathAllowed("src/app.ts", cwd)).toBe(false);
|
|
expect(isPlanWritePathAllowed("notes.txt", cwd)).toBe(false);
|
|
expect(isPlanWritePathAllowed("config.json", cwd)).toBe(false);
|
|
});
|
|
|
|
test("rejects files with no extension or bare directories", () => {
|
|
expect(isPlanWritePathAllowed("plans", cwd)).toBe(false);
|
|
expect(isPlanWritePathAllowed("PLAN", cwd)).toBe(false);
|
|
});
|
|
|
|
test("rejects traversal and absolute paths outside cwd", () => {
|
|
expect(isPlanWritePathAllowed("../escape.md", cwd)).toBe(false);
|
|
expect(isPlanWritePathAllowed("../../etc/passwd.md", cwd)).toBe(false);
|
|
expect(isPlanWritePathAllowed("/tmp/leak.md", cwd)).toBe(false);
|
|
});
|
|
|
|
test("allows absolute paths that resolve inside cwd", () => {
|
|
expect(isPlanWritePathAllowed("/r/plans/foo.md", cwd)).toBe(true);
|
|
});
|
|
|
|
test("rejects empty path and the cwd itself", () => {
|
|
expect(isPlanWritePathAllowed("", cwd)).toBe(false);
|
|
expect(isPlanWritePathAllowed(".", cwd)).toBe(false);
|
|
});
|
|
|
|
test("extension check is case-insensitive", () => {
|
|
expect(isPlanWritePathAllowed("PLAN.MD", cwd)).toBe(true);
|
|
expect(isPlanWritePathAllowed("notes.MdX", cwd)).toBe(true);
|
|
});
|
|
});
|