paired-session · diff
git:20261004.34fe8eb to git:20261004.d06fe04
91 added, 186 removed. Audit A to A.
---
name: paired-session
description: Use in exactly three cases - the user explicitly asks for paired-session, the review-loop entry hands off because .review-loop/config.md sets `entry: paired-session`, or the review-loop entry hands off because that key is absent (the default entry); this is the plan, implementation, independent-review, finish, polish, docs, security and acceptance workflow. Generic review-loop requests in every other case (key invalid, `legacy`, or explicit legacy) use the legacy entry.
---
- # Paired-session workflow
+ # Paired-session workflow (Codex)
- Use the paired-session coordinator shipped with this plugin. Do not load or
- invoke the legacy review-loop workflow for this task.
+ First, before any stage A check or question, read `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:
- How this skill was entered decides failure handling (stage A = everything before
- the first command that runs `bin/paired-session`):
- - Default entry (review-loop handoff with the `entry` key absent): a failed
- stage A check is reported back as `stage A failure: <reason>`; the review-loop
- entry then prints its fallback notice and runs legacy.
- - Explicit entry (`entry: paired-session` handoff, or the user asked for
- paired-session): a failed stage A check refuses with the reason, except that a
- declined or ungranted outside-sandbox approval is reported as HOLD. On an
- `entry: paired-session` handoff, print other refusals as
- `review-loop: paired-session entry refused (<reason>); set "entry: legacy" or ask for "the legacy review-loop workflow"`.
- - From the first `bin/paired-session` command on, every refusal or HOLD is
- reported verbatim and never falls back to legacy.
+ ```bash
+ python3 <support-root>/scripts/read_protocol.py --runtime codex --stage entry-paired-session --output .review-loop/tmp/protocol-codex-entry-paired-session.md
+ ```
- 1. Stage A checks. When the user invoked this skill directly, first check that
- every CLI the resolved roles need is on PATH (`command -v`; the default roles
- need `codex` and `claude`); the review-loop entry has already checked this on
- a handoff. Identify the intended Git worktree. Use a dedicated task worktree;
- preserve unrelated user changes and do not switch away from a dirty checkout.
- If a dedicated worktree is unavailable, ask before creating one. Determine
- the project's test command from its docs/manifests and ask only if it cannot
- be established safely. When a Claude role (reviewer or gate) runs it, the
- command must be one exact command without `$(`, backticks, `|`, `;`, `&&`,
- redirection or loops; otherwise ask for a `/bin/bash /absolute/path/script.sh`
- form. A declined or unanswered question is a failed stage A check. Under
- `--handsfree` or `handsfree: true` nobody answers, so any such question is a
- failed stage A check, reported as
- `stage A failure: handsfree cannot answer (<question>)`.
- 2. Profile and settings. Use the operator profile the user names; otherwise
- use `~/.config/review-loop/paired-session.json` when it exists. Pass its
- absolute path with `--config` on every call. `--config` replaces
- `<workspace>/.review-loop/paired-session.json`, which may set non-program
- limits only; copy desired non-program limits into the external profile.
- Role/vendor/program/test-command settings belong in the operator profile,
- outside the workspace and run directory. CLI options can override that
- profile. Models are operator-set (ADR-9); a role without one gets its
- vendor's default (`claude-opus-5-5` for Claude; `gpt-6.1-sol` for Codex), and
- the gate defaults to the author's vendor (ADR-10).
- Legacy keys in `.review-loop/config.md` that are set map to one-run options:
- `docs_file` → `--docs-file`, `skip_quality_polish` → `--skip-quality-polish
- true|false`, `soft_limit_plan` / `soft_limit_exec` → `--max-plan-rounds` /
- `--max-exec-rounds`. Do not apply, but print a warning for, `auto_commit: true`
- (`review-loop: auto_commit in .review-loop/config.md is not applied by
- paired-session; set it in the operator profile`) and a `reviewer_model` /
- `executor_model` set to anything other than empty or `inherit` (models come
- from the operator profile, ADR-9). Never pass
- `--adversarial-gate off`.
- 3. For a new run, pass `--lifecycle-mode on` to `run` and, in strict mode, to
- `permission-probe`, identically; the CLI value overrides any profile value. Never pass
- `--skip-probe`, `--accept-unverified-codex-cli`,
- `--accept-unverified-claude-author`, `--accept-probe-skip` or
- `--override-rejection` on your own initiative.
- `--strict` comes only from the user or the operator profile (`safety_mode`),
- and goes to `permission-probe` and `run` alike:
- both modes keep every sandbox; the default `efficient` mode does not require
- the probe PASS and its evidence guard only logs, while `--strict` restores
- both (`paired_session/docs/efficient-mode.md`). A strict lifecycle run also
- refuses `--accept-unverified-claude-author` and `--accept-probe-skip` (D-7);
- an efficient run needs no waiver. The run is strict when the user asked for
- `--strict` or the operator profile sets `"safety_mode": "strict"`; read the
- profile before choosing the flow.
- 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.
- The coordinator spawns provider CLIs and writes to `$CODEX_HOME` outside the
- product workspace; execute each invocation below with the host's full
- filesystem/network permission, outside the current Codex sandbox, and never
- inside the sandbox. If that permission is declined or cannot be granted for
- the first invocation, that is a failed stage A check; for any later
- invocation, report HOLD with the reason (no fallback). 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 below.
+ 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:
- First invocation (setup, then `run`; strict: `permission-probe`):
+ - Legacy pointer: `ask for "the legacy review-loop workflow"`. Long-command
+ execution failure: a declined or ungranted outside-sandbox approval (a failed
+ stage A check for the first invocation; on the explicit entry, and for any
+ later invocation, HOLD with the reason, no fallback).
+ - Outside-sandbox execution: the coordinator spawns provider CLIs and writes to
+ `$CODEX_HOME` outside the product workspace; 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.
+ - Run directory: `$CODEX_HOME/state/paired-session/runs/<task id>`, created by
+ the first invocation, which writes the work item there. Resolve `PLUGIN_ROOT`
+ again inside each invocation.
+ - 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.
- ```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())')"
- RUN_DIR="$CODEX_HOME/state/paired-session/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
- ```
+ First invocation (setup, then `run`; strict: `permission-probe`):
- 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
- above. Any other non-zero exit from the coordinator is a refusal or HOLD:
- report its output verbatim (no fallback). Once `RUN_DIR/state.json` exists, read its frozen `config` and
- print one start line from it: author, reviewer and gate vendor and model;
- plan and exec rounds, invocations and timeout; docs file and
- skip-quality-polish; and which values came from `.review-loop/config.md`.
- Default (efficient): if `config.lifecycle_mode` is not `on`, apply the
- backstop below. Strict: exit 0 from the probe means PASS or
- PASS_RESIDUAL_RISK; any other result is a HOLD: report it and stop. On
- exit 0, tell the user if `RUN_DIR/permission-probe.json` says
- PASS_RESIDUAL_RISK; if `config.lifecycle_mode` is not `on`, run `abort` with
- the run's saved options and report a plugin version mismatch instead of
- starting the run.
+ ```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())')"
+ RUN_DIR="$CODEX_HOME/state/paired-session/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
+ ```
- Second invocation (strict only: run), with the `RUN_DIR` printed by the first and the same
- resolved values:
+ 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).
- ```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
- ```
+ Second invocation (strict only: run), with the `RUN_DIR` printed by the first and the same
+ resolved values:
- Replace the placeholders before running. In the work item include only
- user-approved requirements; mark uncertainties as questions instead of
- inventing acceptance criteria. Add the same `--config` and mapped one-run
- options to every call; add `--stop-after-plan` only to `run` when requested.
- Keep the heredoc delimiter unique and quoted; never interpolate user text as
- shell code. 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. While the run is active, do not call plain `status`: it
- needs the run lease and, while the run holds it, prints
- `HOLD: another coordinator currently owns this run` although the run is not
- on HOLD. Read `RUN_DIR/state.json` directly or use `status --brief`. As a
- backstop, if the running state shows `config.lifecycle_mode` other than `on`,
- end the polled command session, wait until the child in `state.active` has
- exited, run `abort` with the run's saved options, and report a plugin version
- mismatch.
- 4. Later commands on an existing run (`resume`, `permission-probe
- --retry-uncertain`, `abort`, `reject`, `accept`, `note`,
- `attach-verification`) pass the saved `state.json` `config.lifecycle_mode`
- value and the run's original workspace, work item, run directory, profile
- and options, never the current default; resolve `PLUGIN_ROOT` again inside
- each invocation. Report DONE/HOLD and the run directory. On HOLD, inspect its
- state, findings, and receipts before resuming. In both modes a reviewer, gate
- or shadow turn that changes the workspace is void: the coordinator restores
- the workspace and re-dispatches it once, and a second change or a failed
- restore is a HOLD; an author turn that changes HEAD or the branch (a commit,
- reset or checkout) is a HOLD. If `uncertain_active` is
- present, do not rerun the probe or resume automatically: check its pid and
- receipts; if the child is still alive, wait for it to stop. If its phase is
- `PROBE` or `AUTHOR_PERMISSION_PROBE`, ask before rerunning the disposable
- probe with `permission-probe --retry-uncertain`. For a product-work turn, ask
- before `resume --retry-uncertain` because this may replay a model turn. After
- recovering an interrupted probe, use `resume` on the existing run directory;
- do not run a new work item. In strict mode, re-run the permission probe
- first if it is missing or no longer matches.
- 5. DONE. With the saved `config.lifecycle_mode` `on`, DONE means the security
- stage passed and acceptance is pending; with `off` (a run started before
- v2.10.0), DONE has no finish, quality-polish, docs or security stages, so say
- so. Report the stage receipts, open findings, operator verification records
- still valid for the tree, and whether the operator profile's `auto_commit`
- will make one local commit on acceptance; offer `accept` or `reject`. Never
- accept or reject under handsfree, and never on your own judgment:
- - Accept only after the user explicitly accepts in this conversation. Run
- `accept --intent-only` with the user's reason as `--reason TEXT` (or no
- `--reason` if they give none), show the digest, then run
- `accept --expect <digest>` with the same `--reason`.
- - Reject only on the user's explicit rejection with their note: run
- `reject --intent-only --text NOTE`, show the digest, then run
- `reject --expect <digest> --text NOTE` as a polled invocation (it reopens
- EXEC and dispatches model turns) and inspect the final status.
- - Use `--override-rejection` only when the user asks for it with a reason.
- Do not imply user acceptance or delivery authorization from DONE.
+ ```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
+ ```
- The coordinator owns reviewer dispatch and limits. This skill is the explicit
+ 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.
+
+ 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; with `legacy`
or an invalid value, routing stays legacy.