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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Or as a Claude Code plugin:
> [!NOTE]
> The plugin ships the skills only. Run `/super-board:super-board onboard` and its 🔍 Checks step adds the guard hooks, scripts and workflows. Needs Claude Code, `gh`, `jq`, bash 3.2+ and Python 3.9+; the installer checks.

Running from Codex or OpenCode? See [supported hosts](docs/hosts.md) for tested versions, sign-in and gaps.

Optional post-run learning can delegate an already installed `calibrate` skill after
verified selected work completes. It applies only to explicitly permitted existing
canonical skill files; the package bundles no private skill. See
Expand Down
37 changes: 37 additions & 0 deletions docs/hosts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Agent hosts

super-board runs inside an agent host: the CLI whose session types `/super-board run`.
`scripts/super-board-host.sh` (installed as `.claude/bin/super-board-host.sh`) names it.

| Host value | Tested version | Start a run | Sign-in | Gaps |
|---|---|---|---|---|
| `claude` | Claude Code 2.1.302 | `/super-board run` (Workflow backend) | `claude` then `/login`; `gh auth login` | None. The reference host. |
| `codex` | codex-cli 0.160.0 | `/super-board run` inside Codex runs as `--codex`; from Claude, `/super-board run --codex` | `codex login`; `gh auth login` | No Workflow tool, so every lane is headless `codex exec`. Review runs only with explicit `--codex` (a host-switched plain `run` counts). No Claude usage guard. |
| `opencode` | OpenCode v2.0.22 | Not supported yet: the run halts with a host-selection request | `opencode auth login`; `gh auth login` | No OpenCode lane backend yet. `permission.external_directory: deny` is not enforced in v2.0.22 (`patch` still writes outside the repo), so OpenCode runs stay attended-only once a backend lands. |

Every host also needs `gh`, `jq`, bash 3.2+ and Python 3.9+.

## How the host is picked

`super-board-host.sh [--config <config.json>]` prints one value. First match wins:

1. `SUPER_BOARD_HOST=claude|codex|opencode`, the manual override. Any other value exits 64.
2. `CODEX_THREAD_ID` or `CODEX_SESSION_ID` → `codex`.
3. `OPENCODE=1` → `opencode`. Checked before `CLAUDECODE`, which leaks into an OpenCode
session started from Claude Code. `AGENT=1` is ignored: other tools set it too.
4. `CLAUDECODE` → `claude`.
5. The board config's top-level `host` key, when `--config` is given.
6. Nothing matched: empty stdout, a hint on stderr, exit 65. The run halts and asks for a
host. It never guesses Claude.

A live env sign beats the saved config, so a Codex lane started from a Claude-configured
board still answers `codex`.

Onboard always writes `host` into `.claude/super-board/configs/<slug>.json`.
`super-board-setup.py fix` adds a missing one from `--host`; `check` flags any other value.

## Evidence

