Skip to content
Closed
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
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,11 @@ Each name links to that skill's README. Adding a skill? Write its `SKILL.md` and
/git-sync # commit, pull, push
```

Flags: `run --low|--high` · `run --codex[=<model>]` · `super-collect --since 30d`
Flags: `run --low|--high` · `run --codex[=<model>]` · `run --pi[=<model>]` · `super-collect --since 30d`

## Guards & hooks

Run automatically once installed. Python stdlib, JSON in, JSON out.
Run automatically once installed. Python stdlib, JSON in, JSON out. In Pi the same guards run through `.pi/extensions/super-board-guards.ts`.

| | |
| --- | --- |
Expand All @@ -134,7 +134,7 @@ Run automatically once installed. Python stdlib, JSON in, JSON out.

## Models

A cheap router grades each card easy, medium or hard first; the run flag picks the model for each grade. With `--codex`, every lane runs on Codex GPT models instead of Claude.
A cheap router grades each card easy, medium or hard first; the run flag picks the model for each grade. With `--codex` or `--pi`, every lane runs on that backend instead of Claude.

| Command | Router | Easy card | Medium card | Hard card |
| --- | --- | --- | --- | --- |
Expand All @@ -144,12 +144,21 @@ A cheap router grades each card easy, medium or hard first; the run flag picks t
| `/super-board run --codex` | gpt-6-luna | gpt-6.1-sol | gpt-6.1-sol | gpt-6-astra |
| `/super-board run --codex --low` | gpt-6-luna | gpt-6-luna | gpt-6.1-sol | gpt-6.1-sol |
| `/super-board run --codex --high` | gpt-6-luna | gpt-6-astra | gpt-6-astra | gpt-6-astra |
| `/super-board run --pi` | claude-haiku-latest | claude-sonnet-latest | claude-sonnet-latest | claude-opus-latest |
| `/super-board run --pi --low` | claude-haiku-latest | claude-haiku-latest | claude-sonnet-latest | claude-sonnet-latest |
| `/super-board run --pi --high` | claude-haiku-latest | claude-opus-latest | claude-opus-latest | claude-opus-latest |

`/super-board run --codex=<model>` pins one model for every card and skips the router.
`/super-board run --codex=<model>` or `--pi=<model>` pins one model for every card and skips the router.
The Pi names are defaults: Pi resolves a model pattern across the providers you connected, and bare
`haiku`/`sonnet`/`opus` can land on a `:batch` catalog variant no chat endpoint accepts, so the
defaults use the `claude-<family>-latest` aliases — a board on another provider overrides
`pi.ladders` (and `pi.model`/`pi.router_model`) in the config, and a `:thinking` suffix still applies.

Run inside Codex? plain `/super-board run` switches to the Codex ladder by itself. Codex has no
Run inside Codex or Pi? plain `/super-board run` switches to that host's ladder by itself. Codex has no
`/super-board` command: it finds skills in `.agents/skills/` (symlinks followed), so link
`.claude/skills/super-board` there and type `$super-board run`.
`.claude/skills/super-board` there and type `$super-board run`. Pi discovers the same
`.agents/skills/` locations, plus `.pi/skills/` and its agent directory; invoke it as
`/skill:super-board run`. There is no Claude-usage fallback into Pi: `--pi` (or the Pi host) is required.

## Writing standard

Expand Down Expand Up @@ -188,7 +197,7 @@ MIT © Eric Tech ([LICENSE](LICENSE)). Built on [obra/superpowers](https://githu
### Worker skill availability

Setup and dispatch resolve real, nonempty `SKILL.md` files for the actual
Claude or Codex host. Missing required skills halt before claims or workers;
Claude, Codex or Pi host. Missing required skills halt before claims or workers;
lockfiles and arbitrary plugin caches do not count. The shared registry in
`scripts/super-board-skills-ready.py` owns lane dependencies, overrides and the
two declared ponytail fallbacks. Setup's full helper union may be red while a
Expand Down
38 changes: 38 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
# Release notes

## v3.2.0 — 2026-10-17

Pi (pi.dev) is a third agent host: `/super-board run --pi` drains the board on Pi's models.

- ✨ **A Pi lane and wave.** `super-board-pi-wave.sh` → `super-board-pi-lane.sh` runs every
lane headless (`pi -p -a`) with the same claims, worktrees, handoff and halt contract as
Codex. Pi has no `--output-schema`: the worker writes its final JSON to a private result
file that the wrapper validates fail-closed (a missing, malformed or contradicting file
fails the lane; exit 9/10/79 keep their halt meanings).
- ✨ **`--pi[=<model>] [--low|--high]`.** Ladders default to the
`claude-haiku-latest` / `claude-sonnet-latest` / `claude-opus-latest` aliases and Pi resolves
a model pattern across the providers you connected (bare `haiku`/`sonnet`/`opus` can land on
a `:batch` catalog variant no chat endpoint accepts — found in a live smoke); a
`claude-haiku-latest` router grades each Ready card and `pi.model` / `pi.router_model` /
`pi.ladders` override the defaults. `--pi` is always required: unlike Codex there is no
Claude-usage fallback that may start Pi on its own, and `review` without the flag refuses
with its own message.
- ✨ **A Pi session means `--pi`.** `super-board-host.sh` reads `AI_AGENT=pi` /
`PI_CODING_AGENT=true` before the Codex ids, a plain `run` inside Pi takes the Pi ladder,
and Pi invokes the skill as `/skill:super-board run`.
- 🛡️ **The guards port to Pi.** `.pi/extensions/super-board-guards.ts` translates Pi tool
calls into the same payloads the `.claude` hooks read, so worktree, secrets, key-literal,
delete-outside and the opt-in protected-push guard behave identically — fail-open, like
the hooks. `install.sh` installs it; `--no-hooks` skips it, and
`--no-hooks --protect-main` installs it with the push guard alone.
- 🔧 **Stop and calibration know Pi.** `super-board-stop.sh` validates and stops a Pi wave
(its own `pi-wave.pid`) with the same SIGTERM-then-claims order; a completed calibration
receipt is refused while a Pi wave marker exists.
- 🔎 **Skill readiness resolves Pi's real surfaces**: the scoped `.agents/skills` search,
project `.pi/skills`, user and agent-directory roots, and the fail-closed
`skills`/`packages` settings policy (authoritative `--catalog` unblocks it).
- 📝 **Docs.** `references/run-workflow.md` gains "Pi runs", the README models table gains
Pi rows, and `references/config-schema.json` documents the `pi` block.
- 🧪 **Offline tests.** `tests/test-pi-lane.sh` (opt-in, models, ladders, wave, halt,
cancellation), the Pi marker scenarios in `tests/test-codex-stop.sh`, and
`tests/test-pi-guards.sh`, which runs the real extension through Node's type stripping
against a stub Pi API.

## v3.1.2 — 2026-10-11

`/super-collect` dedupes on the exact fingerprint and says when Sentry or PostHog is down.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.1.2
3.2.0
162 changes: 162 additions & 0 deletions extensions/super-board-guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* super-board guards — Pi port of the .claude guard hooks.
*
* Pi loads project extensions only after the project is trusted (headless lanes
* pass `-a`). Each handler builds the same stdin JSON the Python guards already
* understand, runs the guard with python3, and turns a PreToolUse deny into
* `{ block, reason }`. A missing script or a broken guard never blocks a call,
* exactly like the hooks' fail-open contract.
*
* Mirrors hooks/settings-snippet.json:
* Bash -> guard-worktree-path, guard-delete-outside, guard-secrets, guard-key-literals
* Read|Grep -> guard-secrets
* Edit|Write -> guard-key-literals (PreToolUse; PostToolUse re-reads the file)
* session_start -> cleanup-wt.py --auto --fetch (interactive sessions only)
* guard-protected-push runs only when `.claude/settings.json` wires it
* (install.sh --protect-main / onboard), the same opt-in switch Claude uses.
*
* Pi's tool names and inputs differ from Claude's; `claudeTool` and
* `claudeInput` below are the whole translation layer.
*/
import { spawn, spawnSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

type Json = Record<string, unknown>;

/** Pi tool name -> the Claude hook tool name the Python guards match on. */
function claudeTool(toolName: string): string | undefined {
switch (toolName) {
case "bash":
return "Bash";
case "read":
return "Read";
case "grep":
return "Grep";
case "edit":
return "Edit";
case "write":
return "Write";
default:
return undefined;
}
}

/** Pi tool input -> the Claude hook tool_input fields the guards read. */
function claudeInput(toolName: string, input: Json): Json {
if (toolName === "read") return { file_path: input.path };
if (toolName === "write") return { file_path: input.path, content: input.content };
if (toolName === "grep") return { pattern: input.pattern, path: input.path, glob: input.glob };
if (toolName === "edit") {
const edits = Array.isArray(input.edits) ? (input.edits as Json[]) : [];
return {
file_path: input.path,
edits: edits.map((edit) => ({ old_string: edit.oldText, new_string: edit.newText })),
new_string: edits.map((edit) => String(edit.newText ?? "")).join("\n"),
};
}
return input;
}

/** Run one guard; returns its parsed stdout, or undefined on missing script/error/no output. */
function runGuard(file: string, project: string, payload: Json): Json | undefined {
try {
if (!existsSync(file)) return undefined;
const result = spawnSync("python3", [file], {
cwd: project,
input: JSON.stringify(payload),
encoding: "utf8",
timeout: 10_000,
env: { ...process.env, CLAUDE_PROJECT_DIR: project },
});
const text = (result.stdout ?? "").trim();
return text ? (JSON.parse(text) as Json) : undefined;
} catch {
return undefined; // a broken guard must never block unrelated work
}
}

function denyReason(output: Json | undefined): string | undefined {
const specific = (output?.hookSpecificOutput ?? {}) as Json;
if (specific.permissionDecision === "deny") {
return String(specific.permissionDecisionReason ?? "Blocked by super-board guard");
}
return undefined;
}

/** The protect-main guard is opt-in: settings.json says whether it was wired. */
function protectsMain(project: string): boolean {
try {
const settings = join(project, ".claude", "settings.json");
return existsSync(settings) && readFileSync(settings, "utf8").includes("guard-protected-push.py");
} catch {
return false;
}
}

export default function (pi: ExtensionAPI) {
pi.on("tool_call", async (event, ctx) => {
const project = ctx.cwd;
const tool = claudeTool(event.toolName);
if (!tool) return undefined;
const hooks = join(project, ".claude", "hooks");
const guards: string[] = [];
if (event.toolName === "bash") {
guards.push(join(hooks, "guard-worktree-path.py"), join(hooks, "guard-delete-outside.py"));
if (protectsMain(project)) guards.push(join(hooks, "guard-protected-push.py"));
}
if (event.toolName === "bash" || event.toolName === "read" || event.toolName === "grep") {
guards.push(join(hooks, "guard-secrets.py"));
}
if (event.toolName === "bash" || event.toolName === "edit" || event.toolName === "write") {
guards.push(join(hooks, "guard-key-literals.py"));
}
const payload = {
hook_event_name: "PreToolUse",
tool_name: tool,
tool_input: claudeInput(event.toolName, event.input as Json),
cwd: project,
};
for (const guard of guards) {
const reason = denyReason(runGuard(guard, project, payload));
if (reason) return { block: true, reason };
}
return undefined;
});

// PostToolUse key-literals: re-read what reached disk and surface the warning
// with the tool result, where Claude's additionalContext would land.
pi.on("tool_result", async (event, ctx) => {
if (event.toolName !== "edit" && event.toolName !== "write") return undefined;
if (event.isError) return undefined;
const output = runGuard(join(ctx.cwd, ".claude", "hooks", "guard-key-literals.py"), ctx.cwd, {
hook_event_name: "PostToolUse",
tool_name: claudeTool(event.toolName)!,
tool_input: claudeInput(event.toolName, event.input as Json),
cwd: ctx.cwd,
});
const specific = (output?.hookSpecificOutput ?? {}) as Json;
if (!specific.additionalContext) return undefined;
return { content: [...event.content, { type: "text" as const, text: String(specific.additionalContext) }] };
});

// SessionStart parity: cleanup-wt --auto --fetch. Interactive sessions only —
// headless lanes get the post-merge cleanup through the merge gate, and a
// detached cleanup next to parallel lane worktrees would only add races.
pi.on("session_start", async (_event, ctx) => {
if (ctx.mode !== "tui" && ctx.mode !== "rpc") return;
const script = join(ctx.cwd, ".claude", "hooks", "cleanup-wt.py");
if (!existsSync(script)) return;
try {
spawn("python3", [script, "--auto", "--fetch"], {
cwd: ctx.cwd,
detached: true,
stdio: "ignore",
env: { ...process.env, CLAUDE_PROJECT_DIR: ctx.cwd },
}).unref();
} catch {
// fail open, like the hook's `|| true`
}
});
}
17 changes: 17 additions & 0 deletions hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ project and merges the block below (kept in `settings-snippet.json`) into
}
```

