relay-xyz
Drive an automated /relay review loop on THIS repo with the shipped relay-automation harness (relay-drive.sh + codex-turn.sh / agy-turn.sh / poll.sh) rather than improvising the handoff by hand. Use w
SKILL.mdskills/1-hourly/relay-xyz/SKILL.md
GitHubrelay-xyz — automated relays on the shipped harness
ALWAYS locate and run the bundled locator first — never claim the harness is missing without it. Resolve the installed skill from stable install roots; do not assume the session CWD is this repo:
L=""
for candidate in "${XYZ_HARNESS:+$XYZ_HARNESS/skills/1-hourly/relay-xyz/find-harness.sh}" \
"$HOME/.claude/skills/relay-xyz/find-harness.sh" \
"$HOME/.codex/skills/relay-xyz/find-harness.sh" \
"$HOME/.gemini/config/skills/relay-xyz/find-harness.sh" \
"$HOME/.gemini/antigravity/skills/relay-xyz/find-harness.sh" \
"$HOME/.gemini/antigravity-cli/skills/relay-xyz/find-harness.sh" \
"$HOME/.agents/skills/relay-xyz/find-harness.sh" \
"$HOME/.zcode/skills/relay-xyz/find-harness.sh" \
"$(git rev-parse --show-toplevel 2>/dev/null)/.claude/skills/relay-xyz/find-harness.sh" \
"$(git rev-parse --show-toplevel 2>/dev/null)/skills/1-hourly/relay-xyz/find-harness.sh"; do
[ -n "$candidate" ] && [ -f "$candidate" ] && { L="$candidate"; break; }
done
[ -n "$L" ] || { echo "relay-xyz: locator not found — install the skill or set XYZ_HARNESS" >&2; exit 1; }
bash "$L" --check
That locator resolves the harness from wherever your CWD is and reports which workers
(tick/codex/agy/cmd/dsh) are on PATH. See
Preconditions below for the
full env-exporting form (eval "$(... --env)" + cd) that every recipe in this doc assumes has already
run.
This repo already ships the relay automation. Don't reinvent the CLI handoff turn by turn — call
the scripts under relay-automation/. /relay defines the thread format
and turn protocol and scaffolds the dated file; relay-xyz is the thin repo-specific layer that
drives that thread to completion with the shipped supervisor + turn-takers.
Use /relay to create the thread (or reuse one under relay-system/<date>/), then relay-xyz to
run it headless or hands-free.
When to use
- "Run an automated relay" / "drive this relay to completion" / "run the relay harness."
- "Have Codex or agy review
<file>end-to-end." - Setting up the all-Claude hands-free
/looppoll so two Claude windows self-serialize. - Running automated relays in two different repos at the same time on one machine — see
Concurrent relays across repos (each repo needs its own
vendored
.xyz/). - You have a relay thread (or are about to scaffold one with
/relay); the current working tree may be a foreign repo if the locator can reach a canonical XYZ-forge harness.
Not for: scaffolding a brand-new thread from scratch (that's /relay), or work that needs a human checkpoint between every turn (use plain /relay
manual mode).
Bounded marathon repair to PR
This procedure is performed by an active supervising caller, not by the progress observer or
a durable background skill. Before launch, obtain explicit machinery-repair authorization and an
absolute UTC recovery deadline. The allowance is one repair episode and at most one continuation
dispatch, shared across diagnosis, consult, implementation, plan/final QA, gates and publication.
Do not pause or extend the deadline during repair, nest recovery, raise caps, use --force, or
reset attempts by choosing a new token. Without that authorization, report the halt and park.
- Preserve the original halt, result receipt, relay/escalation and run-log pointers; record the exact plan, product/coordination/harness roots, revision, failure attempts and remaining deadline. Distinguish liveness from product progress (a newly accepted revision with gate and independent review) and preparation progress (a reviewed repair PR or actionable reproduced blocker).
- Reproduce and trace a local machinery defect using debug-mantra. Name its failing input, narrow write allowlist and falsifier; inspect current issues/PRs before creating duplicate work. A small diff is insufficient. Exclude lock/ownership, containment, gate/review semantics, schemas, auth/network bridges, dependency expansion, deletion, product-scope changes and unknown causes.
- Run
$consultwith three distinct seats reading the same retained evidence. Require three affirmative recommendations for this unattended repair; dissent, abstention, missing output or timeout parks. Record disagreements and your disposition. A successful consult command is not quorum, and advice never overrides the exclusions, deterministic checks or independent QA. - Use
$start-taskin a fresh full clone for the eligible fix: grounded plan and independent Codex plan QA, implementation, existing covering verification in a disposable full clone, final independent Codex QA and required exact-revision gate. No new suites/gates/controllers. Stop at the original deadline or any exhausted cap and retain unfinished work with its next action. - Publish the verified repair PR against
development; inspect its actual base, head SHA, diff and review/gate receipts. Default to publish and park. Report original failure, repair SHA, evidence and one specific next action. Do not merge, hot-patch an installed harness, or rewrite the original failure as a successful run. - Held capability: this delivery does not automatically continue from an unmerged repair PR. A future continuation must prove exclusive ownership and stopped descendants, exact PR revision and independent QA/gate evidence, #752-safe re-entry and #1004 adoption, preserved original authorization/plan/state roots/attempts/deadline, and normal preflight/dry-run. It needs explicit authorization for the unmerged dependency and an immutable per-run harness checkout. Unknown predicates mean park; neither publication nor the consult vote authorizes a bypass or a merge.
An idle gap can be correct. Promise a bounded attempt and durable, attributable evidence; never promise an accepted product change merely because several unattended hours were authorized.
First-time setup on a new clone or machine (make the skill discoverable)
First check whether Skills Army HQ already manages this skill on the machine:
readlink ~/.claude/skills/relay-xyz # or the relay-xyz entry in your app's skills root
Compare the printed target with your Skills Army collection root: $XYZ_SKILLS_ROOT when it is set,
otherwise ~/git-pulse-sync/Deployed Skills. Three outcomes:
Managed by Skills Army HQ: skip install.sh. The link points into the collection root
(<collection root>/relay-xyz). This counts even when the link is dangling, for example after the
collection moved or mid-sync. Skills Army HQ owns the link, and its rule is not to run copied install.sh
files. Running it there exits 1 on a live link (GH-678 keeps it). On a dangling one it replaces the link
with this clone and adds links in app roots the collection does not target. Repair or refresh links with
Skills Army HQ (sync.py), then go straight to the locator below.
To check the harness, run --check from the installed path and from outside any repo:
cd "$HOME" && bash "$HOME/.claude/skills/relay-xyz/find-harness.sh" --check # or your app root's relay-xyz
Inside a task clone, --check resolves that clone and would offer to save it. When it resolves your
canonical clone, run the one-line save this harness on this Mac command it prints. When it finds no
harness at all, it exits 1 without a save command. Write the per-Mac config yourself:
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/xyz" && printf '%s\n' /path/to/XYZ-forge > "${XDG_CONFIG_HOME:-$HOME/.config}/xyz/harness"
The alternative is to prefix a single command with XYZ_HARNESS=/path/to/XYZ-forge. Do not export it from
shell startup files.
A live link to somewhere else (another XYZ-forge clone, or a vendored .xyz/ copy). Another installer
owns it, and install.sh refuses it with exit 1 (GH-678). If that target is the copy you want, keep it and
go to the locator. To switch, remove the link yourself, then follow the next case.
Not managed (no link, or a dangling link outside the collection root). This repo keeps its skills in top-level skills/, which
Claude Code does not scan. A session finds relay-xyz only if it's symlinked into ~/.claude/skills/.
A fresh clone or second machine without Skills Army HQ has no such symlink, so the skill is invisible in
every session there — the "other VS Code sessions can't find the relay-xyz files" failure. Fix it
once per maintained clone (idempotent, self-locating, no hardcoded path):
bash skills/1-hourly/relay-xyz/install.sh # symlinks this clone's skills/1-hourly/relay-xyz into the Claude Code, Codex, and Gemini/Antigravity skills roots
It also replaces a stale/dangling symlink and verifies find-harness.sh resolves the harness. The
locator below handles where the harness scripts live; this step handles whether Claude Code can
load the skill at all — a layer the locator can't reach, since it runs only after the skill loads.
Preconditions — locate the harness (bundled locator, never hardcode a path)
relay-xyz ships its own device-agnostic locator, find-harness.sh, beside this
skill. It checks an explicit override, a caller's vendored harness, the current repo, its own
installed location, a per-Mac config at ${XDG_CONFIG_HOME:-$HOME/.config}/xyz/harness, and bounded
canonical XYZ-forge clone locations. A copied Skills Army deployment therefore works from a foreign
repo. --check shows a command to save the chosen canonical harness in that config and warns when
its cached upstream is ahead. It never fetches while checking.
Run this first. It finds the locator, exports the harness env, cds into the clone that ships the
harness, and prints a one-glance readiness line:
# Find the bundled locator. The skill installs at one of these — all anchored on $HOME or
# the CWD, never an absolute machine path:
for L in "${XYZ_HARNESS:+$XYZ_HARNESS/skills/1-hourly/relay-xyz/find-harness.sh}" \
"$HOME/.claude/skills/relay-xyz/find-harness.sh" \
"$HOME/.codex/skills/relay-xyz/find-harness.sh" \
"$HOME/.gemini/config/skills/relay-xyz/find-harness.sh" \
"$HOME/.gemini/antigravity/skills/relay-xyz/find-harness.sh" \
"$HOME/.gemini/antigravity-cli/skills/relay-xyz/find-harness.sh" \
"$HOME/.agents/skills/relay-xyz/find-harness.sh" \
"$HOME/.zcode/skills/relay-xyz/find-harness.sh" \
"./.claude/skills/relay-xyz/find-harness.sh" \
"$(git rev-parse --show-toplevel 2>/dev/null)/skills/1-hourly/relay-xyz/find-harness.sh"; do
[ -n "$L" ] && [ -x "$L" ] && break
done
[ -x "$L" ] || { echo "relay-xyz: locator not found — set XYZ_HARNESS to your XYZ-forge clone"; exit 1; }
eval "$("$L" --env)" # exports HARNESS, TICK, TICK_REPO_ROOT, RELAY_HAS_{TICK,CODEX,AGY,COMMANDCODE,DEEPSEEK}
cd "$HARNESS"
"$L" --check # prints: harness path + which Path-A workers (tick/codex/agy/cmd/dsh) are on PATH
After this, $HARNESS is the harness repo root, $TICK is the absolute bin/tick, and
TICK_REPO_ROOT points tick at that clone's event log. The relay/turn scripts self-resolve their
own location ($(dirname "$BASH_SOURCE")/..), so invoke them with repo-relative paths exactly as
the relay automation README shows.
The relay always operates on the
harness clone (its .tick/ log and guarded git root live there), whatever repo you launched from —
so a clone with only relay-system/ thread files still drives the real harness next door.
Concurrent relays across repos (same machine)
relay-drive.sh/marathon-drive.sh hold one global driver lock per harness clone. This is
intentional — two worktrees on the same ROOT@HEAD can corrupt git state (GH-42) — but it means
every repo pointed at the same harness clone shares that one lock, so their automated relays
serialize: a second one blocks (exit 1) until the first frees.
The driver-lock exclusion matrix (GH-354 Phase 3 — canonical)
This table is the one canonical statement of what the driver lock guarantees (PDDA Principle #4). Anything else that describes the lock — driver headers, monitor docs — links here rather than restating it. A second copy is how the wrong sentence in
marathon-drive.sh:194-196survived.
Both drivers resolve the lock through one shared resolver (GH-448) — utils/py/rtl.py::driver_lock_path
and its Bash twin relay-automation/driver-lock-lib.sh::driver_lock_path_for_repo — which yields three
shapes:
| Repo shape | Lock path | Display label |
|---|---|---|
normal clone (.git is a directory) |
<root>/.git/relay-driver.lock |
.git/relay-driver.lock |
linked worktree (.git is a file) |
<git-common-dir>/relay-driver.lock — i.e. the parent clone's |
.git/relay-driver.lock |
vendored .xyz/ (no .git) |
<root>/.relay-driver.lock |
.relay-driver.lock |
The middle row is the load-bearing one: a linked worktree does not get its own lane. It resolves
to the same lock as the clone it was cut from — pinned by test/gh448-driver-lock-resolver.sh
("worktree case resolves to the git COMMON dir, not <worktree>/.git/…", plus a bash/python
"parity" assertion per shape). So all three driver pairs mutually exclude per clone:
| Pair | Excludes? | Pinned by |
|---|---|---|
| marathon ↔ marathon | yes | test/driver-lock.sh — "live lock (alive holder) → driver refuses (exit 1)", "live lock left intact (not stolen)" |
| marathon ↔ relay | yes | test/gh376-relay-drive-lock-parity.sh — "THE PIN (bash): relay-drive.sh refuses — the frozen twin agrees with the Python half" |
| relay ↔ relay | yes | test/gh376-relay-drive-lock-parity.sh — "neither lane left a worktree-local lock behind at $WT/.relay-driver.lock", "twin parity: both lanes emit a byte-identical REFUSAL" |
This became true only in GH-376. Before it, relay-drive used a two-branch guess with no case for
a linked worktree, so it took a per-worktree .relay-driver.lock while marathon-drive took the
shared one — the bottom two rows did not exclude, and #354's original premise that the lock was
"the hard blocker (by design)" was false for them. The negative control
("pre-fix 2-branch logic sails past the held lock") is what pins that it can't return.
To actually run two swarms concurrently, use separate full clones — not linked worktrees, which
share the lock by the table above, and not a shared harness, which serializes. Per-run hygiene that
keeps the event stream readable even across clones: distinct --phase-id / --relay-task, plus
MARATHON_LANE_NS for the lane namespace and an explicit XYZ_SESSION_ID (its fallback to PHASE_ID
cannot tell one run from another).
To run relays in different repos at the same time on one machine, give each repo its own harness
so each gets its own lock, .tick/, and worktrees:
| Install path / Tier | Ships | Relay capability | Releases ledger? | Lock |
|---|---|---|---|---|
install.sh (tick-only) |
bin/tick + src/*.js |
❌ falls back to the centralized harness | ❌ no | shared (serializes) |
Tier 1 (default): xyz-vendor.sh <target-repo> [--no-register] |
full core harness (relay-automation/ minus xyz-releases-onboard.sh, bin/, src/, test/, skills/, utils/ minus overlay) into gitignored .xyz/ |
✅ per-repo | ❌ no overlay | own .xyz/.relay-driver.lock |
Tier 2 (opt-in): xyz-vendor.sh <target-repo> --with-releases (or auto-detected via releases.db at root) |
full core harness + RELEASES overlay (releases_app.py, releases_cycle.py, releases-merge-resolve.sh, release-lanes.sh, utils/timeline/, xyz-releases-onboard.sh, RELEASES-DB-FAQS.md) |
✅ per-repo | ✅ opt-in overlay | own .xyz/.relay-driver.lock |
Updating a vendored copy (xyz-sync.sh update, or re-running xyz-vendor.sh over an existing
.xyz/) replaces the harness code and preserves the per-repo state above — relay-system/,
.tick/, .relay-driver.lock, and the XYZ.json* telemetry ride across the rebuild (GH-312).
Note that RELEASES ledger runtime state (releases.db, releases.sql,
RELEASES-PREVIEW.html) lives at the target repository root, outside .xyz/, while .xyz/-resident
runtime state is preserved across swaps. This matters because .xyz/ is gitignored: state lost there
is unrecoverable, with no reflog or stash behind it. A new runtime artifact under .xyz/ must be added
to the preserve list in xyz-vendor.sh's materialize_vendor(), or the next update will delete it.
So: xyz-vendor.sh (not install.sh) is the path to concurrent per-repo relays. Once a repo has
.xyz/, find-harness.sh prefers it automatically (env → .xyz/ → current repo → script-relative → config → search), and
find-harness.sh --check warns when you're in a foreign repo with no .xyz/ (using the shared
harness) and points you at the vendor command. Two vendored repos each run relay-drive.sh from their
own .xyz/relay-automation/, holding independent locks — no contention. (Editing the central harness
clone also can't disturb a vendored run, since it uses its own pinned .xyz/ copy.)
Per-repo persistence (don't cache a path)
Once a target repo has used relay-xyz once, don't leave behind a machine-specific breadcrumb so the
next session skips the "run find-harness.sh first" gate above. The only two persistence channels
Claude Code auto-loads are:
- The target repo's memory — seed a line the first time a run succeeds there, e.g. "this repo uses
relay-xyz; run
find-harness.sh --checkfirst." - That repo's own
CLAUDE.md, by skill name — a pointer such as "for automated relays, use therelay-xyzskill" (not a path).
Either breadcrumb must be a portable pointer — the skill name or the find-harness.sh command —
never a cached absolute path and never a bare root pointer file dropped into the target repo. A
bare file isn't auto-loaded (a skimming agent skips it exactly like it skips this doc's own body), it's
machine-specific (breaks on the next clone or device), a stale cached path is worse than no path at
all, and cleaning one up later has cross-repo blast radius. relay-xyz never auto-installs any file
into a target repo. Only install.sh writes anything, and it writes only skill symlinks into the
machine's app skills roots (~/.claude/skills/, ~/.codex/skills/, and the Gemini/Antigravity roots),
never into the target repo itself.
The two automated paths
Role split (GH-221): Claude Code is the orchestrator/reviewer here, not a default builder. The
Claude Code session driving relay-drive.sh/marathon-drive.sh plans, dispatches, and reviews/verifies
turns — it does not spawn itself as the headless build lane. Agy CLI and Codex CLI are the builders:
the two cost-blind (subscription-billed, not per-call API) headless turn-takers --agent-cmd /
--builder default to. Claude CLI (subscription or API, according to authentication) is not a builder by default —
--builder claude / a claude-turn.sh shim stay fully supported, but only as an explicit,
usage-acknowledged choice the user makes locally, never something a session reaches for on its own
reasoning that it's "just another supported turn-taker." If a task needs a headless build lane and
neither agy nor codex is on PATH, stop and ask — don't default to spawning a headless Claude CLI turn.
| Path | One session? | Models | Driver |
|---|---|---|---|
| A. Headless single-session | yes — Claude drives both roles | Codex / agy as co-equal headless workers | relay-drive.sh + a turn-taker shim |
| B. Hands-free poll | no — two live Claude windows | all-Claude | poll.sh under /loop in each window |
Path A is the marquee flow — what "have Codex or agy review this for me" means. Path B is the all-Claude self-serializing loop: no human nudge, no second model.
Path A — headless single-session (relay-drive.sh + a shim)
relay-drive.sh is the supervisor (round cap, no-progress escalation, reads the file's STATUS:
as the terminal signal). The turn-taker is --agent-cmd — a shipped shim (codex-turn.sh or
agy-turn.sh) that owns the safety boundary: path-allowlist, commit-bypass guard, no push.
Whose-turn is a tick relay task, handed off with tick release --to.
End-to-end headless review of an artifact (run after Preconditions — $TICK and $HARNESS set, CWD
is the harness clone). Choose either worker. The examples below pass ALLOW_PATHS="$ARTIFACT", which
fits a build/fix turn; for a pure review turn set ALLOW_PATHS="" (relay file only) so the
reviewer reports instead of editing — see the env table's ALLOW_PATHS row (note that fixed log paths break concurrent same-machine runs; prefer the shims' per-PID default or use per-PID $$ variables):
The one-line form (GH-346 Phase 3a) — name the reviewer, skip the table
If a profile exists for the model you want, the whole env block below collapses to one call:
eval "$(relay-automation/resolve-profile.sh 'glm 5.3 max' --env)"
ALLOW_PATHS="" relay-automation/relay-drive.sh \
--relay-file "$RELAY" --relay-task "$TASK" \
--agent-cmd "$RELAY_AGENT_CMD" --reviewer "$RELAY_REVIEWER" --review-once
--env emits the lane's *_AGENT, *_MODEL, its gateway variable, *_REASONING_EFFORT,
*_FLAGS, plus RELAY_AGENT_CMD, $HARNESS and $TICK. Profiles live in the profiles block of
~/.xyz/device_config.json:
"profiles": {
"glm 5.3 max": { "harness": "commandcode", "gateway": "self",
"model": "zai-org/glm-5.3", "effort": "max" },
"qwen 3.8 max": { "harness": "deepseek", "gateway": "openrouter",
"model": "qwen/qwen3.8-max" }
}
"gateway": "self" is for a harness that is its own router — Command Code resolves models from
its own catalog and has no OpenRouter key or base URL, so naming a third-party router there would
emit a value the shim ignores and telemetry would then record a route that never happened.
Name matching is fuzzy (it reuses resolve-model-alias.sh), so GLM5.3 max, glm 5.3 max and
max glm 5.3 are one entry. --list shows every profile and flags broken ones; --explain says
which tier answered.
This never blocks a turn. A missing config, malformed JSON, or an unmatched name falls through
to the shims' own defaults — the tables below — and says why on stderr. An explicit *_AGENT +
*_MODEL already in the environment always wins and is never second-guessed.
The tables below remain correct and are what a fall-through lands on. Use them when no profile exists, or when you want a one-off that is not worth naming.
| Worker | Availability check | Handoff target | Env prefix | Shim | Log |
|---|---|---|---|---|---|
| Codex | "$RELAY_HAS_CODEX" = 1 |
codex |
CODEX_AGENT=codex ALLOW_PATHS="$ARTIFACT" CODEX_LOG="${TMPDIR:-/tmp}/codex-turn-$$.log" |
relay-automation/codex-turn.sh |
${TMPDIR:-/tmp}/codex-turn-$$.log |
| agy | "$RELAY_HAS_AGY" = 1 |
agy |
AGY_AGENT=agy ALLOW_PATHS="$ARTIFACT" AGY_LOG="${TMPDIR:-/tmp}/agy-turn-$$.log" |
relay-automation/agy-turn.sh |
${TMPDIR:-/tmp}/agy-turn-$$.log |
| Commandcode | "$RELAY_HAS_COMMANDCODE" = 1 |
commandcode |
COMMANDCODE_AGENT=commandcode ALLOW_PATHS="$ARTIFACT" COMMANDCODE_LOG="${TMPDIR:-/tmp}/commandcode-turn-$$.log" |
relay-automation/commandcode-turn.sh |
${TMPDIR:-/tmp}/commandcode-turn-$$.log |
| DeepSeek | "$RELAY_HAS_DEEPSEEK" = 1 |
deepseek |
DEEPSEEK_AGENT=deepseek ALLOW_PATHS="$ARTIFACT" DEEPSEEK_LOG="${TMPDIR:-/tmp}/deepseek-turn-$$.log" |
relay-automation/deepseek-turn.sh |
${TMPDIR:-/tmp}/deepseek-turn-$$.log |
Model selection per worker (GH-346) — the one lookup that used to mean reading each shim's
source. RELAY_HAS_* is set by find-harness.sh --env; every worker below also honors
<PREFIX>_FLAGS and RELAY_TURN_TIMEOUT_S:
| Worker | Model env | Default | Notes |
|---|---|---|---|
| Codex | (none) | codex CLI's own | codex-turn.sh never passes --model; set it in codex's own config |
| agy | AGY_MODEL |
agy CLI's own | validated against agy models — an unlisted id fails fast rather than falling back |
| Commandcode | COMMANDCODE_MODEL |
meta/muse-spark-1.2-contributor |
cmd --list-models for the live catalog (GLM, Qwen, DeepSeek all reachable here) |
| DeepSeek | DEEPSEEK_MODEL |
deepseek/deepseek-v4-pro |
accepts a colloquial alias ("deepseek v4 pro"); also DEEPSEEK_PROVIDER (openrouter|deepseek|alibaba) selecting the endpoint and its key variable. An unrecognised value REFUSES the turn (it used to fall through to DeepSeek silently). alibaba is the Alibaba Token Plan, which serves Qwen under bare ids (qwen3.8-max, not qwen/...) and reads its key from ALIBABA_TOKEN_PLAN_API_KEY or, failing that, the file named by ALIBABA_TOKEN_PLAN_API_KEY_FILE. |
| Aider | AIDER_MODEL |
openrouter/anthropic/claude-sonnet-5, or openai/agents-a1 when AIDER_OPENAI_API_BASE is set |
force AIDER_FLAGS=--edit-format diff on GLM |
| Claude | CLAUDE_MODEL |
claude-sonnet-4-6 |
Explicit operator choice, never a session default; see subscription setup |
| Pi | PI_MODEL |
none — required | refuses to guess (GH-295) |
Codex example:
# 0. The reviewer you want must be on PATH (set by the locator).
[ "$RELAY_HAS_CODEX" = 1 ] || { echo "codex not on PATH — use agy or Path B"; exit 1; }
# 1. Have a relay thread with an embedded "▶ TAKE YOUR TURN" block.
# Reuse one under relay-system/<date>/, or scaffold a fresh thread with /relay first.
RELAY=relay-system/<date>/<slug>.md
ARTIFACT=<repo-relative-path-the-turn-reviews> # e.g. skills/1-hourly/relay-xyz/SKILL.md
TASK="RELAY-$(basename "$RELAY" .md)" # use a per-relay id, not literal RELAY-TURN
# 2. Seed the relay task and hand the first turn to the Codex agent.
"$TICK" log task.created "$TASK" --agent claude-a
"$TICK" claim "$TASK" --agent claude-a --paths "$ARTIFACT"
"$TICK" release "$TASK" --agent claude-a --to codex
# 3. Drive it. The shim dispatches ONLY when the token's actor == CODEX_AGENT.
CODEX_AGENT=codex ALLOW_PATHS="$ARTIFACT" CODEX_LOG="${TMPDIR:-/tmp}/codex-turn-$$.log" \
relay-automation/relay-drive.sh \
--relay-file "$RELAY" \
--relay-task "$TASK" \
--agent-cmd relay-automation/codex-turn.sh \
--reviewer codex \
--round-cap 4
agy example:
[ "$RELAY_HAS_AGY" = 1 ] || { echo "agy not on PATH — use codex or Path B"; exit 1; }
RELAY=relay-system/<date>/<slug>.md
ARTIFACT=<repo-relative-path-the-turn-reviews>
TASK="RELAY-$(basename "$RELAY" .md)"
"$TICK" log task.created "$TASK" --agent claude-a
"$TICK" claim "$TASK" --agent claude-a --paths "$ARTIFACT"
"$TICK" release "$TASK" --agent claude-a --to agy
AGY_AGENT=agy ALLOW_PATHS="$ARTIFACT" AGY_LOG="${TMPDIR:-/tmp}/agy-turn-$$.log" \
relay-automation/relay-drive.sh \
--relay-file "$RELAY" \
--relay-task "$TASK" \
--agent-cmd relay-automation/agy-turn.sh \
--reviewer agy \
--round-cap 4
Commandcode example:
[ -x "$(command -v cmd 2>/dev/null)" ] || { echo "cmd not on PATH — use codex or agy"; exit 1; }
RELAY=relay-system/<date>/<slug>.md
ARTIFACT=<repo-relative-path-the-turn-reviews>
TASK="RELAY-$(basename "$RELAY" .md)"
"$TICK" log task.created "$TASK" --agent claude-a
"$TICK" claim "$TASK" --agent claude-a --paths "$ARTIFACT"
"$TICK" release "$TASK" --agent claude-a --to commandcode
COMMANDCODE_AGENT=commandcode ALLOW_PATHS="$ARTIFACT" COMMANDCODE_LOG="${TMPDIR:-/tmp}/commandcode-turn-$$.log" \
relay-automation/relay-drive.sh \
--relay-file "$RELAY" \
--relay-task "$TASK" \
--agent-cmd relay-automation/commandcode-turn.sh \
--reviewer commandcode \
--round-cap 4
$TICK is absolute, so either worker path still works if CWD drifts.
Important — run the shim OUTSIDE the Bash sandbox. When you (Claude Code) drive this, the
codex / agy subprocess needs the OS keychain + outbound network to authenticate. Claude Code's
Bash sandbox blocks both: codex errors (looks like a keychain/login fault, but it's the sandbox),
and agy -p fails silently — exit 0, empty output (the shim catches this and exits 5, but only
un-sandboxed). Run these Bash calls with dangerouslyDisableSandbox: true. (Memory:
codex-cli-needs-sandbox-disabled, agy-antigravity-cli.)
Important — never hand-roll backgrounding for a driven run (GH-183/187 dogfood, 2026-07-10). A
multi-round marathon-drive.sh/relay-drive.sh run can easily exceed the calling tool's own
foreground timeout. Always use that tool's native background-execution mechanism (e.g. Claude
Code's run_in_background), never a manual nohup ... & disown — a disown can itself fail (exit
- while the backgrounded job survives anyway, undetected, and races a subsequent re-fire against
the same repo/worktree state (observed: a stray
git worktreeplus aticktoken stuck inclaimed, never handed off). If a driver call does get killed mid-turn:git worktree remove --force <path>(seegit worktree listfor stragglers) thentick reap <agent> --by <caller> --task <task>to clear the stuck claim —reapis the sanctioned recovery (logs an auditabletask.releasedevent), not a hand-writtentick release.
Inspecting token state, and a one-shot review
Inspect whose-turn mid-drive:
"$TICK" info <task>prints the token'sstatus/claimer/handoff-to(this is what the driver reads internally). The verb isinfo, notstatus—tick statusis not a verb and errors withunknown verb: status.Single deliberate review turn: pass
--review-oncetorelay-drive.shto drive exactly ONE turn and classify the outcome by exit code, so a correct "changes requested" review is not mistaken for a stall:Exit Meaning 0reviewer Approved/Closed 5reviewer completed a turn and handed back without approving ("changes requested") — a successful single review, not a stall 3genuine stall — the reviewer did nothing (token + STATUS unchanged) 4escalated by design ( STATUS: Escalated), round cap, or a close mismatchWithout
--review-oncea non-approval handback advances the multi-round loop instead (the producer takes the next turn); use--review-oncewhen you want exactly one review and a clean exit code.Review an external / cross-repo artifact (a PR or diff from another repo): pass
--artifact-file <path>torelay-drive.shto seed it READ-ONLY into the isolated worktree at.relay-artifacts/<basename>— the reviewer reads it there without it being committed into the target repo (a reviewer edit fails the turn). To scaffold the thread for such a review, userelay-automation/new-relay.sh --title T --reviewer <agent> --artifact-file <path>(add--embedto inline the artifact in a fence-collision-safe block instead of referencing the seed path). The scaffolder only writes a thread; you still drive it withrelay-drive.shper the paths above.Drive a full relay/build that lands in a DIFFERENT repo (
--target-root): the normal case — the harness lives inXYZ-forge, the code you want built or reviewed-and-committed lives in your own repo. Pass--target-root <repo>torelay-drive.sh(ormarathon-drive.sh): the relay thread +ticktoken stay in the harness clone, while the worktree base,ALLOW_PATHSresolution, and the file-scoped commit all route to<repo>(the harness clone is never touched).find-harness.sh(Preconditions) solves discovery of the harness;--target-rootis the inverse — pointing the harness at your repo. A same-repo lane must OMIT--target-root— passing it for the harness's own repo trips a relay-file off-lane false-positive (exit 6; see #51).One-shot cross-repo review without a relay loop (
CONSULT_ROOT): to apply a lens to a file in a foreign repo with Codex/agy headless — no Producer↔Reviewer loop, advisory only — reach forconsult.shwithCONSULT_ROOTset to that repo. Advisors run in a throwaway worktree ofCONSULT_ROOT, so they read it but can never mutate it:CONSULT_ROOT=/path/to/your/repo \ relay-automation/consult.sh --models codex \ --prompt-file /abs/path/Q.md --out "$TMPDIR/consult"$TMPDIRgotcha: when a prompt/artifact is authored in a sandboxed step and consumed un-sandboxed (or vice-versa),$TMPDIRresolves to a different dir and the path 404s (prompt file not found). Pass prompts/artifacts by absolute path, never a bare$TMPDIR-relative one.
Path B — hands-free poll (all-Claude, two windows)
In each Claude window, run a guarded /loop that uses poll.sh as the gate, then take the turn from
the relay file's embedded instructions. The token is the lock — a window acts only when the token is
claimable by its agent and the artifact scope is clean.
Each window is its own shell, so run Preconditions in each one (or cd into the find-harness.sh --root output) before the loop — the relay-automation/ paths below are relative to $HARNESS.
# In each window (set --agent to that window's id). --claude-agents lists EVERY Claude id in
# the relay so the poller knows whose turns can self-poll vs. which need a cross-model nudge:
/loop 60s run relay-automation/poll.sh --mode relay --agent <claude-a|claude-b> \
--claude-agents "claude-a,claude-b" \
--relay-file relay-system/<date>/<slug>.md --artifact <path> \
--deadline "$(date -v+30M +%s)" --dry-run ;\
if it prints "DECISION: run-runner", take your turn on that relay file per its embedded \
instructions (review/produce, append your block, `tick release RELAY-TURN --to <other>` or \
`tick done` on approve, commit); on "DECISION: stop" CronList+CronDelete this job; else do nothing.
--claude-agents is load-bearing: a turn belonging to an agent not in this list yields
DECISION: nudge-cross-model (a one-line "take your turn" for the human to relay to a non-Claude
window), not idle. List both Claude ids and Path B stays fully hands-free; omit one and that
window's turns surface as a manual nudge. 60s keeps the prompt cache warm; the lock/heartbeat is the
real correctness guard, not the timer. Always set a --deadline so the loop self-closes — cron jobs
are per-session, and you can't stop another window's loop from yours. poll.sh exits 10 on a closed
relay (STATUS: Approved|Closed); see /relay → "Self-closing loops". Optionally run one extra
window with --watchdog-authority (longer interval, e.g. 120s) so a stalled turn escalates exactly
once.
Worked recipe — "Dueling Claudes": for the full copy-paste two-window setup (Reporter↔Maintainer,
same machine, with the one human go-gate before commit), see
relay-automation/DUELING-CLAUDES.md. It carries the exact
/loop strings, the fresh-token-per-run rule, and the foreign-CWD tick pitfalls for Path B.
Path B cadence — fixed interval (today) vs adaptive (GH-33)
The /loop 60s above is a fixed cadence: it wakes every 60s and usually decides "do nothing,"
burning a re-invocation per idle minute. Adaptive cadence lets poll.sh suggest when to wake
next from its own DECISION:
poll.sh --emit-delayadds aDELAY: <seconds> (<reason>)line (act-now → 0, idle backoff → 300, dirty → 30, waiting-for-peer-commit → 90, cross-model → 120; clamped to--deadline). Additive — theDECISION:line is unchanged, so the fixed/loop 60srecipe above still works untouched.relay-automation/relay-loop.shwraps it. Default = one tick that printsNEXT-POLL: <seconds>and exitspoll.sh's code (10 = stop) — the unit a/loopdynamic-mode tick reads to schedule its next wake (viaScheduleWakeup), so an idle relay backs off and a live one stays responsive.- The cadence is not Claude-locked:
relay-loop.sh --sleep-loopself-paces in pure bash (tick → sleepDELAY→ repeat until stop), and theNEXT-POLL/DELAYoutput is plain text any scheduler (cron, systemd timer) can consume./loopdynamic mode is one option, not a dependency.
Dynamic-mode /loop (self-paced) replacement for the fixed recipe — read NEXT-POLL, sleep that long:
/loop run relay-automation/relay-loop.sh --mode relay --agent <claude-a|claude-b> \
--claude-agents "claude-a,claude-b" --relay-file relay-system/<date>/<slug>.md \
--artifact <path> --deadline "$(date -v+30M +%s)" --dry-run ;\
act on "DECISION: run-runner" as above; on "DECISION: stop" CronDelete this job; \
otherwise ScheduleWakeup after the printed "NEXT-POLL:" seconds.
Turn-taker shims & their env
Both shims are thin dispatchers over relay-turn-lib.sh (the model-agnostic containment core), so
they share the same env shape:
| Env | codex-turn.sh |
agy-turn.sh |
Meaning |
|---|---|---|---|
| dispatch gate | CODEX_AGENT |
AGY_AGENT |
NO-OPS unless RELAY_AGENT == this |
| extra writable paths | ALLOW_PATHS |
ALLOW_PATHS |
comma-sep git paths the turn may change (the relay file is always allowed). For a review turn, set ALLOW_PATHS="" — relay file only. If the artifact is writable, the reviewer tends to start editing and building it instead of reviewing (it can read any path regardless), which over-runs the turn cap (exit 7) — observed 2026-06-26. A build/fix turn is the only case that needs the artifact in ALLOW_PATHS. A whole DIRECTORY is a valid lane entry (GH-90): an entry that names an existing directory, or any entry written with a trailing slash (skills/1-hourly/standup/fixtures/ — the only form that works when the turn is about to create the directory), makes that directory and everything beneath it writable. Without either signal the entry is a FILE path, and a file entry is never a bare prefix — GH-59: green must not reach greenfield/output.txt. Before GH-90 a bare directory entry was unmatchable by construction and the turn failed as a containment violation, which reads as a misbehaving builder rather than a malformed lane spec; the off-lane report now names that mistake explicitly. |
| peer id | RELAY_PEER |
RELAY_PEER |
so the turn hands off --to <peer> (else "the other agent") |
| binary | CODEX_BIN |
AGY_BIN |
override the CLI path |
| autonomy | CODEX_FLAGS (default -s workspace-write) |
AGY_MODEL / AGY_FLAGS |
the codex sandbox/approval flags or the agy model |
| transcript | CODEX_LOG |
AGY_LOG |
where the CLI transcript lands (default a $TMPDIR file) |
| turn ceiling | RELAY_TURN_TIMEOUT_S |
RELAY_TURN_TIMEOUT_S |
per-turn wall-clock cap (Aider default: 900s in both runtime shims; hung CLI → exit 7) |
If a fresh device's codex still blocks writes, escalate autonomy:
CODEX_FLAGS='--dangerously-bypass-approvals-and-sandbox' (or -c approval_policy=never).
For agy, the common failure is different: sandboxed runs can exit 0 with empty
output, so run the lane sandbox-OFF before concluding the worker is broken.
Exit codes
relay-drive.sh:0closed Approved/Closed ·3no-progress (token actor didn't move) ·4round-cap / closed-not-approved ·2usage.- shims (
codex-turn.sh/agy-turn.sh):0acted or deferred ·5CLI failed (or agy empty output) ·6off-allowlist edit reverted (or committed mid-turn → reset) ·7timeout-killed ·2usage. poll.sh:10relay closed (stop the loop); on stdout one ofDECISION: run-runner | run-watchdog | nudge-cross-model | stop | idle(nudge-cross-model= turn belongs to an agent not in--claude-agents; relay it as a manual nudge).
Safety boundary (what the shim guarantees)
The shim is the containment contract, so an unattended turn can't run away: path-allowlist
(anything off RELAY_FILE + ALLOW_PATHS is reverted, exit 6), commit-bypass guard (if the CLI
commits mid-turn, the shim resets and re-commits file-scoped), and no push (turns commit locally
only — git push yourself when ready). .tick/ is gitignored and per-device, so this is single-clone
coordination, not cross-machine.
Never hand-edit a clone while a driven turn is in flight there (GH-141). rtl_before() snapshots
the dirty set once, at turn start. A second session's edit landing during the turn window produces a
porcelain entry with no match in that snapshot — byte-identical to the agent's own off-lane
self-escape — so rtl_check() reverts it in the real tree. This is not a bug that can be fixed by
detection: preserving newly-dirty non-allowlisted paths would disable the documented GH-22
self-escape backstop. Observed live twice (2026-07-05, 2026-07-18); the second incident silently
deleted an untracked doc and reverted a tracked one mid-session. Since 2026-07-18 the pre-revert
content is copied to .tick/orphan-backups/<utc>-<pid>/<path> first, so a wrongly-caught edit is
recoverable — but the revert still happens. Wait for the turn, or work in a separate worktree.
Worktree isolation is ON by default for driven runs. relay-drive.sh exports
RELAY_WORKTREE_ISOLATION=1, so each turn-taker runs in a throwaway git worktree of ROOT@HEAD —
an off-task model's stray creations/renames (not just tracked edits) can't reach the real tree,
closing the gap where the allowlist only reverted named tracked files. Opt out per run with
RELAY_WORKTREE_ISOLATION=0; direct/attended shim use keeps the leaf default OFF. Also: --agent-cmd
runs a bare executable path directly, so an absolute path with spaces (a clone under
…/GH Repos/…) is safe — no quoting needed.
Verify the harness is green before a real run
Run mutation-heavy suites only in a separate disposable full clone, never in the maintained
$HARNESS or a linked worktree. Commit the inputs first: cloning does not copy uncommitted edits.
For a vendored install without its own Git checkout, select the maintained XYZ-forge full clone
containing that harness revision as VERIFY_SOURCE.
VERIFY_SOURCE="$HARNESS" # must be a full XYZ-forge clone with committed test inputs
[ -d "$VERIFY_SOURCE/.git" ] || { echo "select a full harness clone as VERIFY_SOURCE"; exit 1; }
VERIFY_CLONE="$(mktemp -d "${TMPDIR:-/tmp}/xyz-verify.XXXXXX")" || exit 1
[ -n "$VERIFY_CLONE" ] && [ -d "$VERIFY_CLONE" ] || exit 1
git clone --no-local "$VERIFY_SOURCE" "$VERIFY_CLONE" || exit 1
cd "$VERIFY_CLONE" || exit 1
unset XYZ_HARNESS TICK_REPO_ROOT # test fixtures must not inherit the maintained clone
# Bracket identity as described in WORKTREE-SAFETY.md (HEAD, origin, bare flag, local identity).
bash ./validate.sh # when the task's classified gate requires it
bash ./test/codex-turn.sh # focused check before a Codex run
bash ./test/agy-turn.sh # focused check before an agy run
# Compare the identity bracket before trusting results; drift invalidates them.
See WORKTREE-SAFETY.md for the existing identity and recovery procedure. Run only the checks applicable to the task.
If your turn's --artifact/ALLOW_PATHS includes anything under relay-automation/, re-run
bash "$HARNESS/skills/1-hourly/relay-automation/make-pkg.sh" after the turn lands, before trusting a green
validate.sh. relay-pkg-freshness.sh catches a stale vendored relay-pkg.tar.gz, but it reads
as one more red line in a 100+-test suite rather than a real, fix-needed gap — this has bitten two
separate passes on relay-automation/agy-turn.sh/consult.sh (GH-178's original B1/A4 pass on
2026-07-08, and the GH-183/187 fix on 2026-07-10). Don't wait to discover it after the fact.
Run the shim test matching the worker you'll drive (both are first-class). Run these un-sandboxed —
mktemp/network under the Bash sandbox can fail them for reasons unrelated to the code.
Relationship to the other skills
/relay— portable, dependency-free. Owns the thread template, turn block formats, evidence contract, and guardrails. Use it to scaffold the thread; relay-xyz never redefines the protocol, only runs it./xyz— concurrent, non-overlapping path-scoped lanes (parallel builds), alsotick-backed. A relay is sequential review (one writer, turn-based);xyzis parallel construction. Different shapes.consult.sh—relay-automation/consult.shasks one question of Codex and agy in parallel, captures both transcripts, leaves synthesis to you. Advisory-only, read-only, not a relay turn — reach for it when you want a second opinion without a review loop.
Skipped independent review
Use a short, neutral note in PRs, issues, receipts and handoffs: Independent review did not run. Add one brief factual reason only when needed for the next action or required by the platform. Missing review alone is not a code finding: do not assign defect severity, infer operator/setup failure, or create/escalate a blocking issue from it. Downstream agents must not upgrade this note into a defect. Keep approval status truthful; report actual code findings and explicit repository gates separately.
Framing
Open with a human sentence ("Driving a headless Codex or agy review of <artifact> — round cap 4…") and
close with the result + exit code. The structured thread lives in the relay file; the operator gets a
sentence and a verdict, not a wall of transcript.
Review Scope & Commensurate Complexity Standard
Reviewers (Codex, agy, Claude, etc.) and authors must adhere to a strict standard of commensurate complexity:
- Surgical, DRY, Safe, Secure, and Stable within Reason: Reviews must evaluate correctness, safety, and invariants against the stated requirements without encouraging speculative over-engineering.
- Commensurate Machinery & Tests: Defensive handling, recovery mechanisms, and test footprints must remain strictly commensurate with the scale and role of the core code. An 80-line sync script or local tool must not become entangled with multi-layered enterprise fail-safes, distributed locks, journaled recovery, or bespoke fuzzing/scanning runtimes unless the operator explicitly specifies it.
- Operational Envelope Grounding: Reviewers must grade against the explicitly declared operational envelope (e.g. local developer CLI, single-repo task) rather than unrequested enterprise multi-tenant threat models.
- Challenge Unwarranted Complexity: Reviewers should actively challenge speculative abstractions, parallel subsystems, and overbuilding, acting as a filter against bloat rather than an engine of scope creep.
- Measure, read-only (GH-681): a headless Reviewer MAY run narrow, non-mutating probes and queries against the seeded artifact to measure a claim — output and temp files under
.relay-scratch/or$TMPDIRonly, withexport PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.relay-scratch/tmp"first, and the command, exit status and decisive output quoted in the finding (scratch is discarded after the turn). It may NOT runvalidate.sh,test/*.sh, pytest, or executable fixtures in the relay worktree — those belong in a disposable full clone (AGENTS.md); a claim only measurable that way is graded[Unverified — needs clone run]. Containment is unchanged. The canonical wording is the reviewer note inrelay-automation/relay-turn-lib.sh(rtl_turn_prompt); the marathon reviewer brief (utils/py/marathon_drive.py, step 4b) already allows$TMPDIRprobes. - Generalizations carry a falsifier (GH-681): a finding that asks for a behaviour change is a generalization unless the Reviewer can paste the concrete input — a row, a value, a
file:line— that fails under the current code. Every[Blocker]or[Should]requesting a behaviour change MUST carryObserved input:,Affected scope:andFalsifier:lines; a[Blocker]must cite an observed failure. The Producer may disposition a request lacking these asDeclined — unproven generalization. Protocol rule, not a mechanical check; canonical wording inrelay-automation/new-relay.sh(Reviewer bullet), mirrored inskills/1-hourly/relay/SKILL.mdand the marathon brief (step 4c). Origin: the gh673 final QA relay, where a Round-1[Blocker]generalized one late-error observation into a rule that blanks every issue on real data, and the same seat[Pass]ed it next round. - No new tests where the repo forbids them (XYZ-forge, GH-831): there, a new
test/suite, a newvalidate.shTESTS entry or new gate machinery in the diff is itself a finding, and a reviewer does not ask for one. Verification uses the existing suite that covers the change, or a manual check recorded underTESTS-RESULTS/with itsprovenance.jsonl. Other repos keep their own test policy.
QA / Consult Template Formatting
Since agents often scaffold relay threads manually (when the /relay slash command isn't used or available), it is critical to structure QA / consultation threads correctly. Do not write open-ended instructions like "QA this codebase against the requirements."
Headless agents (Codex, agy, Aider) perform best when given explicit questions and a defined operational envelope. A proper QA thread must include:
- The goal, operational envelope, and files to read.
- Explicit statement of commensurate complexity and non-goals.
- A numbered list of concrete, specific questions to answer.
- Instructions on what the agent should output (e.g. file:line citations).
Example format:
---
Goal: QA Phase 3 Implementation (Semantic Layer)
Date: 2026-08-25
NEXT: Reviewer
STATUS: Open
---
# Context
Adjudicate the implementation of Phase 3 Semantic Layer against its plan in PROJECT/2-WORKING/GH-1-firebase-ai-reports-plan.md.
Operational Envelope: Local CLI tool. Tests and machinery must be commensurate with scope; do not demand unrequested enterprise multi-tenant fail-safes.
Read the plan doc in full, plus the code it references:
- api/src/indexer.ts
- api/src/semantic.ts
Questions:
1. Are the requirements for soft-failing met? Does it gracefully continue if the vector index is missing or empty?
2. Are limits capped properly? The plan says "findNearest capped at top-K <= 10". Is this enforced securely?
3. Is the Indexer idempotent? Review `processItem` in `indexer.ts` which uses a hash check. Does this prevent unnecessary re-embedding?
4. Is the implementation surgical and DRY? Flag any unnecessary layers of abstraction or unneeded machinery.
Flag anything wrong, missing, incorrectly scoped, or over/under-engineered. Be concrete and cite file:line where you disagree with a specific claim.
Write your verdict below and change the STATUS to Approved/Closed if it passes.
<!-- ▽ RELAY AUTOMATION: DO NOT MODIFY THIS BLOCK ▽ -->
▶ TAKE YOUR TURN (codex)
<!-- △ RELAY AUTOMATION: DO NOT MODIFY THIS BLOCK △ -->