- Codex: env of `codex exec -s read-only`, 2026-10-04 (recorded in the script header).
- OpenCode: env diff of an `opencode run` shell, 2026-10-19: `OPENCODE=1`,
`OPENCODE_SESSION_ID`, `OPENCODE_TERMINAL=1`, `AGENT=1`.
57 changes: 44 additions & 13 deletions scripts/super-board-host.sh
Original file line number Diff line number Diff line change
@@ -1,29 +1,60 @@
#!/usr/bin/env bash
# super-board-host.sh — which agent host is this shell inside: `claude` or `codex`.
# super-board-host.sh — which agent host is this shell inside: `claude`, `codex` or `opencode`.
#
# WHY. `/super-board run` launched from a Codex CLI session has no Claude Workflow tool, so the
# workflow backend cannot start. The orchestrator checks its own tool list first; this is the
# check a script can make, and the tie-breaker when the tool list is unclear.
# check a script can make, and the tie-breaker when the tool list is unclear. A shell with no
# known sign gets no answer (exit 65): guessing Claude there starts the wrong backend.
#
# THE SIGNAL. Codex CLI sets CODEX_THREAD_ID (and CODEX_SESSION_ID, the same value) in every
# shell it runs. Observed 2026-10-04 on codex-cli 0.160.0 with `codex exec -s read-only`:
# CODEX_THREAD_ID, CODEX_SESSION_ID, CODEX_VERSION, CODEX_SANDBOX=seatbelt, CODEX_CI=1.
# Not used: CODEX_HOME (people export it in their own profile), CODEX_SANDBOX (absent when
# the sandbox is off). A Codex lane started from a Claude session also carries CLAUDECODE; the
# Codex ids still win there, because the innermost host is the one running this shell.
# THE SIGNALS.
# - Codex CLI sets CODEX_THREAD_ID (and CODEX_SESSION_ID, the same value) in every shell it
# runs. Observed 2026-10-04 on codex-cli 0.160.0 with `codex exec -s read-only`:
# CODEX_THREAD_ID, CODEX_SESSION_ID, CODEX_VERSION, CODEX_SANDBOX=seatbelt, CODEX_CI=1.
# Not used: CODEX_HOME (people export it in their own profile), CODEX_SANDBOX (absent when
# the sandbox is off). A Codex lane started from a Claude session also carries CLAUDECODE;
# the Codex ids still win there, because the innermost host is the one running this shell.
# - OpenCode sets OPENCODE=1 (plus OPENCODE_SESSION_ID, OPENCODE_TERMINAL=1, AGENT=1) in the
# shells it runs. Observed 2026-10-19 on opencode v2.0.22 (env diff of a `run` shell).
# Checked before CLAUDECODE, which leaks into OpenCode started from a Claude session.
# Not used: AGENT=1 (a generic name other tools set too).
# - Claude Code sets CLAUDECODE=1.
#
# Usage: super-board-host.sh prints `claude` or `codex`, exit 0
# SUPER_BOARD_HOST=claude|codex forces the answer (tests, or a host the check misreads).
# ORDER. SUPER_BOARD_HOST, Codex ids, OPENCODE, CLAUDECODE, then the config's top-level
# `host` key (--config <file>), else exit 65. A live env sign beats the saved config.
#
# Usage: super-board-host.sh [--config <config.json>]
# prints `claude`, `codex` or `opencode`, exit 0
# exit 65 no sign: nothing on stdout, how to choose on stderr
# exit 64 SUPER_BOARD_HOST or config `host` is not one of the three
# SUPER_BOARD_HOST=claude|codex|opencode forces the answer (tests, or a host the check
# misreads).
set -euo pipefail

config=
case "${1:-}" in
--config) config=${2:-} ;;
'') ;;
*) echo "usage: super-board-host.sh [--config <config.json>]" >&2; exit 64 ;;
esac

case "${SUPER_BOARD_HOST:-}" in
claude|codex) echo "$SUPER_BOARD_HOST"; exit 0 ;;
claude|codex|opencode) echo "$SUPER_BOARD_HOST"; exit 0 ;;
'') ;;
*) echo "super-board-host.sh: SUPER_BOARD_HOST must be claude or codex" >&2; exit 64 ;;
*) echo "super-board-host.sh: SUPER_BOARD_HOST must be claude, codex or opencode" >&2; exit 64 ;;
esac

