paired-session · diff
git:20261004.a8b7044 to git:20261004.8d2ac07
156 added, 53 removed. Audit A to A.
---
name: paired-session
- description: Use in exactly two cases - the user explicitly asks for paired-session, or the review-loop entry hands off because .review-loop/config.md sets `entry: paired-session`; this is the opt-in plan, implementation, independent-review, and delivery workflow. Generic review-loop requests in every other case (key absent, invalid, legacy, or explicit legacy) use the legacy entry.
+ 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
Use the paired-session coordinator shipped with this plugin. Do not load or
invoke the legacy review-loop workflow for this task.
- 1. 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.
- 2. Read `<workspace>/.review-loop/paired-session.json` if present. It may set
- non-program limits only. Put role/vendor/program/test-command settings in an
- operator-owned profile outside the workspace and run directory, and pass
- its absolute path with `--config` for probe and run. `--config` replaces the
- workspace profile; copy desired non-program limits into the external profile.
- CLI options can override
- that profile. Models are operator-set (ADR-9); a role without one gets its
+ 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.
+
+ 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).
- Determine the project's test command from its docs/manifests and ask only if
- it cannot be established safely.
- 3. Run the setup, work-item write, permission probe, and coordinator run in one
- Bash invocation so its shell variables remain available. Start that shell
- call with a short nonblocking yield (about 1 second), then poll its returned
- command-session id until the full call completes. Do not cancel it or launch
- a duplicate while it is active. The coordinator
- spawns provider CLIs and writes to `$CODEX_HOME` outside the product
- workspace; execute this command with the host's full filesystem/network
- permission, outside the current Codex sandbox. If that permission cannot be
- granted, stop and report HOLD instead of trying inside the sandbox.
+ 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 both `permission-probe` and `run`,
+ 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`).
+ 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.
+ First invocation (setup and probe):
+
```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"])')"
+ 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" || { echo 'paired-session executable missing from installed plugin'; exit 2; }
+ 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)"
- test "$(git -C "$WORKSPACE" rev-parse --show-toplevel)" = "$WORKSPACE" || { echo 'selected workspace is not a Git worktree root'; exit 2; }
+ 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"
- cat > "$WORKITEM" <<'PAIRED_SESSION_WORKITEM'
+ 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" permission-probe \
--workspace "$WORKSPACE" --workitem "$WORKITEM" --run-dir "$RUN_DIR" \
- --test-command "$TEST_COMMAND"
+ --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
+ above. Exit 0 from the probe means PASS or PASS_RESIDUAL_RISK; any other
+ result is a HOLD: report it and stop. On exit 0, read
+ `RUN_DIR/permission-probe.json` and tell the user if the status is
+ PASS_RESIDUAL_RISK, then read the frozen `config` in `RUN_DIR/state.json` 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`. 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.
+
+ Second invocation (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"
+ --test-command "$TEST_COMMAND" --lifecycle-mode on
```
- Replace the work-item placeholder and test command before running. Add
- `--config /absolute/path/to/profile.json` to both calls when using the
- external operator profile. Add
- user-requested CLI overrides identically to probe and run; add
- `--stop-after-plan` only to `run` when requested. Keep the heredoc delimiter
- unique and quoted; never interpolate user text as shell code. Never use
- `--skip-probe` for product work.
- 4. Report DONE/HOLD and the run directory. On HOLD, inspect its state, findings,
- and receipts before resuming. 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
+ 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 both calls; 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. 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. Otherwise,
- resolve `PLUGIN_ROOT` again inside the same shell invocation that runs
- `resume`; use the saved workspace, work item, run directory, profile, and
- options. Start the shell with a short nonblocking yield, then poll its
- command-session id until completion. Re-run the permission probe first if it
- is missing or no longer matches. Do not imply user acceptance or delivery
- authorization from DONE.
+ do not run a new work item. 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.
The coordinator owns reviewer dispatch and limits. This skill is the explicit
paired-session entry and the review-loop handoff target only when the config
- key `entry` is exactly `paired-session` and the work is fresh; otherwise default
- routing stays legacy.
+ key `entry` is `paired-session` or absent and the work is fresh; with `legacy`
+ or an invalid value, routing stays legacy.