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
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: ci

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.x
- run: bun scripts/check-port.ts
- run: bun test ./scripts/check-port.test.ts
- working-directory: skills/poteto-mode/scripts
run: bun install --frozen-lockfile
- working-directory: skills/poteto-mode/scripts
run: bun run test
- working-directory: skills/poteto-mode/scripts
run: bun run typecheck
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ claude plugin install pstack@pstack

To update, run `claude plugin marketplace update pstack`, then `claude plugin update pstack@pstack`. The plugin has no version number on purpose, so every commit to `main` counts as a new release (see `translation.md`).

Requirements: Claude Code, plus `gh` for anything touching PRs. Stacked PRs need the [`gh stack`](https://github.com/github/gh-stack) extension (`gh extension install github/gh-stack`). The helper scripts and `merge-gate` run on [`bun`](https://bun.sh).

## Use

`poteto-mode` is the entry point. It switches on by itself for engineering work that matches a playbook or needs rigor, or you can call it with `/pstack:poteto-mode <task>`. It matches the task to one of its playbooks (bug fix, feature, refactoring, perf, investigation, babysit, shipping, multi-phase plan, autonomous run, and more), copies the steps into a todo list, and routes to the other skills as the steps need them.
Expand Down Expand Up @@ -53,9 +55,21 @@ You can also call the other skills directly:

The 23 principles live in `skills/poteto-mode/principles/`. poteto-mode reads them on demand instead of loading them into every session.

## Letting agents merge

By default agents open PRs ready for review and never merge. To let them merge their own PRs, write a policy outside git at `~/.claude/pstack/merge-policy.json`. Without that file `merge-gate` refuses every merge.

```bash
mkdir -p ~/.claude/pstack && [ -e ~/.claude/pstack/merge-policy.json ] || curl -fsSL https://raw.githubusercontent.com/lucasheriques/pstack/main/skills/poteto-mode/scripts/merge-gate/merge-policy.example.json -o ~/.claude/pstack/merge-policy.json
```

Edit it for your repos. Both `defaults.humanOnly` and `repos` are required, and a repo with no entry is refused. `humanOnly` globs mark paths that need a human's approval on the exact head commit before the agent merges. The approver must be a collaborator other than the PR author, so on a solo repo those paths mean you merge them yourself. `requireHumanApprovalOnHead` asks for that approval on every PR in the repo. `mergeCommitBranches` lists branch globs that merge with a merge commit instead of a squash.

`merge-gate` also refuses unless `gh` is signed in as the PR author, the PR targets the default branch (so upper PRs in a stack wait for their turn), and the head has passing checks. A repo with no CI is refused with `checks-missing`.

## Differences from upstream

- **Claude models only.** Roles map to `sonnet` and `opus` in [`skills/poteto-mode/references/models.md`](skills/poteto-mode/references/models.md). Edit that file to change a role. It replaces `/setup-pstack`.
- **Claude models only.** Roles map to `sonnet` and `opus` in [`skills/poteto-mode/references/models.md`](skills/poteto-mode/references/models.md). Edit that file to change a role, and fork the repo to keep the edit, because `claude plugin update` overwrites the installed copy. It replaces `/setup-pstack`.
- **Harness.** `Task` becomes the Agent tool, `readonly` becomes the `pstack:read-only` agent, `/deslop` becomes `/simplify`, `control-ui` and `control-cli` become `claude-in-chrome` and the `run` skill, and the Origin forge is gone in favor of `gh` and `gh stack`.
- **My house rules.** A few defaults follow my own workflow instead of poteto's: `interrogate` gates every PR, agents ready their own PRs, and they merge only through `merge-gate` under a policy file you keep outside git (`~/.claude/pstack/merge-policy.json`). Comment cleanup never touches pre-existing comments.
- **Not shipped.** benny, make-bot-ui, setup-pstack, poteto-help, and the guide. They depend on Cursor.
Expand Down
15 changes: 15 additions & 0 deletions skills/poteto-mode/scripts/merge-gate/merge-policy.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"defaults": {
"humanOnly": [".github/**", "**/.env*", "**/Dockerfile*", "**/infra/**", "**/auth/**", "**/*billing*", "**/*billing*/**", "**/*payment*", "**/*payment*/**"]
},
"repos": {
"your-name/your-repo": {
"humanOnly": ["db/migrations/**"],
"requireHumanApprovalOnHead": false,
"mergeCommitBranches": ["sync/*"]
},
"your-org/*": {
"requireHumanApprovalOnHead": true
}
}
}
13 changes: 12 additions & 1 deletion skills/poteto-mode/scripts/merge-gate/policy.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "bun:test";
import { PolicyError, type RepoPolicy, parsePolicy, repoPolicy } from "./policy.ts";
import { join } from "node:path";
import { PolicyError, type RepoPolicy, loadPolicy, parsePolicy, repoPolicy } from "./policy.ts";

const valid = {
defaults: { humanOnly: [".github/**"] },
Expand Down Expand Up @@ -74,3 +75,13 @@ describe("parsePolicy", () => {
expect(() => parsePolicy(value)).toThrow(PolicyError);
});
});

describe("merge-policy.example.json", () => {
it("loads as a valid policy", async () => {
const policy = await loadPolicy(join(import.meta.dir, "merge-policy.example.json"));
const repo = repoPolicy(policy, { owner: "your-name", repo: "your-repo" });
const needsHuman = (path: string) => repo?.humanOnly.some((glob) => new Bun.Glob(glob.toLowerCase()).match(path));
expect([".github/ci.yml", "src/billing/charge.ts", "payment/handler.ts", "db/migrations/1.sql"].map(needsHuman)).toEqual([true, true, true, true]);
expect(needsHuman("src/index.ts")).toBe(false);
});
});
Loading