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
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
MIT License

Copyright (c) 2026 Lauren Tan
Copyright (c) 2026 Martin Patino (pstack-flex modifications)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
4 changes: 4 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This plugin is a port of upstream MIT-licensed work. All upstream copyright notices and license terms are preserved. The open-pstack history begins from `michael-denyer/pstack-claude` through proven import commit `053ed78732e3b71826933170eafe7f7782dda844`.

## pstack-flex provenance

This repository, **pstack-flex** (Martin Patino), is a fork of [ericlitman/open-pstack](https://github.com/ericlitman/open-pstack) at v1.4.1 (`de67e6b40511814171e5e4c8ad7af3b79f07c9ee`), which ports [Lauren Tan's pstack](https://github.com/cursor/plugins/tree/main/pstack) (Cursor) to Claude Code and Codex. Provenance chain: pstack-flex <- ericlitman/open-pstack <- cursor/plugins/pstack. All licenses remain MIT; every upstream license and notice file is preserved. The flex gateway additions and their tests are (c) 2026 Martin Patino, MIT, and are inventoried in [UPSTREAM-FLEX.md](UPSTREAM-FLEX.md).

## Upstream sources

| Component | Upstream | Copyright | License | License file |
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ Lauren built pstack from the skills she uses to ship code at Cursor. In a [55-mi

Open Pstack is an unofficial community project that makes pstack work in Claude Code and Codex. If Cursor is your main coding environment, use [Lauren's original pstack](https://github.com/cursor/plugins/tree/main/pstack). If Claude Code or Codex is your main coding environment, use this repository.

## This fork: pstack-flex

**pstack-flex** is a fork of [ericlitman/open-pstack](https://github.com/ericlitman/open-pstack) at v1.4.1. This change adds `deepseek` and `minimax` providers to the external runner. Each provider runs the stock `claude` binary against its Anthropic-compatible endpoint with its own API key. The runner uses an isolated `CLAUDE_CONFIG_DIR`, rejects OAuth credentials found there, and removes inherited Anthropic headers and provider-selection flags before starting the child. Gateway receipts keep token usage but set `costUsd` to null because Claude Code's cost estimate uses Anthropic prices.

The stock setup and model matrix remain in place. A follow-up change will add gateway routes to setup and document the lane configurations. The fork's provenance and sync process are recorded in [UPSTREAM-FLEX.md](UPSTREAM-FLEX.md). Anthropic does not support pointing Claude Code at non-Anthropic endpoints; use synthetic data for gateway testing and keep API keys in your local environment.

## What pstack does

pstack is a plugin for coding agents. It is not a new model or a hosted service. It gives your agent engineering rules, step-by-step workflows for different kinds of work, focused skills, and small local tools.
Expand Down
46 changes: 46 additions & 0 deletions UPSTREAM-FLEX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Flex fork synchronization

pstack-flex layers on top of open-pstack's own upstream tracking. Two sync relationships exist:

1. `cursor/plugins/pstack` -> `ericlitman/open-pstack` — documented in [UPSTREAM.md](UPSTREAM.md), unchanged by this fork.
2. `ericlitman/open-pstack` -> `thisguymartin/pstack-flex` — this document.

## Fork point

| Source | Value |
| --- | --- |
| Repository | `https://github.com/ericlitman/open-pstack.git` |
| Tag | `v1.4.1` |
| Commit | `de67e6b40511814171e5e4c8ad7af3b79f07c9ee` |
| Tracks Cursor pstack | `0.15.1` (`f8abedd`) |

The fork keeps full upstream history. The `upstream` remote points at ericlitman/open-pstack.

## What the fork owns

All flex changes are additive and live in port-owned files so upstream merges stay cheap:

- `plugins/pstack/skills/poteto-mode/scripts/runner/flex-providers.ts` and `flex-providers.test.ts` (new)
- Gateway-provider hooks in `runner/{types,commands,run,parse-output,cli}.ts` and their tests
- This file, the README fork section, and the NOTICE/LICENSE additions

The follow-up routing and documentation changes will add the flex model matrix, assignment-first setup, and `docs/LANES.md`.

The stock model matrix, the first-run sheet, every upstream skill body, and the static quad invariants are byte-unchanged.

## Merge procedure

```shell
git fetch upstream
git switch -c merge-rehearsal
git merge --no-ff --no-commit upstream/main
# inspect, resolve, run the full local gate, then merge for real or abort
```

Expected conflict surface on future upstream releases:

- `plugins/pstack/skills/setup-pstack/SKILL.md` — upstream issue #88 (1.5.0, syncing Cursor pstack 0.15.5) folds upstream PR #73, which moves setup to the same assignment-first, probe-only-assigned shape this fork already uses. Resolve toward upstream's wording wherever it covers the same rule; keep the flex families and the diversity rule.
- `plugins/pstack/skills/poteto-mode/scripts/runner/model-matrix.test.ts` — upstream 1.5.0 changes the stock panel to three lanes. Take upstream's stock assertions verbatim; the flex-matrix describe block is fork-owned and should survive as-is.
- `plugins/pstack/skills/poteto-mode/references/provider-dispatch.md` — stock matrix and default-panel prose are upstream's; the flex section is fork-owned.

After every merge: run the full local gate (`bun install --frozen-lockfile`, `bun run test`, `bun run typecheck`, manifest JSON parse, `PSTACK_STATIC_ONLY=1 bash tests/skill-collision-repro.sh`), then record the installed version, action, and observed result for each affected harness in the pull request before tagging.
24 changes: 24 additions & 0 deletions plugins/pstack/skills/poteto-mode/scripts/runner/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,28 @@ describe("runner CLI parsing", () => {
"greater than zero"
);
});

it("accepts gateway providers", () => {
const parsed = parseArgs([
...argv().map((value, index, all) =>
all[index - 1] === "--provider"
? "minimax"
: all[index - 1] === "--model"
? "MiniMax-M3"
: value
),
]);
expect(parsed?.provider).toBe("minimax");
expect(parsed?.model).toBe("MiniMax-M3");
});

it("names the gateway providers in the provider rejection", () => {
expect(() =>
parseArgs(
argv().map((value, index, all) =>
all[index - 1] === "--provider" ? "gemini" : value
)
)
).toThrow("deepseek, minimax");
});
});
2 changes: 1 addition & 1 deletion plugins/pstack/skills/poteto-mode/scripts/runner/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
UsageError,
} from "./types.ts";