if [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_SESSION_ID:-}" ]; then
echo codex
else
elif [ -n "${OPENCODE:-}" ]; then
echo opencode
elif [ -n "${CLAUDECODE:-}" ]; then
echo claude
elif saved=$( [ -f "$config" ] && jq -r '.host // empty' "$config" 2>/dev/null ) && [ -n "$saved" ]; then
case "$saved" in
claude|codex|opencode) echo "$saved" ;;
*) echo "super-board-host.sh: config host must be claude, codex or opencode, got '$saved'" >&2; exit 64 ;;
esac
else
echo "super-board-host.sh: unknown agent host. Pick one: SUPER_BOARD_HOST=claude|codex|opencode, or set \"host\" in the board config" >&2
exit 65
fi
27 changes: 19 additions & 8 deletions scripts/super-board-setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
- installs or refreshes skills, .claude/bin scripts, workflows, guard hooks and their
settings.json entries (runs the pack's install.sh) when any is missing or older
- removes the folders of skills that no longer exist: super-refine, cleanup-wt, arch-loop
- migrates every .claude/super-board/configs/*.json to the v3 keys (migrate-config)
- migrates every .claude/super-board/configs/*.json to the v3 keys (migrate-config), and
writes a missing top-level `host` from --host (check flags a host outside claude/codex/opencode)
- `git init` when the folder is not a repo
- adds Matt Pocock's helper skills with npx when Node is present and they are missing
It never installs a system tool or signs in: those come back in `needs` with the exact
Expand Down Expand Up @@ -58,6 +59,7 @@
import sys

COLUMNS = ["Backlog", "Ready", "Building", "QA", "Review", "Blocked", "Done"]
HOSTS = ("claude", "codex", "opencode") # config `host`; super-board-host.sh accepts the same
LABELS = {
"qa": ("0e8a16", "Test what exists; skips Building (super-board)"),
"bug": ("d73a4a", "Something is broken; built, tested, reviewed (super-board)"),
Expand Down Expand Up @@ -215,9 +217,10 @@ def machine_tz():
return "UTC"


def migrate_config(cfg):
def migrate_config(cfg, host=None):
"""Return (new config, [human-readable change lines]). Pure; never drops a project setting
that still means something."""
that still means something. `host` (the actual worker host) fills a missing top-level
`host` key; a set one is never replaced."""
c = json.loads(json.dumps(cfg))
changes = []
variant = c.pop("variant", None)
Expand Down Expand Up @@ -268,6 +271,9 @@ def migrate_config(cfg):
if not c.get("worker_backend"):
c["worker_backend"] = "workflow"
changes.append("worker_backend \"workflow\" added")
if host and "host" not in c:
c["host"] = host
changes.append(f"host \"{host}\" added")
return c, changes


Expand Down Expand Up @@ -295,6 +301,7 @@ def check(root, pack, host="claude", home=None, catalog=None):
old_dirs = [d for d in OLD_SKILL_DIRS if os.path.isdir(os.path.join(claude, "skills", d))]
configs = {}
verification = {}
host_errors = {}
for p in sorted(glob.glob(os.path.join(claude, "super-board", "configs", "*.json"))):
cfg = load_json(p)
ready = subprocess.run([sys.executable, os.path.join(os.path.dirname(__file__),
Expand All @@ -303,7 +310,10 @@ def check(root, pack, host="claude", home=None, catalog=None):
if ready.returncode:
verification[os.path.relpath(p, root)] = ready.stderr.strip()
if isinstance(cfg, dict):
_, ch = migrate_config(cfg)
if "host" in cfg and cfg["host"] not in HOSTS:
host_errors[os.path.relpath(p, root)] = (
f"host \"{cfg['host']}\" must be one of {', '.join(HOSTS)}")
_, ch = migrate_config(cfg, host)
if ch:
configs[os.path.relpath(p, root)] = ch
older = bool(upgrade_rec.get("from")) or bool(old_dirs) or any(
Expand Down Expand Up @@ -332,12 +342,13 @@ def check(root, pack, host="claude", home=None, catalog=None):
ok.append("git repo found")
if helpers:
ok.append("required worker skills resolved for " + host)
red = bool(missing or old_dirs or configs or verification or not git or needs or not helpers)
red = bool(missing or old_dirs or configs or verification or host_errors or not git or needs or not helpers)
return {
"root": root, "pack": pack, "pack_version": pack_version, "installed_version": have_v,
"upgrade": older, "from": from_v or ("older" if older else None),
"added_skills": upgrade_rec.get("added_skills", []),
"missing": missing, "old_dirs": old_dirs, "configs": configs, "verification": verification, "git": git,
"missing": missing, "old_dirs": old_dirs, "configs": configs, "verification": verification,
"host_errors": host_errors, "git": git,
"helper_skills": helpers, "helper_readiness": readiness, "needs": needs, "os": fam, "ok": ok, "green": not red,
}

Expand Down Expand Up @@ -397,7 +408,7 @@ def fix(root, pack, helpers=True, dry=False, host="claude", home=None, catalog=N

for rel, ch in before["configs"].items():
p = os.path.join(root, rel)
new, _ = migrate_config(load_json(p))
new, _ = migrate_config(load_json(p), host)
if not dry:
write_json(p, new)
if before["upgrade"]:
Expand Down Expand Up @@ -455,7 +466,7 @@ def text_report(res):
lines.append(f" ✗ missing {entry['name']} for {res['helper_readiness']['host']}; install it in that host and restart")
if res.get("helper_readiness", {}).get("error"):
lines.append(" ✗ " + res["helper_readiness"]["error"])
for path, error in res.get("verification", {}).items():
for path, error in {**res.get("verification", {}), **res.get("host_errors", {})}.items():
lines.append(f" ✗ {path}: {error}")
for n in res.get("needs", []):
lines.append(f" Needs your machine: {n['name']} isn't installed. It's needed for {n['why']}.")
Expand Down
7 changes: 7 additions & 0 deletions skills/super-board/references/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
// only; the dispatcher refuses to run (exit 78) unless set here.
"worker_backend": "workflow",

// Agent host this project runs on: "claude" | "codex" | "opencode". Separate from
// worker_backend. Onboard and setup's fix ALWAYS write it (setup fills a missing key
// from --host and never replaces a set one); setup's check flags any other value.
// `super-board-host.sh --config <this file>` uses it only when SUPER_BOARD_HOST and
// every env sign (Codex ids, OPENCODE, CLAUDECODE) are absent. See docs/hosts.md.
"host": "claude",

// Optional post-completion adapter; never bundles or installs the external skill.
// Runs only after selected work is verified merged and no work/approvals remain.
// The supervising host checks the resulting diff; the helper itself is read-only.
Expand Down
3 changes: 2 additions & 1 deletion skills/super-board/references/onboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,8 @@ Write it atomically (temp file + rename) after every answer. A halt never loses
│ ├─ .claude/super-board/configs/<slug>.json (committed): description, project, target
│ │ {type: repo | repo+url}, repo, base_branch, timezone, columns (the seven), labels,
│ │ paths, merge_policy, migrations, collect (incl. custom), notifications {channel:
│ │ "session", bot_identity}, worker_backend "workflow", verify_commands (non-blank project checks). No `variant`.
│ │ "session", bot_identity}, worker_backend "workflow", host (always: `bash $SB/super-board-host.sh`;
│ │ on exit 65 ask claude/codex/opencode), verify_commands (non-blank project checks). No `variant`.
│ ├─ .claude/super-board/active ← <slug>
│ ├─ .gitignore += .claude/super-board/active, onboard-answers.json, onboard-staged/,
│ │ backup/, inflight/, migrations/, upgrade.json (all under .claude/super-board/)
Expand Down
9 changes: 7 additions & 2 deletions skills/super-board/references/run-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ pins one model for every card and skips the router.
Keep this run choice in each wave and resume command; skip only the Claude usage guard.

**Inside Codex, plain `run` is `--codex`.** Your tool list has no Workflow tool, or
`bash .claude/bin/super-board-host.sh` prints `codex` → run as `--codex` (tier flags and
`bash .claude/bin/super-board-host.sh --config <config-path>` prints `codex` → run as `--codex` (tier flags and
`--codex=<model>` still apply); it counts as explicit `--codex`, Review included.
Say so once, in the first wave report: `🔁 codex host — running as --codex`. Never improvise a Claude workflow wave in Codex.

**Unknown host halts.** Run the host check before preconditions. Exit 65 (no host sign) or
`opencode` (no OpenCode backend yet) halts before any claim, even with no Workflow tool, with one line:
`⛔ unknown host — set SUPER_BOARD_HOST=claude|codex and re-run`. Never guess a backend.

After the same plan and claim steps, launch this command as a background task:
`bash .claude/bin/super-board-codex-wave.sh --config <config-path> --cards <claimed-cards.json> --codex[=<model>] --tier <low|medium|high> --output <wave-result.json>`.
The helper starts one `super-board-codex-lane.sh` per card/lane, cards in parallel,
Expand Down Expand Up @@ -242,7 +246,8 @@ Repeat until a done condition or halt gate fires:
there is no cross-session claim at all, so never run two orchestrators
(or /loop re-entries) against the same board without bot_identity.
4. **Launch** — With `--codex`, or no Workflow tool, or the host check printing `codex`, use
"Codex runs" above; otherwise Workflow tool with
"Codex runs" above. The host check printing `claude` uses the Workflow tool; exit 65 or
`opencode` never reaches Launch (see "Unknown host halts"). Workflow tool with
`scriptPath: .claude/workflows/super-board-wave.js` and
`args: { configPath, cards, humanApprovesMerge, tier }` (`cards` hydrated after the planner: each
carries `lane` and `labels`, and a `qa` card skips the Builder). Runs in the background; the
Expand Down
45 changes: 41 additions & 4 deletions tests/test-host.sh
Original file line number Diff line number Diff line change
@@ -1,20 +1,57 @@
#!/usr/bin/env bash
# Tests super-board-host.sh — the Codex-host check that turns a plain `run` into `--codex`.
# Tests super-board-host.sh — which agent host runs this shell, else exit 65 so the run
# asks for a host instead of guessing.
set -euo pipefail
KIT=$(cd "$(dirname "$0")/.." && pwd)
S="$KIT/scripts/super-board-host.sh"
fail() { echo "FAIL: $1" >&2; exit 1; }
# A clean env, so the test answers the same inside Claude Code, Codex or CI.
host() { env -i PATH="$PATH" "$@" bash "$S"; }

[ "$(host)" = claude ] || fail 'no Codex ids means claude'
[ "$(host CLAUDECODE=1)" = claude ] || fail 'CLAUDECODE means claude'
[ "$(host CODEX_THREAD_ID=01a1)" = codex ] || fail 'CODEX_THREAD_ID means codex'
[ "$(host CODEX_SESSION_ID=01a1)" = codex ] || fail 'CODEX_SESSION_ID means codex'
[ "$(host CODEX_THREAD_ID=01a1 CLAUDECODE=1)" = codex ] || fail 'a Codex lane under Claude is codex'
[ "$(host CODEX_HOME=/x CODEX_SANDBOX=seatbelt)" = claude ] || fail 'CODEX_HOME/CODEX_SANDBOX alone are not a host'
[ "$(host CODEX_THREAD_ID=)" = claude ] || fail 'an empty id is not a host'
[ "$(host SUPER_BOARD_HOST=codex)" = codex ] || fail 'override forces codex'
[ "$(host SUPER_BOARD_HOST=claude CODEX_THREAD_ID=01a1)" = claude ] || fail 'override beats the env'
[ "$(host SUPER_BOARD_HOST=opencode)" = opencode ] || fail 'override accepts opencode'
rc=0; host SUPER_BOARD_HOST=gemini >/dev/null 2>&1 || rc=$?
[ "$rc" = 64 ] || fail "bad override exits 64, got $rc"

# No sign and no override: empty stdout, a hint on stderr, exit 65 — never a guessed claude.
ERR=$(mktemp); trap 'rm -f "$ERR"' EXIT
nohost() {
local rc=0 out
out=$(host "$@" 2>"$ERR") || rc=$?
[ "$rc" = 65 ] || fail "'$*' should exit 65, got $rc"
[ -z "$out" ] || fail "'$*' should print nothing to stdout, got '$out'"
grep -q SUPER_BOARD_HOST "$ERR" || fail "'$*' should explain SUPER_BOARD_HOST on stderr"
}
nohost
nohost CODEX_HOME=/x CODEX_SANDBOX=seatbelt # not host signs
nohost CODEX_THREAD_ID= CLAUDECODE= # empty values are not signs
nohost AGENT=1 # OpenCode sets it, but so do other tools

# OpenCode v2.0.22 exports OPENCODE=1. It wins over a CLAUDECODE leaked from a parent Claude
# session; the override and a Codex id still win over it.
[ "$(host OPENCODE=1)" = opencode ] || fail 'OPENCODE=1 means opencode'
[ "$(host OPENCODE=1 CLAUDECODE=1)" = opencode ] || fail 'OpenCode under a Claude parent is opencode'
[ "$(host SUPER_BOARD_HOST=claude OPENCODE=1)" = claude ] || fail 'override beats the OpenCode sign'
[ "$(host CODEX_THREAD_ID=01a1 OPENCODE=1)" = codex ] || fail 'a Codex id beats the OpenCode sign'

# Config `host` (top-level key): used only when no override and no env sign is present.
T=$(mktemp -d); trap 'rm -f "$ERR"; rm -rf "$T"' EXIT
printf '{"host":"codex"}' > "$T/codex.json"
printf '{"worker_backend":"workflow"}' > "$T/none.json"
printf '{"host":"gemini"}' > "$T/bad.json"
cfg() { local f=$1; shift; env -i PATH="$PATH" "$@" bash "$S" --config "$f"; }
[ "$(cfg "$T/codex.json")" = codex ] || fail 'config host codex prints codex'
[ "$(cfg "$T/codex.json" CLAUDECODE=1)" = claude ] || fail 'an env sign beats config host'
[ "$(cfg "$T/codex.json" SUPER_BOARD_HOST=opencode)" = opencode ] || fail 'override beats config host'
for f in none missing; do
rc=0; out=$(cfg "$T/$f.json" 2>/dev/null) || rc=$?
[ "$rc" = 65 ] && [ -z "$out" ] || fail "config $f without host should exit 65, got $rc '$out'"
done
rc=0; cfg "$T/bad.json" >/dev/null 2>&1 || rc=$?
[ "$rc" = 64 ] || fail "a bad config host exits 64, got $rc"
echo "PASS: super-board-host"
Loading
Loading