Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions plugins/pstack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,20 @@ Then `/poteto-mode on`, or `alt+shift+t`, or `omp -p --poteto '...'` for headles
`pstackpolicy` loads with the plugin and needs no configuration. It registers `tool_call` on `bash`
and `eval`, and refuses the forge mutations the playbooks only ask for in prose: `gh pr merge`,
merge and close through `gh api`, `gh pr close|reopen|delete`, `gh pr edit --base`, `gh pr review`,
merge-when-ready, `gh repo delete`, `gh release create|delete|upload`,
merge-when-ready, a trunk ref moved through `gh api`, the GraphQL `updateRef`, `updateRefs` and
`deleteRef` mutations, `gh repo delete`, `gh release create|delete|upload`,
`gh secret|variable set|delete`, `gh workflow run --ref <trunk>`, the same forge calls made with
`curl`, and `gt submit|rebase|restack`. Locally it refuses `git update-ref`, `git branch -D`,
`git reset --hard` against a remote, and `git reflog expire --expire=now --all`. Any `git push`
whose refspec lands on `main`, `master`, `trunk`, `develop`, or `release` is refused, forced or not;
a force push to a feature branch stays allowed, because that is the port's own rebase flow.
`curl`, and `gt submit|rebase|restack|merge`. A `gh` flag such as
`-R o/r` may sit before or after the command group. Locally it refuses `git update-ref`,
`git branch -D`, `git reset --hard` against a remote, and `git reflog expire --expire=now --all`.
Any `git push` whose refspec lands on `main`, `master`, `trunk`, `develop`, or `release` is
refused, forced or not; a force push to a feature branch stays allowed, because that is the port's
own rebase flow. A refspec the scan cannot read may be trunk and is refused too: `HEAD`, no refspec
at all, a `$VAR`, `${VAR}`, `$(...)` or backtick substitution, and a `*` pattern. A push's
refspecs end at its own command, so a later `&&`, `||`, `;`, `|` or newline does not lend it
arguments, inside a subshell or brace group as well. A comment ends at its newline whatever quotes
it holds, and a redirection target is not a refspec. Each line of a heredoc body is checked as a
command, because a shell may run it.

To land, set `PSTACK_LANDING_GRANT=1` in the environment omp starts from. It is an environment
variable and not a flag or a slash command because a flag an agent can pass is not a grant you made.
Expand Down
36 changes: 34 additions & 2 deletions plugins/pstack/extensions/potetomode/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import potetomode from "./index.js";
const entry = (pinned) => ({ type: "custom", customType: "poteto-mode", data: { pinned } });
function host(flag = true) {
const events = new Map();
let command, shortcut, entries = [], status = "";
let command, shortcut, entries = [], status = "", idle = true;
const sent = [];
const ctx = {
sessionManager: { getBranch: () => entries },
ui: { theme: { fg: (_color, text) => text }, setStatus: (_key, value) => { status = value; }, notify() {} },
isIdle: () => idle,
};
potetomode({
registerCommand: (_name, value) => { command = value.handler; },
Expand All @@ -16,7 +18,7 @@ function host(flag = true) {
on: (name, handler) => events.set(name, handler),
getFlag: () => flag,
appendEntry: (_name, data) => entries.push(entry(data.pinned)),
sendUserMessage() {},
sendUserMessage: (text, options) => sent.push({ text, options }),
});
return {
start: async (branch = []) => { entries = branch; await events.get("session_start")({}, ctx); },
Expand All @@ -25,6 +27,8 @@ function host(flag = true) {
command: (args) => command(args, ctx),
shortcut: () => shortcut(ctx),
status: () => status,
busy: () => { idle = false; },
sent,
};
}

Expand Down Expand Up @@ -93,3 +97,31 @@ test("switch, branch and tree navigation reload the pin from the branch they lan
expect((await session.before()).systemPrompt[1]).toContain("poteto-mode is PINNED");
}
});

test("/poteto-mode <task> pins the session and starts the task", async () => {
const session = host(false);
await session.start();
await session.command(" Fix the bug in parser.ts ");
expect(session.sent).toHaveLength(1);
expect(session.sent[0].text).toBe("Fix the bug in parser.ts");
expect(session.sent[0].options?.deliverAs).toBeUndefined();
expect(await session.before()).toBeDefined();
});

test("/poteto-mode <task> queues the task as a follow-up while a turn is running", async () => {
const session = host(false);
await session.start();
session.busy();
await session.command("fix the bug");
expect(session.sent).toEqual([{ text: "fix the bug", options: { deliverAs: "followUp" } }]);
});

test("/poteto-mode status sends nothing and leaves the pin where it was", async () => {
for (const flag of [true, false]) {
const session = host(flag);
await session.start();
await session.command("status");
expect(session.sent).toEqual([]);
expect((await session.before()) !== undefined).toBe(flag);
}
});
Loading
Loading