Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ predate the plugin rewrite and are grouped by date.

## [Unreleased]

## [2.11.8] — 2026-08-05

### Fixed

- Every command/skill surface that spelled the interpreter fallback as
`python3 X || python X` now resolves the interpreter once, by presence,
before invoking a helper (#577). The `||` fold branches on the helper's
EXIT CODE, so any helper answer that carries information as a nonzero code
(e.g. `taskwrite.py archive`'s "archive could not be read") got silently
re-run under `python` and had its verdict replaced by the second run's.
`plugins/*/hooks/hooks.json` is deliberately unchanged — its pass/fail
hooks carry no exit-code vocabulary to lose.

## [2.11.7] — 2026-08-05

### Changed
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ project context. You decide. codeArbiter enforces.
<img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757">
<img alt="Codex plugin" src="https://img.shields.io/badge/OpenAI_Codex-plugin-10a37f">
<img alt="Pi Feature Forge preview" src="https://img.shields.io/badge/ca--pi-Feature_Forge_preview-d97757">
<img alt="version 2.11.7" src="https://img.shields.io/badge/version-2.11.7-2b7489">
<img alt="version 2.11.8" src="https://img.shields.io/badge/version-2.11.8-2b7489">
<img alt="commands" src="https://img.shields.io/badge/commands-40-555">
<img alt="skills" src="https://img.shields.io/badge/skills-23-555">
<img alt="agents" src="https://img.shields.io/badge/agents-28-555">
Expand Down Expand Up @@ -119,7 +119,7 @@ Approve the normal plugin trust prompt, open the target repository, and continue

### Codex CLI

The public GitHub-slug flow is **available now**. The repository currently ships `ca-codex 0.4.6`;
The public GitHub-slug flow is **available now**. The repository currently ships `ca-codex 0.4.7`;
the dated end-to-end public-install record discovered `ca-codex 0.2.4` from release `v2.8.13`.
Current packaging and shared-core parity are continuously verified, while that dated live-install
record stays labeled rather than being silently promoted to evidence for a newer adapter:
Expand Down
6 changes: 3 additions & 3 deletions core/pysrc/babysit.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
# underscore library itself as a script. Mirrors doctor.py / taskwrite.py:
# entry point stays thin, all resolution logic lives in _babysitlib.
#
# Invoked by command prose as:
# python3 "<plugin>/hooks/babysit.py" --root "<dir>"
# || python "<plugin>/hooks/babysit.py" --root "<dir>"
# Invoked by command prose as (interpreter resolved once by presence, never a
# `python3 X || python X` fold -- #577):
# "$PY" "<plugin>/hooks/babysit.py" --root "<dir>"
#
# Prints one JSON line, e.g. {"enabled": true, "on_red": "propose"}. Fail-safe:
# _babysitlib.main() itself never raises past its own try/except (any resolver
Expand Down
6 changes: 3 additions & 3 deletions core/pysrc/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
# multi-statement block in command prose. Mirrors doctor.py / taskwrite.py:
# entry point stays thin, all computation lives in _metricslib.
#
# Invoked by commands/metrics.md as:
# python3 "<plugin>/hooks/metrics.py" --root "<dir>" [--window N]
# || python "<plugin>/hooks/metrics.py" --root "<dir>" [--window N]
# Invoked by commands/metrics.md as (interpreter resolved once by presence,
# never a `python3 X || python X` fold -- #577):
# "$PY" "<plugin>/hooks/metrics.py" --root "<dir>" [--window N]
#
# Read-only: _metricslib.compute() never writes; this wrapper only prints its
# return value. json.dumps is called with its default ensure_ascii=True so the
Expand Down
7 changes: 3 additions & 4 deletions core/pysrc/migration-pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@
# any edit changes the digest -> the backstop re-blocks (closing TOCTOU and
# enforcing immutability at commit time).
#
# Invoked by skill prose as:
# python3 "<plugin>/hooks/migration-pass.py" || python "<plugin>/hooks/migration-pass.py"
# (same interpreter-fallback shape as hooks.json; rerun-safe — recording is a
# deterministic overwrite.)
# Invoked by skill prose as (interpreter resolved once by presence, never a
# `python3 X || python X` fold -- #577):
# "$PY" "<plugin>/hooks/migration-pass.py"

import os
import subprocess
Expand Down
7 changes: 4 additions & 3 deletions core/pysrc/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
# doctor.py / taskwrite.py: entry point stays thin, all logic lives in
# _previewlib.
#
# Invoked by commands/preview.md as:
# python3 "<plugin>/hooks/preview.py" diff || python "<plugin>/hooks/preview.py" diff
# python3 "<plugin>/hooks/preview.py" secrets || python "<plugin>/hooks/preview.py" secrets
# Invoked by commands/preview.md as (interpreter resolved once by presence,
# never a `python3 X || python X` fold -- #577):
# "$PY" "<plugin>/hooks/preview.py" diff
# "$PY" "<plugin>/hooks/preview.py" secrets
#
# Read-only, mirrors _previewlib's own read-only contract (git rev-parse/diff/
# ls-files and read-only file opens only): this wrapper writes nothing.
Expand Down
7 changes: 3 additions & 4 deletions core/pysrc/security-pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
# admits a commit only when every sensitive line being committed is in the
# recorded set.
#
# Invoked by skill prose as:
# python3 "<plugin>/hooks/security-pass.py" || python "<plugin>/hooks/security-pass.py"
# (same interpreter-fallback shape as hooks.json; rerun-safe — recording is
# idempotent).
# Invoked by skill prose as (interpreter resolved once by presence, never a
# `python3 X || python X` fold -- #577; recording itself stays idempotent):
# "$PY" "<plugin>/hooks/security-pass.py"

import os
import subprocess
Expand Down
5 changes: 3 additions & 2 deletions core/pysrc/taskwrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
# _taskboardlib; this is the thin I/O wrapper (read board -> transform -> write),
# the same shape as doctor.py / security-pass.py.
#
# Invoked by skill/command prose as:
# python3 "<plugin>/hooks/taskwrite.py" <verb> ... || python "<plugin>/hooks/taskwrite.py" ...
# Invoked by skill/command prose as (interpreter resolved once by presence,
# never a `python3 X || python X` fold -- #577):
# "$PY" "<plugin>/hooks/taskwrite.py" <verb> ...
#
# Verbs:
# add "<desc>" [--from ORIGIN] [--id GROUP.TYPE] [--boundaries a,b]
Expand Down
4 changes: 2 additions & 2 deletions core/surface/commands/doctor.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ remediation.
## Flow

{{IF:claude}}
1. Run `python3 "{{PLUGIN_ROOT}}/hooks/doctor.py" || python "{{PLUGIN_ROOT}}/hooks/doctor.py"`
and present its report verbatim.
1. Resolve the interpreter once by presence — `PY=python3; { command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; } || PY=python` — never `python3 X || python X`, which reruns X on any nonzero exit and reports the second run's code instead of the first's (#577). Run
`"$PY" "{{PLUGIN_ROOT}}/hooks/doctor.py"` and present its report verbatim.
2. In an arbiter-enabled repo, attempt `git add --all --dry-run` via Bash. `[H-03]` means hooks are
firing; execution means **CRITICAL: gates dormant**.
{{END}}
Expand Down
11 changes: 7 additions & 4 deletions core/surface/commands/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ glance; reach for `{{CMD:audit}}` when you need the full evidentiary packet.
## Flow

1. **Invoke the helper.** Call the thin entry hook `metrics.py`, which wraps
`compute` from `_metricslib.py`, via a Windows-safe `python3 … || python …`
fallback. Pass `{{PROJECT_DIR}}` as `--root`. If `--window N` was
`compute` from `_metricslib.py`. Resolve the interpreter once by presence —
`PY=python3; { command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; } || PY=python`
— never `python3 … || python …`, which reruns the helper on any nonzero exit
and reports the second run's code instead of the first's (#577). Pass
`{{PROJECT_DIR}}` as `--root`. If `--window N` was
supplied, pass it through as `--window N`; otherwise omit it (the helper
applies the default of 20).

```
python3 "{{PLUGIN_ROOT}}/hooks/metrics.py" --root "{{PROJECT_DIR}}" || python "{{PLUGIN_ROOT}}/hooks/metrics.py" --root "{{PROJECT_DIR}}"
"$PY" "{{PLUGIN_ROOT}}/hooks/metrics.py" --root "{{PROJECT_DIR}}"
```

> **`ensure_ascii` note — do not remove this.** `metrics.py` calls `json.dumps`
Expand All @@ -35,7 +38,7 @@ glance; reach for `{{CMD:audit}}` when you need the full evidentiary packet.

With a custom window size:
```
python3 "{{PLUGIN_ROOT}}/hooks/metrics.py" --root "{{PROJECT_DIR}}" --window N || python "{{PLUGIN_ROOT}}/hooks/metrics.py" --root "{{PROJECT_DIR}}" --window N
"$PY" "{{PLUGIN_ROOT}}/hooks/metrics.py" --root "{{PROJECT_DIR}}" --window N
```
Replace `N` with the integer the user supplied.

Expand Down
6 changes: 4 additions & 2 deletions core/surface/commands/override.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ Heavier path (all required, in order):
```
[ISO-8601] | BY: <email> | SECURITY-OVERRIDE | FINDING: <specific finding> | REASON: <reason>
```
4. **Only then** record the bypass. For the crypto/secret commit gate, that means running
`python3 "{{PLUGIN_ROOT}}/hooks/security-pass.py" || python "{{PLUGIN_ROOT}}/hooks/security-pass.py"`,
4. **Only then** record the bypass. For the crypto/secret commit gate, that means resolving the
interpreter once by presence — `PY=python3; { command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; } || PY=python`
— never `python3 X || python X`, which reruns X on any nonzero exit (#577), and running
`"$PY" "{{PLUGIN_ROOT}}/hooks/security-pass.py"`,
which writes `{{PROJECT_DIR}}/.codearbiter/.markers/security-gate-passed` bound to the
sensitive lines it approves, so hook H-09b/H-10b allows the commit — recorded **only** after
steps 1–3, never to skip the gate proper.
Expand Down
6 changes: 4 additions & 2 deletions core/surface/commands/pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ apply, then:
`medium-documents` leaf, and apply at least the §3.A em-dash ban and the §3.B copy self-audit to the
prose. Then `gh pr create`; return the URL.
6. **Auto-attach the babysitter** — resolve the flag with the canonical resolver, never by eyeballing
the env var (so the accepted `on|true|1` spellings and the dormancy gate can't drift):
the env var (so the accepted `on|true|1` spellings and the dormancy gate can't drift). Resolve the
interpreter once by presence — `PY=python3; { command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; } || PY=python`
— never `python3 X || python X`, which reruns X on any nonzero exit (#577):
```
python3 "{{PLUGIN_ROOT}}/hooks/babysit.py" --root "{{PROJECT_DIR}}" || python "{{PLUGIN_ROOT}}/hooks/babysit.py" --root "{{PROJECT_DIR}}"
"$PY" "{{PLUGIN_ROOT}}/hooks/babysit.py" --root "{{PROJECT_DIR}}"
```
It prints one JSON line, e.g. `{"enabled": true, "on_red": "propose"}`. Only when `enabled` is
true (the global flag `CODEARBITER_BABYSIT` is on — default off, mirrors `CODEARBITER_PRUNE` — and
Expand Down
8 changes: 5 additions & 3 deletions core/surface/commands/preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ not the index, not `.codearbiter/`. `git status` is unchanged by a run.
1. **Collect the diff.** Call the thin entry hook `preview.py` in `diff` mode, which wraps
`collect_diff` from `{{PLUGIN_ROOT}}/hooks/_previewlib.py`. It unions HEAD-vs-worktree
changes, staged changes, and untracked files (forward-slash paths). Run it from the project
root so `_previewlib`/`_hooklib` resolve on the same `sys.path`:
root so `_previewlib`/`_hooklib` resolve on the same `sys.path`. Resolve the interpreter once
by presence — `PY=python3; { command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; } || PY=python`
— never `python3 X || python X`, which reruns X on any nonzero exit (#577):
```
python3 "{{PLUGIN_ROOT}}/hooks/preview.py" diff || python "{{PLUGIN_ROOT}}/hooks/preview.py" diff
"$PY" "{{PLUGIN_ROOT}}/hooks/preview.py" diff
```
If the result is empty (clean tree, or not a git repo), print a friendly **"Nothing to
preview"** line and STOP. This is a clean exit, not an error: no stack trace, no failure.
Expand All @@ -37,7 +39,7 @@ not the index, not `.codearbiter/`. `git status` is unchanged by a run.
changed file's current content and returns `SecretFinding(path, line_no, snippet)` for every
credential line, with the secret VALUE already masked to `****` in `snippet`:
```
python3 "{{PLUGIN_ROOT}}/hooks/preview.py" secrets || python "{{PLUGIN_ROOT}}/hooks/preview.py" secrets
"$PY" "{{PLUGIN_ROOT}}/hooks/preview.py" secrets
```
Report each finding by `path:line_no` with its redacted snippet. The snippet arrives already
masked: never reconstruct or print a raw secret value.
Expand Down
8 changes: 5 additions & 3 deletions core/surface/commands/prune.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,18 @@ Pi session file.

## Flow

1. **status / dry / audit** — run the backing tool and present its output verbatim:
1. **status / dry / audit** — run the backing tool and present its output verbatim. Resolve the
interpreter once by presence — `PY=python3; { command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; } || PY=python`
— never `python3 X || python X`, which reruns X on any nonzero exit (#577):
```
python3 "{{PLUGIN_ROOT}}/hooks/prune-transcript.py" <subcommand> [<path>] || python "{{PLUGIN_ROOT}}/hooks/prune-transcript.py" <subcommand> [<path>]
"$PY" "{{PLUGIN_ROOT}}/hooks/prune-transcript.py" <subcommand> [<path>]
```
For serialized hosts, `dry` analyzes `<path>.copy.jsonl`; Pi active sessions use the native
semantic planner and return a custom compaction result without session-file writes.

2. **run** — confirm the path is a copy or an inactive session, then:
```
python3 "{{PLUGIN_ROOT}}/hooks/prune-transcript.py" <path> --execute [--tier T] || python "{{PLUGIN_ROOT}}/hooks/prune-transcript.py" <path> --execute [--tier T]
"$PY" "{{PLUGIN_ROOT}}/hooks/prune-transcript.py" <path> --execute [--tier T]
```
Present the per-strategy reduction report; follow with `audit` on the result.

Expand Down
8 changes: 5 additions & 3 deletions core/surface/commands/standup.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ that names every member, never an implied yes.
`git stash show`). Report-and-route only: never discard a stash, reset, or push.
5. **Advisory board-drift sweep** — run `git log` over the recent merge window
(since the last `ca`-scoped tag, or a rolling 30-day window when no tag exists)
and pipe that text to
`python3 "{{PLUGIN_ROOT}}/hooks/boardsync.py" reconcile || python "{{PLUGIN_ROOT}}/hooks/boardsync.py" reconcile`.
and pipe that text to `"$PY" "{{PLUGIN_ROOT}}/hooks/boardsync.py" reconcile`. Resolve `$PY`
once by presence — `PY=python3; { command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; } || PY=python`
— never `python3 X || python X`, which reruns X on any nonzero exit (#577); this resolution
covers step 6's helper call too.
Display the advisory drift report as-is: DRIFTED tasks (work merged but board
state not `[x]`) and informational UNKNOWN ids (in the log but absent from the
board). This step is read-only and best-effort — the dotted-id grep can miss a
Expand All @@ -65,7 +67,7 @@ that names every member, never an implied yes.
accumulate on the board and inflate the in-flight count until it stops meaning
anything. List the done items older than the cutoff, then ask about **each one
separately** and archive only the ones the user says yes to:
`python3 "{{PLUGIN_ROOT}}/hooks/taskwrite.py" archive <id> || python "{{PLUGIN_ROOT}}/hooks/taskwrite.py" archive <id>`.
`"$PY" "{{PLUGIN_ROOT}}/hooks/taskwrite.py" archive <id>`.

One confirmation per item, one helper call per item — the two map 1:1 on
purpose. A batched "archive all 12?" turns twelve decisions into one, and the
Expand Down
10 changes: 6 additions & 4 deletions core/surface/commands/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,22 @@ pure `_taskboardlib` transforms; this command runs the thin writer
## Verbs

Always put `--` before user text (a desc or title) so a value beginning with `-` is not
parsed as a flag. Interpreter fallback, same shape as the hooks: `python3 … || python …`.
parsed as a flag. Resolve the interpreter once by presence — `PY=python3; { command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; } || PY=python`
— never `python3 … || python …`, which reruns the helper on any nonzero exit and reports the
second run's code instead of the first's (#577) — then invoke `"$PY"` below.

- **add** — append a queued task. ID-less by default; pass `--id <group>.<type>` to mint
a dotted ID now, `--from <origin>` for a harvest back-ref, `--boundaries a,b` for the
security/trust boundaries it touches. The description must be nonblank and
single-line; origin and boundary values must also stay on one line.
- `python3 "{{PLUGIN_ROOT}}/hooks/taskwrite.py" add [--id group.type] [--from origin] [--boundaries a,b] -- "<desc>" || python "{{PLUGIN_ROOT}}/hooks/taskwrite.py" add ... -- "<desc>"`
- `"$PY" "{{PLUGIN_ROOT}}/hooks/taskwrite.py" add [--id group.type] [--from origin] [--boundaries a,b] -- "<desc>"`
- **start** — flip a task to in-progress and **stamp the started date** (so it can never
be a dateless `[~]`). On an ID-less item, pass `--as <group>.<type>` to mint its dotted
ID at pick-up. `--date YYYY-MM-DD` overrides today.
- `python3 "{{PLUGIN_ROOT}}/hooks/taskwrite.py" start [--as group.type] [--date YYYY-MM-DD] -- "<id|title>"`
- `"$PY" "{{PLUGIN_ROOT}}/hooks/taskwrite.py" start [--as group.type] [--date YYYY-MM-DD] -- "<id|title>"`
- **done** — flip an in-progress task to done and stamp the done date (`--date`
overrides today). A queued task must be `start`ed first.
- `python3 "{{PLUGIN_ROOT}}/hooks/taskwrite.py" done [--date YYYY-MM-DD] -- "<id|title>"`
- `"$PY" "{{PLUGIN_ROOT}}/hooks/taskwrite.py" done [--date YYYY-MM-DD] -- "<id|title>"`

A missing target, an already-matching state, an out-of-order transition, a malformed
add field or `--date`, or an invalid `GROUP.TYPE` namespace is reported and writes
Expand Down
6 changes: 4 additions & 2 deletions core/surface/commands/watch.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ a phantom watcher.
3. **On red** — retrieve the failing job's logs (`gh run view --log-failed` /
`gh pr checks`) and act at the configured depth. Resolve that depth with the
canonical resolver rather than reading the env var by hand (so the accepted
values can't drift):
values can't drift). Resolve the interpreter once by presence —
`PY=python3; { command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; } || PY=python`
— never `python3 X || python X`, which reruns X on any nonzero exit (#577):
```
python3 "{{PLUGIN_ROOT}}/hooks/babysit.py" --root "{{PROJECT_DIR}}" || python "{{PLUGIN_ROOT}}/hooks/babysit.py" --root "{{PROJECT_DIR}}"
"$PY" "{{PLUGIN_ROOT}}/hooks/babysit.py" --root "{{PROJECT_DIR}}"
```
It prints one JSON line; act at its `on_red` value (`CODEARBITER_BABYSIT_ONRED`,
default `propose`):
Expand Down
6 changes: 4 additions & 2 deletions core/surface/includes/security-gate-record.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ The shared mechanism by which `crypto-compliance` and `secret-handling` unblock
by both skills' "On pass" step; the only difference between them is which commit hook the marker
satisfies (H-09b for crypto/TLS, H-10b for secrets).

**On a genuine PASS only**, run:
**On a genuine PASS only**, resolve the interpreter once by presence — `PY=python3; { command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; } || PY=python`
— never `python3 X || python X`, which reruns X on any nonzero exit and reports the second run's
code instead of the first's (#577) — then run:

```bash
python3 "{{PLUGIN_ROOT}}/hooks/security-pass.py" || python "{{PLUGIN_ROOT}}/hooks/security-pass.py"
"$PY" "{{PLUGIN_ROOT}}/hooks/security-pass.py"
```

It writes `{{PROJECT_DIR}}/.codearbiter/.markers/security-gate-passed` containing a digest of
Expand Down
Loading
Loading