## Pi port: `.pi/extensions/super-board-guards.ts`

Pi uses its own extension API instead of Claude Code hooks. `install.sh` copies
`extensions/super-board-guards.ts` into the target's `.pi/extensions/`, with the same
switches as the hooks: skipped by `--no-hooks`, installed by `--no-hooks --protect-main`
for the push guard alone. The extension translates Pi's tool calls (`bash`, `read`,
`grep`, `edit`, `write`) into the same stdin JSON these scripts already read, runs them
with `python3`, and turns a `permissionDecision: deny` into `{ block: true, reason }`.
A missing script, a crash or malformed output never blocks a call — the hooks' fail-open
contract. PostToolUse key-literals warnings ride along with the tool result, and
`cleanup-wt.py --auto --fetch` runs detached on interactive session start only (lanes
already get the merge gate's `--post-merge` run). `guard-protected-push.py` stays
opt-in: the extension runs it only when `.claude/settings.json` wires it, the same
switch `onboard` and `install.sh --protect-main` flip. Pi loads project extensions
only after the project is trusted; interactive users answer the trust prompt once, and
headless lanes pass `-a`.

## Opt-in: block pushes to the base branch

`guard-protected-push.py` — PreToolUse `Bash`: denies a `git push` that would update or delete
Expand Down
4 changes: 2 additions & 2 deletions hooks/guard-worktree-path.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ def check(command, cwd, root_of, env):
where = ("resolves to " + resolved) if resolved else "cannot be resolved before it runs"
return (
'Blocked: worktree path "%s" %s, outside %s/.\n'
"Worktrees live only under .claude/worktrees/ so cleanup-wt and Claude Code's sweep can see them.\n"
'Prefer the Agent tool\'s isolation: "worktree" or `claude --worktree <name>`. If you need git directly:\n'
"Worktrees live only under .claude/worktrees/ so cleanup-wt and the host's worktree sweep can see them.\n"
'Prefer your host\'s isolation (Claude Code: the Agent tool\'s "worktree" option or `claude --worktree <name>`). If you need git directly:\n'
" git worktree add .claude/worktrees/<name> -b <branch> <base>\n"
"(from the repo root, with a literal path)." % (raw, where, allowed)
)
Expand Down
23 changes: 18 additions & 5 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# super-board installer.
# Copies skills/, scripts/, workflows/ and the guard hooks into the target
# project's .claude/ tree, and merges the hook settings into
# .claude/settings.json (backed up first, never duplicated).
# project's .claude/ tree, the Pi guard extension into .pi/extensions/, and
# merges the hook settings into .claude/settings.json (backed up first, never
# duplicated).
#
# Usage:
# ./install.sh [--no-hooks] [--protect-main] [target-project-dir]
Expand Down Expand Up @@ -145,7 +146,17 @@ copy_file() {
fi
}

for script in super-board-project-verifier.py super-board-skills-ready.py super-board-calibration.py super-board-verification-ready.py super-board-github-read.py super-board-collect-state.py super-board-run.sh super-board-gh-guard.sh super-board-card.sh super-board-status.py super-board-wave-plan.sh super-board-codex-lane.sh super-board-codex-wave.sh super-board-deps.sh super-board-preflight.sh super-board-merge-gate.sh super-board-merge-policy.py super-board-approval.py super-board-env-check.sh super-board-agents-md.py super-board-settings.py super-board-setup.py super-board-usage.sh super-board-throttle.sh super-board-host.sh super-board-pr-body.sh super-review-file-refactor.sh super-qa-file-bug.sh super-board-stop.sh; do
# Pi loads project guards from .pi/extensions/; the extension drives the same
# .claude/hooks/*.py scripts, so it is only useful when they were installed.
install_pi_extension() {
mkdir -p "$TARGET/.pi/extensions"
for ext in "$REPO_ROOT"/extensions/*.ts; do
[ -f "$ext" ] || continue
copy_file "$ext" "$TARGET/.pi/extensions/$(basename "$ext")"
done
}

for script in super-board-project-verifier.py super-board-skills-ready.py super-board-calibration.py super-board-verification-ready.py super-board-github-read.py super-board-collect-state.py super-board-run.sh super-board-gh-guard.sh super-board-card.sh super-board-status.py super-board-wave-plan.sh super-board-codex-lane.sh super-board-codex-wave.sh super-board-pi-lane.sh super-board-pi-wave.sh super-board-deps.sh super-board-preflight.sh super-board-merge-gate.sh super-board-merge-policy.py super-board-approval.py super-board-env-check.sh super-board-agents-md.py super-board-settings.py super-board-setup.py super-board-usage.sh super-board-throttle.sh super-board-host.sh super-board-pr-body.sh super-review-file-refactor.sh super-qa-file-bug.sh super-board-stop.sh; do
if [ -f "$REPO_ROOT/scripts/$script" ]; then
copy_file "$REPO_ROOT/scripts/$script" "$TARGET/.claude/bin/$script"
chmod +x "$TARGET/.claude/bin/$script"
Expand Down Expand Up @@ -188,15 +199,17 @@ if [ "$HOOKS" -eq 1 ]; then
done
SNIPPETS="$REPO_ROOT/hooks/settings-snippet.json"
if [ "$PROTECT" -eq 1 ]; then SNIPPETS="$SNIPPETS $REPO_ROOT/hooks/settings-protect-main.json"; fi
install_pi_extension
# shellcheck disable=SC2086
echo " 🛡️ $n guard hooks → .claude/settings.json ($(merge_settings $SNIPPETS))"
echo " 🛡️ $n guard hooks → .claude/settings.json + .pi/extensions/ ($(merge_settings $SNIPPETS))"
elif [ "$PROTECT" -eq 1 ]; then
# --no-hooks --protect-main: the one guard asked for, nothing else. This is the
# command onboard names when its protect-main question finds no guard script.
mkdir -p "$TARGET/.claude/hooks"
copy_file "$REPO_ROOT/hooks/guard-protected-push.py" "$TARGET/.claude/hooks/guard-protected-push.py"
chmod +x "$TARGET/.claude/hooks/guard-protected-push.py"
echo " 🛡️ --no-hooks: only the push guard → .claude/settings.json ($(merge_settings "$REPO_ROOT/hooks/settings-protect-main.json"))"
install_pi_extension
echo " 🛡️ --no-hooks: only the push guard → .claude/settings.json + .pi/extensions/ ($(merge_settings "$REPO_ROOT/hooks/settings-protect-main.json"))"
else
echo " ⏭️ guard hooks skipped (--no-hooks)"
fi
Expand Down
3 changes: 2 additions & 1 deletion scripts/super-board-calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ def prepare(config, receipt_path, cwd):
require(not inflight.exists() or not any(re.fullmatch(r'\d+', p.name) for p in inflight.iterdir()),
'unfinished-work')
require(not (inflight / 'workflow-wave.lock').exists() and
not (root / '.claude/super-board/codex-wave.pid').exists(), 'unfinished-work')
not (root / '.claude/super-board/codex-wave.pid').exists() and
not (root / '.claude/super-board/pi-wave.pid').exists(), 'unfinished-work')
selected = receipt.get('selected_issues')
cards = receipt.get('cards')
require(isinstance(selected, list) and 0 < len(selected) <= 100 and
Expand Down
Loading