git:20261030.1bea9e5 to git:20261030.67cd0ad

3 added, 5 removed. Audit A to A.

---
name: paired-session
description: Use in exactly four cases - the user explicitly asks for paired-session, the review-loop entry hands off because .review-loop/config.md sets `entry: paired-session`, the review-loop entry hands off because that key is absent or invalid (the default entry), or the review-pr skill hands off on its paired route; with lifecycle on, this is the plan, implementation, independent-review, finish, polish, docs, security and acceptance workflow. Generic review-loop requests go to the review-loop entry first, which routes them here; `entry: legacy` and the explicit request for the legacy review-loop workflow are refused there (the legacy workflow was removed in v2.13.0).
---
# Paired-session workflow (Codex)
First, before any stage A check or question, read `<support-root>/docs/protocol/loading.md`
and load the shared entry contract (`docs/protocol/paired-session-entry.md`) as
its own command, cwd in the user's workspace. The bundle must stay out of the
product worktree (FIELD-18): first print a fresh bundle directory as its own
command,
`python3 -c 'import os, tempfile, uuid; print(os.path.join(os.path.realpath(tempfile.gettempdir()), f"review-loop-protocol-{os.getuid()}", uuid.uuid4().hex[:12]))'`,
then write that printed directory out literally as `<bundle-dir>`:
```bash
python3 <support-root>/scripts/read_protocol.py --runtime codex --stage entry-paired-session --output <bundle-dir>/protocol-codex-entry-paired-session.md
```
On native Windows Codex runs commands in PowerShell, Python is `python` (or `py -3`) and `os.getuid` does not exist:
print the bundle directory with
`python -X utf8 -c "import getpass, os, re, tempfile, uuid; print(os.path.join(os.path.realpath(tempfile.gettempdir()), 'review-loop-protocol-' + (re.sub(r'[^A-Za-z0-9._-]', '_', os.environ.get('USERNAME') or getpass.getuser()) or 'user'), uuid.uuid4().hex[:12]).replace(os.sep, '/'))"`
(the loader names the directory after the Windows user name), then run
`python -X utf8 '<support-root>\scripts\read_protocol.py' --runtime codex --stage entry-paired-session --output '<bundle-dir>/protocol-codex-entry-paired-session.md'`.
The Windows commands in this skill are best effort; the Claude Code entry is the tested Windows path.
Resolve `<support-root>` to this plugin, not the task workspace. After exit 0,
read the complete output file in bounded chunks and follow it; a missing,
unreadable or incompletely read file is a failed stage A check, reported as
`stage A failure: cannot load the shared entry contract (<reason>)`. After
compaction, reload it the same way when no coordinator command is running;
while one is running, read `<support-root>/docs/protocol/paired-session-entry.md`
directly (the stage is that whole file), which writes nothing to the workspace.
This file adds only the Codex host rules:
- Legacy pointer: none (the legacy workflow was removed in v2.13.0). Long-command
execution failure: a declined or ungranted outside-sandbox approval: HOLD with
the reason, on every entry and invocation (nothing falls back).
- Outside-sandbox execution: the coordinator spawns provider CLIs and writes to
the selected `$CODEX_HOME`; execute each invocation below
with the host's full filesystem/network permission, outside the current
Codex sandbox, and never inside the sandbox.
- Long-command form: every invocation that can dispatch model turns
(`permission-probe`, `run`, `resume`, `reject --expect`) starts with a short
nonblocking yield (about 1 second); then poll its returned command-session id
until it completes. Do not cancel it or launch a duplicate while it is
active, except for the lifecycle-mode backstop, which ends the polled command
session.
- Detached commands: with `--detach`, wait until the printed pid has exited
(the printed record path then reads "exited" with the exit code), then read
the last line of its log and `status --brief`. A successful permission probe
stays ACTIVE; a report run ends in REPORTED. A probe's result is
`RUN_DIR/permission-probe.json`; end the detached command only with `stop`
(`paired_session/docs/detach.md`).
- Run directory: default to `<repo>/.review-loop/runs/<UUID>/`, where `<repo>` is the main
checkout resolved by `git rev-parse --path-format=absolute --git-common-dir` and its parent
when it ends in `/.git`. A lane's output therefore lives in the main checkout.
Set `WORKSPACE` in the same Bash call that runs `project_paths.py`. On native Windows
PowerShell, set `$Workspace` in the same call that runs `project_paths.py`.
`scripts/project_paths.py` resolves this root and creates `.review-loop/.gitignore`
(`*`, `!config.md`) only when missing. Not a Git repo, a bare repo or a submodule/separate-git-dir checkout: report fallback to
`${XDG_STATE_HOME:-$HOME/.local/state}/review-loop` (relative XDG uses `$HOME/.local/state`).
- An explicit operator-chosen run dir wins; existing runs resume at their original `--run-dir`, without migration.
+ An explicit operator-chosen run dir replaces the RUN_DIR assignment below; existing runs resume at their original `--run-dir`, without migration.
The plugin does not create `CODEX_HOME`. Write launcher logs under the root's `logs/`.
Persistent output is gitignored; scratch stays outside the repo. Do not run `git clean -x`
on the workspace while a run is active: it would delete the run.
Read a launcher log's last lines only when a polled command ends without its
final coordinator line (the shared contract's Work item section).
- Default (efficient): no permission probe; the first invocation below starts
the run. Strict: replace `run` in the first invocation with
`permission-probe` (same arguments, without `--stop-after-plan`), then start
the run with the second invocation.
First invocation (setup, then `run`; strict: `permission-probe`):
```sh
set -eu
export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
stage_a() { echo "stage A failure: $1"; exit 3; }
test -d "$CODEX_HOME" || stage_a "CODEX_HOME $CODEX_HOME is not an existing directory"
VERSION="$(codex plugin list --json --marketplace review-loop-marketplace | python3 -c 'import json,sys; d=json.load(sys.stdin); rows=[p for p in d.get("installed",[]) if p.get("pluginId")=="review-loop@review-loop-marketplace" and p.get("enabled")]; assert len(rows)==1, "review-loop plugin must be installed and enabled exactly once"; print(rows[0]["version"])')" || stage_a "review-loop plugin is not installed and enabled exactly once"
python3 -c 'import re,sys; v=sys.argv[1]; m=re.fullmatch(r"(\d+)\.(\d+)\.(\d+)(\.\d+)*", v); msg=None if m and tuple(map(int, m.groups()[:3])) >= (2, 10, 0) else ("installed review-loop %s predates the paired-session lifecycle (2.10.0)" % v if m else "cannot parse installed review-loop version %r" % v); msg and print("stage A failure: " + msg); sys.exit(3 if msg else 0)' "$VERSION" || exit 3
echo "VERSION=$VERSION"
PLUGIN_ROOT="$CODEX_HOME/plugins/cache/review-loop-marketplace/review-loop/$VERSION"
test -x "$PLUGIN_ROOT/bin/paired-session" || stage_a 'paired-session executable missing from installed plugin'
WORKSPACE='/absolute/path/to/the-selected-dedicated-worktree'
WORKSPACE="$(cd "$WORKSPACE" && pwd -P)" || stage_a 'selected workspace does not exist'
test "$(git -C "$WORKSPACE" rev-parse --show-toplevel)" = "$WORKSPACE" || stage_a 'selected workspace is not a Git worktree root'
TASK_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')"
- if [ -z "${RUN_DIR:-}" ]; then
- RUN_ROOT="$(python3 "$PLUGIN_ROOT/scripts/project_paths.py" "$WORKSPACE")" || stage_a 'cannot resolve the run root'
- RUN_DIR="$RUN_ROOT/runs/$TASK_ID"
- fi
+ RUN_ROOT="$(python3 "$PLUGIN_ROOT/scripts/project_paths.py" "$WORKSPACE")" || stage_a 'cannot resolve the run root'
+ RUN_DIR="$RUN_ROOT/runs/$TASK_ID"
WORKITEM="$RUN_DIR/WORKITEM.md"
mkdir -p "$RUN_DIR" || stage_a 'cannot create the run directory'
cat > "$WORKITEM" <<'PAIRED_SESSION_WORKITEM' || stage_a 'cannot write the work item'
[Write the user's approved goal, acceptance criteria, scope, and verification here.]
PAIRED_SESSION_WORKITEM
echo "RUN_DIR=$RUN_DIR"
TEST_COMMAND='[verified project command, or the exact value from the profile]'
"$PLUGIN_ROOT/bin/paired-session" run \
--workspace "$WORKSPACE" --workitem "$WORKITEM" --run-dir "$RUN_DIR" \
--test-command "$TEST_COMMAND" --lifecycle-mode on
```
Exit 3 (a line printed `stage A failure: …`) is a failed stage A check,
including the plugin-version floor (P14); report it per the entry rules, even
though `RUN_DIR/state.json` does not exist. Any
other non-zero exit from the coordinator is a refusal or HOLD: report its
output verbatim (no fallback).
Second invocation (strict only: run), with the `RUN_DIR` printed by the first and the same
resolved values:
```sh
set -eu
export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
VERSION="$(codex plugin list --json --marketplace review-loop-marketplace | python3 -c 'import json,sys; d=json.load(sys.stdin); rows=[p for p in d.get("installed",[]) if p.get("pluginId")=="review-loop@review-loop-marketplace" and p.get("enabled")]; assert len(rows)==1, "review-loop plugin must be installed and enabled exactly once"; print(rows[0]["version"])')" || { echo "review-loop plugin cannot be resolved after the probe; not starting the run"; exit 4; }
EXPECTED_VERSION='[the VERSION printed by the first invocation]'
test "$VERSION" = "$EXPECTED_VERSION" || { echo "plugin version changed ($EXPECTED_VERSION -> $VERSION) after the probe; not starting the run"; exit 4; }
PLUGIN_ROOT="$CODEX_HOME/plugins/cache/review-loop-marketplace/review-loop/$VERSION"
WORKSPACE='/the/resolved/worktree/from/the/first/invocation'
RUN_DIR='/the/RUN_DIR/printed/by/the/first/invocation'
WORKITEM="$RUN_DIR/WORKITEM.md"
TEST_COMMAND='[the same test command]'
"$PLUGIN_ROOT/bin/paired-session" run \
--workspace "$WORKSPACE" --workitem "$WORKITEM" --run-dir "$RUN_DIR" \
--test-command "$TEST_COMMAND" --lifecycle-mode on
```
Replace the placeholders before running. Keep the heredoc delimiter unique and
quoted. Exit 4 from the second invocation means the installed plugin changed
or became unresolvable after the probe: report it as a HOLD (no fallback); the
run was not started.
Native Windows (PowerShell; best effort, the Claude Code entry is the tested Windows path): efficient only, so
there is no second invocation; the coordinator refuses `--strict`, `permission-probe` and `--detach`. Git for
Windows (Git Bash) must be installed, and the run root and the workspace stay out of OneDrive-synced folders. The
first invocation becomes (same placeholders, same exit 3 rule; run the coordinator and every script through
`python`, never by shebang):
```powershell
$ErrorActionPreference = 'Stop'
function Stage-A($Reason) { Write-Output "stage A failure: $Reason"; exit 3 }
$CodexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $HOME '.codex' }
$env:CODEX_HOME = $CodexHome
if (-not (Test-Path -LiteralPath $CodexHome -PathType Container)) { Stage-A "CODEX_HOME $CodexHome is not an existing directory" }
try { $Rows = @((codex plugin list --json --marketplace review-loop-marketplace | ConvertFrom-Json).installed | Where-Object { $_.pluginId -eq 'review-loop@review-loop-marketplace' -and $_.enabled }) } catch { $Rows = @() }
if ($Rows.Count -ne 1) { Stage-A 'review-loop plugin is not installed and enabled exactly once' }
$Version = [string]$Rows[0].version
try { $Parsed = [version]$Version } catch { Stage-A "cannot parse installed review-loop version '$Version'" }
if ($Parsed -lt [version]'3.3.0') { Stage-A "installed review-loop $Version predates native Windows support (3.3.0)" }
Write-Output "VERSION=$Version"
$PluginRoot = Join-Path $CodexHome "plugins\cache\review-loop-marketplace\review-loop\$Version"
$Launcher = Join-Path $PluginRoot 'bin\paired-session'
if (-not (Test-Path -LiteralPath $Launcher -PathType Leaf)) { Stage-A 'paired-session launcher missing from installed plugin' }
$Workspace = 'C:\absolute\path\to\the-selected-dedicated-worktree'
try { $Workspace = (Resolve-Path -LiteralPath $Workspace).ProviderPath } catch { Stage-A 'selected workspace does not exist' }
$Top = git -C $Workspace rev-parse --show-toplevel
if ($LASTEXITCODE -ne 0 -or [IO.Path]::GetFullPath($Top) -ne [IO.Path]::GetFullPath($Workspace)) { Stage-A 'selected workspace is not a Git worktree root' }
if (-not $RunDir) {
$RunRoot = python -X utf8 (Join-Path $PluginRoot 'scripts\project_paths.py') $Workspace
if ($LASTEXITCODE -ne 0) { Stage-A 'cannot resolve the run root' }
$RunDir = Join-Path $RunRoot ('runs\' + [guid]::NewGuid())
}
$WorkItem = Join-Path $RunDir 'WORKITEM.md'
try { New-Item -ItemType Directory -Force -Path $RunDir | Out-Null } catch { Stage-A 'cannot create the run directory' }
$WorkItemText = @'
[Write the user's approved goal, acceptance criteria, scope, and verification here.]
'@
try { [IO.File]::WriteAllText($WorkItem, $WorkItemText.Replace("`r`n", "`n") + "`n", (New-Object Text.UTF8Encoding $false)) } catch { Stage-A 'cannot write the work item' }
Write-Output "RUN_DIR=$RunDir"
$TestCommand = '[verified project command, or the exact value from the profile]'
python -X utf8 $Launcher run --workspace $Workspace --workitem $WorkItem --run-dir $RunDir --test-command $TestCommand --lifecycle-mode on
exit $LASTEXITCODE
```
Add the review-only and review-pr options below to this block as to the POSIX one; a second quoted
here-string writes `pr-pins.json` the same way as the work item. Windows PowerShell 5.1 does not pass a `"` inside an
argument intact to `python`: keep `"` out of the test command and the option values, or use PowerShell 7.3 or later.
A review-only request (the user asks to review an existing change, or the review-loop handoff of
a code target) follows the shared contract's review-only entry: add `--review-only` (and
`--base "$BASE"` only when the user named a base) to both invocations.
The workspace is the worktree that holds the change (normally the current checkout). Do not
create or switch to a dedicated worktree, and skip the Stage A dedicated-worktree question.
Add `--auto-commit false` too when the review-loop entry handed it over.
A review-pr handoff (from the Codex `review-pr` skill, or the user asks paired-session to review a PR or
ref) follows the shared contract's Review-PR entry.
Resolve the run root from the operator project before switching `WORKSPACE` to the clone.
Host rules: the run root is the resolved `<repo>/.review-loop/` (or the reported state-root fallback) (the materializer's `--root`); the
materializer reads through `gh` and the network, so run it as its own invocation outside the sandbox,
with `PLUGIN_ROOT` resolved as in the first invocation, before the first invocation. In both
invocations set `WORKSPACE` to the clone it printed (no input: the current worktree). In the first
invocation replace the `RUN_ROOT` resolver (PowerShell: `$RunRoot`) with the absolute operator-project
root resolved before materialization, so runs stay beside `pr/` rather than inside the clone. Skip the
dedicated-worktree question, replace `--test-command "$TEST_COMMAND"` with `--no-test-command` unless
the user confirmed a test command, and add `--review-only --review-report --auto-commit false`,
`--base "$BASE"` (the printed `merge_base`; no input: only a base the user named),
`--review-pr-pins "$RUN_DIR/pr-pins.json"` for a materialized input (the first invocation writes the
printed JSON there with a second quoted heredoc, like the work item) and `--aspects "$ASPECTS"` when
aspects were given. `simplify` pointer: `run /review-loop:code-quality-loop on the change (its POLISH-Q simplifier)` (Claude Code).
This skill is the explicit
paired-session entry and the review-loop handoff target only when the config
key `entry` is `paired-session` or absent and the work is fresh, an existing plan (as the work item) or a review-only
code target (an invalid value counts as absent, with a warning); `entry: legacy` is refused by the entry, since the
legacy workflow was removed in v2.13.0.
Operator commands (`accept`, `reject`, `withdraw-findings`) follow the shared
entry contract. Run `withdraw-findings` only on the user's explicit decision
naming the findings and a nonempty reason, never on the host's own judgment or
in handsfree mode. Issue `--intent-only --finding ID --reason TEXT` first
(repeat `--finding` for each named id), show the digest, then submit the same
command with `--expect <digest>` instead of `--intent-only`. Relay the ruling,
evidence, next-step hint and status; withdrawal dispatches no model turn.
`--lifecycle-mode off` is the supported stop-after-gate route: PLAN -> EXEC -> GATE -> DONE (acceptance pending). The operator then handles FINISH, POLISH-Q, docs, security review and merging. Accept never commits on this route: it hands back the uncommitted tree. New runs default to `on` (the full lifecycle); CLI, operator/workspace profiles and Python entry points may select `off`, and saved off runs resume, accept, reject, note and abort normally.
For review-pr report mode, always pass `--lifecycle-mode on` to `run` and, in strict mode, to `permission-probe`. This overrides a profile's off value. For other entries, pass `--lifecycle-mode on` only when no operator/workspace profile sets `lifecycle_mode`. When a profile sets it, omit the flag (or pass the profile's value) in both commands. The CLI value overrides the profile. When the user chooses off for those entries, pass `--lifecycle-mode off` in both commands. Compare the saved lifecycle mode with that selected mode for the backstop; saved off runs continue with off.