From eb8113bad20dad5a89881ef19982474cef9c9ec6 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Tue, 6 Oct 2026 18:27:15 -0300 Subject: [PATCH 1/3] feat: add CI, an example merge policy, and install prerequisites CI runs check-port and its tests, then the script suites and typecheck, on every PR and push to main. Without it merge-gate refuses every PR here with checks-missing. merge-policy.example.json is loaded by a policy test so it cannot rot, and the README documents the opt-in merge policy and the gh, gh stack, and bun requirements. Tested: bun scripts/check-port.ts (0 findings); bun test merge-gate/policy (25 pass); bun run test in skills/poteto-mode/scripts (174 pass); bun run typecheck (clean). The workflow itself runs for the first time on this PR. --- .github/workflows/ci.yml | 21 +++++++++++++++++++ README.md | 6 ++++++ .../merge-gate/merge-policy.example.json | 15 +++++++++++++ .../scripts/merge-gate/policy.test.ts | 10 ++++++++- 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 skills/poteto-mode/scripts/merge-gate/merge-policy.example.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ae2cfa4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: ci + +on: + pull_request: + push: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - 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 diff --git a/README.md b/README.md index 173694f..2360af6 100644 --- a/README.md +++ b/README.md @@ -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 `. 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. @@ -53,6 +55,10 @@ 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 at `~/.claude/pstack/merge-policy.json`, outside git on purpose. Without that file `merge-gate` refuses every merge. Start from [`merge-policy.example.json`](skills/poteto-mode/scripts/merge-gate/merge-policy.example.json). A repo with no entry is refused. `humanOnly` globs mark paths whose PRs only you merge. `requireHumanApprovalOnHead` demands a human approval on the exact head commit, and `mergeCommitBranches` lists branches that merge with a merge commit instead of squash. `merge-gate` also refuses a PR with failing, pending, or missing checks, so the repo needs CI. + ## 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`. diff --git a/skills/poteto-mode/scripts/merge-gate/merge-policy.example.json b/skills/poteto-mode/scripts/merge-gate/merge-policy.example.json new file mode 100644 index 0000000..2d41957 --- /dev/null +++ b/skills/poteto-mode/scripts/merge-gate/merge-policy.example.json @@ -0,0 +1,15 @@ +{ + "defaults": { + "humanOnly": [".github/**", "**/.env*", "**/Dockerfile*", "**/infra/**", "**/auth/**", "**/*billing*", "**/*payment*"] + }, + "repos": { + "your-name/your-repo": { + "humanOnly": ["db/migrations/**"], + "requireHumanApprovalOnHead": false, + "mergeCommitBranches": ["sync/*"] + }, + "your-org/*": { + "requireHumanApprovalOnHead": true + } + } +} diff --git a/skills/poteto-mode/scripts/merge-gate/policy.test.ts b/skills/poteto-mode/scripts/merge-gate/policy.test.ts index 84002f6..24cbf3d 100644 --- a/skills/poteto-mode/scripts/merge-gate/policy.test.ts +++ b/skills/poteto-mode/scripts/merge-gate/policy.test.ts @@ -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/**"] }, @@ -74,3 +75,10 @@ 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")); + expect(policy.repos.has("your-name/your-repo")).toBe(true); + }); +}); From ab14425ac10eaf573b90ba4847aa2a5404f1764b Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Tue, 6 Oct 2026 18:33:20 -0300 Subject: [PATCH 2/3] fix: correct the merge-policy docs and give CI read-only permissions humanOnly needs a collaborator's approval on the head, not 'only you merge'. The README now says how to create the policy file and lists the PR conditions merge-gate also checks. CI gets contents: read and cancels superseded runs. Tested: claims re-read against decide.ts and policy.ts; bun scripts/check-port.ts (0 findings). --- .github/workflows/ci.yml | 7 +++++++ README.md | 10 +++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae2cfa4..ccb313d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,13 @@ on: push: branches: [main] +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + jobs: check: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 2360af6..02bbb1a 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,15 @@ The 23 principles live in `skills/poteto-mode/principles/`. poteto-mode reads th ## Letting agents merge -By default agents open PRs ready for review and never merge. To let them merge their own PRs, write a policy at `~/.claude/pstack/merge-policy.json`, outside git on purpose. Without that file `merge-gate` refuses every merge. Start from [`merge-policy.example.json`](skills/poteto-mode/scripts/merge-gate/merge-policy.example.json). A repo with no entry is refused. `humanOnly` globs mark paths whose PRs only you merge. `requireHumanApprovalOnHead` demands a human approval on the exact head commit, and `mergeCommitBranches` lists branches that merge with a merge commit instead of squash. `merge-gate` also refuses a PR with failing, pending, or missing checks, so the repo needs CI. +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 && 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 From 646dab756426d9f6a40b1dcd29b7f24dad706f43 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Tue, 6 Oct 2026 18:35:00 -0300 Subject: [PATCH 3/3] fix: make the example policy cover directories and the curl step non-destructive **/*billing* only matched a file named billing, so src/billing/charge.ts needed no human. The example now covers directories and its test checks real paths. The curl step no longer overwrites an edited policy, the README warns that plugin update overwrites models.md edits, and CI pins bun 1.3.x so a bun release cannot turn every agent merge red. Tested: bun test merge-gate/policy (25 pass); bun run typecheck clean; bun scripts/check-port.ts (0 findings). --- .github/workflows/ci.yml | 2 ++ README.md | 4 ++-- .../poteto-mode/scripts/merge-gate/merge-policy.example.json | 2 +- skills/poteto-mode/scripts/merge-gate/policy.test.ts | 5 ++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccb313d..6333339 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: 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 diff --git a/README.md b/README.md index 02bbb1a..85f81d2 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ The 23 principles live in `skills/poteto-mode/principles/`. poteto-mode reads th 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 && 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 +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. @@ -69,7 +69,7 @@ Edit it for your repos. Both `defaults.humanOnly` and `repos` are required, and ## 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. diff --git a/skills/poteto-mode/scripts/merge-gate/merge-policy.example.json b/skills/poteto-mode/scripts/merge-gate/merge-policy.example.json index 2d41957..22b3099 100644 --- a/skills/poteto-mode/scripts/merge-gate/merge-policy.example.json +++ b/skills/poteto-mode/scripts/merge-gate/merge-policy.example.json @@ -1,6 +1,6 @@ { "defaults": { - "humanOnly": [".github/**", "**/.env*", "**/Dockerfile*", "**/infra/**", "**/auth/**", "**/*billing*", "**/*payment*"] + "humanOnly": [".github/**", "**/.env*", "**/Dockerfile*", "**/infra/**", "**/auth/**", "**/*billing*", "**/*billing*/**", "**/*payment*", "**/*payment*/**"] }, "repos": { "your-name/your-repo": { diff --git a/skills/poteto-mode/scripts/merge-gate/policy.test.ts b/skills/poteto-mode/scripts/merge-gate/policy.test.ts index 24cbf3d..212ce76 100644 --- a/skills/poteto-mode/scripts/merge-gate/policy.test.ts +++ b/skills/poteto-mode/scripts/merge-gate/policy.test.ts @@ -79,6 +79,9 @@ describe("parsePolicy", () => { describe("merge-policy.example.json", () => { it("loads as a valid policy", async () => { const policy = await loadPolicy(join(import.meta.dir, "merge-policy.example.json")); - expect(policy.repos.has("your-name/your-repo")).toBe(true); + 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); }); });