const HELP = `Usage: pstack-runner --parent <claude|codex> --provider <claude|codex|grok> \\
const HELP = `Usage: pstack-runner --parent <claude|codex> --provider <${PROVIDERS.join("|")}> \\
--model <slug> --effort <level> --mode <read-only|isolated-write> \\
--prompt <file> --cwd <dir> --output <file> --receipt <file> [--timeout <seconds>]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test";
import { invocationCommand } from "./commands.ts";
import { invocationCommand, preflightCommand } from "./commands.ts";
import type { RunnerOptions } from "./types.ts";

function options(overrides: Partial<RunnerOptions> = {}): RunnerOptions {
Expand Down Expand Up @@ -146,6 +146,30 @@ describe("invocationCommand", () => {
);
});

it("runs gateway lanes with the exact claude argv for the lane's model", () => {
for (const [provider, model] of [
["deepseek", "deepseek-flash"],
["minimax", "MiniMax-M3"],
] as const) {
const gateway = invocationCommand(options({ provider, model }));
const claude = invocationCommand(
options({ provider: "claude", model })
);
expect(gateway.command).toBe("claude");
expect(gateway.stdin).toBe("prompt");
expect(gateway.args).toEqual(claude.args);
}
});

it("preflights gateway lanes with a version probe, not an auth check", () => {
for (const provider of ["deepseek", "minimax"] as const) {
const spec = preflightCommand(provider);
expect(spec.command).toBe("claude");
expect(spec.args).toEqual(["--version"]);
expect(spec.stdin).toBe("none");
}
});

it("covers low, medium, and high for every external provider", () => {
const cases = [
{
Expand Down
64 changes: 40 additions & 24 deletions plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
Provider,
RunnerOptions,
} from "./types.ts";
import { isGatewayProvider } from "./types.ts";

export interface CommandSpec {
readonly command: string;
Expand All @@ -12,6 +13,14 @@ export interface CommandSpec {
}

export function preflightCommand(provider: Provider): CommandSpec {
if (isGatewayProvider(provider)) {
// Gateway lanes run the claude binary with token auth against a
// third-party endpoint. `claude auth status` semantics under token
// auth are undocumented, so the preflight only proves the binary
// executes; credentials are checked in-process by the gateway guard
// and the one-shot invocation is the real auth test.
return { command: "claude", args: ["--version"], stdin: "none" };
}
switch (provider) {
case "claude":
return {
Expand Down Expand Up @@ -63,33 +72,40 @@ function effortOverride(effort: Effort): string {
return `model_reasoning_effort=${JSON.stringify(effort)}`;
}

function claudeInvocation(options: RunnerOptions): CommandSpec {
return {
command: "claude",
args: [
"-p",
"--model",
options.model,
"--effort",
options.effort,
"--permission-mode",
permissionMode(options.mode),
"--setting-sources",
"project",
"--strict-mcp-config",
"--tools",
claudeTools(options.mode),
"--no-session-persistence",
"--disable-slash-commands",
"--disallowed-tools",
claudeDeniedTools(options.mode),
"--output-format",
"json",
],
stdin: "prompt",
};
}

export function invocationCommand(options: RunnerOptions): CommandSpec {
// Gateway lanes use the same binary and argv as claude; the difference is
// injected environment (endpoint, token, isolated CLAUDE_CONFIG_DIR).
if (isGatewayProvider(options.provider)) return claudeInvocation(options);
switch (options.provider) {
case "claude":
return {
command: "claude",
args: [
"-p",
"--model",
options.model,
"--effort",
options.effort,
"--permission-mode",
permissionMode(options.mode),
"--setting-sources",
"project",
"--strict-mcp-config",
"--tools",
claudeTools(options.mode),
"--no-session-persistence",
"--disable-slash-commands",
"--disallowed-tools",
claudeDeniedTools(options.mode),
"--output-format",
"json",
],
stdin: "prompt",
};
return claudeInvocation(options);
case "codex":
return {
command: "codex",
Expand Down
Loading