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
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,10 @@ the same name overrides it. `config/env.example` documents each setting with its

- `jpdev setup DIR [--repo OWNER/NAME] [--branch NAME] [--base origin/main] [--manifest FILE]`:
clones with `gh repo clone` (or reuses `DIR`), makes the branch from the base, selects the
manifest's toolchains (node by nvm version, go by its version when the manifest names one, python and its virtual environment), runs the
setup steps under the caps and prints one summary block.
manifest's toolchains (node by nvm version, go by its version when the manifest names one,
and Python by `python3.X` on PATH or the newest matching pyenv version), creates the declared
Python virtual environment, runs the setup steps in it under the caps, and prints one summary
block. `jpdev check` creates a missing declared environment before it runs checks too.
- `jpdev check [--only NAME,...] [--skip NAME,...] [--all] [--manifest FILE]`: runs the checks in
order, each under its toolchain's cap, and stops at the first failure unless `--all`. The
block names each check, pass or fail, the lines its `summary` regex matched, and the first 40
Expand Down Expand Up @@ -135,8 +137,17 @@ the same name overrides it. `config/env.example` documents each setting with its
share caches below `BASE/.jpdev-shared`; `--small` enforces the manifest handover paths and line
bound after the run; `--resume` continues the named thread. Every detected thread id is written
to `.jpdev/output/thread`.
- `jpdev codex brief-template`: prints `templates/codex-brief.md`, a brief skeleton that tells
- `jpdev codex review --brief FILE --model M --effort E --dir CLONE [--pr N | --issue N]
[--repo OWNER/NAME] [--out FILE] [--sandbox read-only|workspace-write] [--network]`: checks the brief and
runs `codex exec` with the model, effort and sandbox explicit and stdin closed. It writes the
final message to `--out` (default `.jpdev/output/review.md`), refuses the result if HEAD or a
tracked file changed, and prints the reviewed SHA. The default `workspace-write` sandbox lets
review probes use temporary files. It has no network unless the caller passes `--network`, the
prompt and post-run gate forbid Git writes, and it never posts. Use `jpdev record post` to post
the saved report.
- `jpdev codex brief-template [--review]`: prints `templates/codex-brief.md`, a brief skeleton that tells
Codex to write `msg.txt` and `pr.md` and not to commit.
`--review` prints the reviewer skeleton with its read-only rules and findings-first report.
- `jpdev codex check-brief FILE`: exits 1 and names the neutral phrasing for each word of
[`config/codex-words.txt`](config/codex-words.txt) (and of the file `JPDEV_CODEX_WORDS` names)
that the brief holds. `codex run` runs the same check first and does not start the run on a
Expand All @@ -152,9 +163,13 @@ the same name overrides it. `config/env.example` documents each setting with its
| `python` | `ulimit -v` and `timeout` |
| `shell` | `timeout` only |

When the manifest declares a virtual environment, its `bin` is first on PATH for every step, whatever
the step's toolchain (the toolchain still decides the cap), so a node or Go step that runs a python
script uses the environment's interpreter and packages.
When the manifest declares a virtual environment, its path must be a non-root relative child of
the clone with no `..` segment, and setup and checks create it when it is missing.
They use `python3.X` on PATH first, then the newest `~/.pyenv/versions/3.X.*/bin/python3`, and
refuse before any step if the declared release is unavailable. Its `bin` is first on PATH for
every step, whatever the step's toolchain (the toolchain still decides the cap), so a node or Go
step that runs a Python script uses the environment's interpreter and packages. Summary rows for
Python steps, and for other steps that invoke `python3`, name the interpreter used.

## What is here

Expand All @@ -164,7 +179,8 @@ script uses the environment's interpreter and packages.
- `templates/dev.json`, `templates/dev.schema.json`: the manifest by example and its JSON Schema.
- `templates/check.sh` and `lib/handover_check.sh`: the public checks a repository vendors as
`scripts/dev/check.sh` and `scripts/dev/handover_check.sh`.
- `templates/codex-brief.md`: the brief skeleton `jpdev codex brief-template` prints.
- `templates/codex-brief.md`, `templates/codex-review-brief.md`: the build and review brief
skeletons `jpdev codex brief-template` prints.
- `templates/AGENTS.md`, `templates/CLAUDE.md`: the agent instructions (one text, generated from
the other by `scripts/sync-agent-docs.sh`).
- `templates/ci-check.yml`: the CI step list.
Expand Down
101 changes: 95 additions & 6 deletions bin/jpdev
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ usage: jpdev COMMAND [ARGS]
model, effort and sandbox explicit, stdin closed and the transcript in the
log; print the final message and the exit code; then commit the tracked
changes (msg.txt, signed off), push, and open the pull request (pr.md)
codex brief-template
codex review --brief FILE --model M --effort low|medium|high --dir CLONE
[--pr N | --issue N] [--repo OWNER/NAME] [--out FILE]
[--sandbox read-only|workspace-write] [--network]
run a review with stdin closed, write its final message, refuse tracked
changes after the run, and print the reviewed SHA; no network unless
--network, and never commit, push or post
codex brief-template [--review]
print the brief skeleton: what Codex writes (msg.txt, pr.md) and does not do
codex check-brief [--build] FILE
refuse a brief that holds a word of the filter list, and suggest the
Expand Down Expand Up @@ -396,13 +402,77 @@ cmd_codex() {
[ $# -eq 0 ] || shift
case $sub in
run) codex_run "$@" ;;
review) codex_review "$@" ;;
check-brief) codex_check_brief "$@" ;;
brief-template) codex_brief_template ;;
brief-template) codex_brief_template "$@" ;;
-h | --help | help) usage; exit 0 ;;
*) jpdev_die "codex: the subcommands are run, check-brief and brief-template" ;;
*) jpdev_die "codex: the subcommands are run, review, check-brief and brief-template" ;;
esac
}

# codex_review: run Codex from a review brief and retain only its final report. The prompt and the
# post-run git gate make the role read-only; workspace-write remains the default because shell
# probes need temporary files.
codex_review() {
local brief="" model=$JPDEV_CODEX_MODEL_DEFAULT effort="" dir="" sandbox=workspace-write
local out="" repo="" pr="" issue="" log="" network=0 network_state=disabled
while [ $# -gt 0 ]; do
case $1 in
--brief) need_value "$@"; brief=$2; shift 2 ;;
--model) need_value "$@"; model=$2; shift 2 ;;
--effort) need_value "$@"; effort=$2; shift 2 ;;
--dir) need_value "$@"; dir=$2; shift 2 ;;
--sandbox) need_value "$@"; sandbox=$2; shift 2 ;;
--out) need_value "$@"; out=$2; shift 2 ;;
--repo) need_value "$@"; repo=$2; shift 2 ;;
--pr) need_value "$@"; pr=$2; shift 2 ;;
--issue) need_value "$@"; issue=$2; shift 2 ;;
--network) network=1; network_state=enabled; shift ;;
-h | --help) usage; exit 0 ;;
*) jpdev_die "codex review: unknown argument $1" ;;
esac
done
[ -f "$brief" ] || jpdev_die "codex review: --brief must name a file"
[ -n "$model" ] || jpdev_die "codex review: --model is needed (the model is always passed; JPDEV_CODEX_MODEL_DEFAULT can name one)"
case $effort in low | medium | high) ;; *) jpdev_die "codex review: --effort is low, medium or high" ;; esac
case $sandbox in read-only | workspace-write) ;; *) jpdev_die "codex review: --sandbox is read-only or workspace-write" ;; esac
[ -n "$dir" ] || jpdev_die "codex review: --dir is needed"
[ -d "$dir" ] || jpdev_die "codex review: --dir is not a directory"
{ [ -z "$pr" ] || [ -z "$issue" ]; } || jpdev_die "codex review: give --pr N or --issue N, not both"
dir=$(cd "$dir" && git rev-parse --show-toplevel) || jpdev_die "codex review: --dir is not a git clone"
[ -f "$dir/AGENTS.md" ] || jpdev_die "codex review: $dir holds no AGENTS.md, which the prompt names as the rules"
command -v codex > /dev/null || jpdev_die "codex review: codex is not on PATH"
brief=$(cd "$(dirname "$brief")" && pwd)/$(basename "$brief")
local sha
sha=$(git -C "$dir" rev-parse HEAD) || jpdev_die "codex review: cannot read HEAD"
if [ -z "$out" ]; then out=$dir/.jpdev/output/review.md; fi
mkdir -p "$(dirname "$out")" "$JPDEV_SCRATCH/logs"
out=$(cd "$(dirname "$out")" && pwd)/$(basename "$out")
log=$JPDEV_SCRATCH/logs/codex-review-$(basename "$dir")-$(date +%Y%m%dT%H%M%S).log
jpdev_log "codex review: model $model, effort $effort, sandbox $sandbox, in $dir; transcript $log; network $network_state"
codex_check_brief "$brief"
local prompt="The rules are this repository's AGENTS.md; follow them. Act as its reviewer. Do not change tracked files or write git metadata."
[ "$network" -eq 1 ] || prompt+=$'\n'"Do not access the network."
if [ -n "$pr" ]; then prompt+=$'\n'"Review pull request #$pr${repo:+ of $repo} at head $sha."; fi
if [ -n "$issue" ]; then prompt+=$'\n'"Review issue #$issue${repo:+ of $repo} at head $sha."; fi
prompt+=$'\n'"$(cat "$brief")"
local args=(exec -m "$model" -c "model_reasoning_effort=$effort" --sandbox "$sandbox")
if [ "$network" -eq 1 ]; then args+=(-c sandbox_workspace_write.network_access=true); fi
args+=(--json -C "$dir" -o "$out" "$prompt")
(cd "$dir" && codex "${args[@]}" < /dev/null > "$log" 2>&1)
local status=$?
[ "$status" -eq 0 ] || { echo "== codex review exit $status (transcript: $log) =="; return "$status"; }
[ -s "$out" ] || { echo "jpdev codex review: refused: codex wrote no final message" >&2; return 1; }
if [ "$(git -C "$dir" rev-parse HEAD)" != "$sha" ] || ! git -C "$dir" diff --quiet || ! git -C "$dir" diff --cached --quiet; then
echo "jpdev codex review: refused: the review changed tracked files or HEAD; restore the clone before continuing" >&2
return 1
fi
local verdict
verdict=$(grep -E '^(mergeable|not mergeable) — head ' "$out" | tail -n 1)
[ -z "$verdict" ] || printf '%s\n' "$verdict"
echo "jpdev codex review: reviewed $sha; record $out; transcript $log"
}

# codex_check_brief [--build] FILE: check the build heading and the filter-list words in FILE.
codex_check_brief() {
local file="" build=0
Expand Down Expand Up @@ -722,8 +792,14 @@ codex_publish() {
exit 0
}

# codex_brief_template prints the brief skeleton a Codex build starts from.
codex_brief_template() { cat "$kit/templates/codex-brief.md"; }
# codex_brief_template prints the brief skeleton a Codex build or review starts from.
codex_brief_template() {
case ${1:-} in
"") cat "$kit/templates/codex-brief.md" ;;
--review) [ $# -eq 1 ] || jpdev_die "codex brief-template: --review takes no value"; cat "$kit/templates/codex-review-brief.md" ;;
*) jpdev_die "codex brief-template: unknown argument $1" ;;
esac
}

# cmd_carry merges the current base into a pull-request branch. Conflicts are handed to the same
# codex_run path as a normal --no-pr build, using a generated brief.
Expand Down Expand Up @@ -805,7 +881,7 @@ PY
}

cmd_env() {
local v cfg
local v cfg manifest pyv venv chosen
cfg=$(jpdev_config_file)
printf 'jpdev %s\nkit %s\nconfig %s (%s)\n\n' "$JPDEV_VERSION" "$kit" "$cfg" "$([ -f "$cfg" ] && echo present || echo absent)"
for v in "${JPDEV_SETTINGS[@]}"; do printf '%-22s %-48s %s\n' "$v" "${!v}" "${JPDEV_SOURCE[$v]}"; done
Expand All @@ -816,6 +892,19 @@ cmd_env() {
done
printf '%-10s %s\n' python3.12 "$(jpdev_python_bin 3.12 || echo none)"
printf '%-10s %s\n' node22 "$(jpdev_node_bin 22 || echo "none under $JPDEV_NVM_DIR")"
manifest=${JPDEV_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null)}/dev.json
if [ -f "$manifest" ]; then
pyv=$(jpdev_manifest_get "$manifest" toolchains python version)
venv=$(jpdev_manifest_get "$manifest" toolchains python venv)
if [ -n "$venv" ] && [ -x "${manifest%/dev.json}/$venv/bin/python" ]; then
chosen=${manifest%/dev.json}/$venv/bin/python
elif [ -n "$pyv" ]; then
chosen=$(jpdev_python_bin "$pyv" || echo "not installed")
else
chosen=$(command -v python3 || echo "not on PATH")
fi
printf '%-10s %s\n' manifest-python "$chosen${pyv:+ (declared $pyv)}${venv:+ (venv $venv)}"
fi
[ -f "$JPDEV_NAME_PATTERNS" ] || echo "name-pattern file absent: jpdev record post refuses to post without --no-name-check"
}

Expand Down
28 changes: 25 additions & 3 deletions docs/adopting.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ Start from `templates/dev.json` and keep what the repository runs. The manifest
`jpdev` selects an installed Go that matches it and says when none is installed, and the
workflow falls back to the `go.mod` version when the manifest names none), `python`
(`{"version": "3.12", "venv": ".venv"}`; the version and the virtual environment are optional;
a declared virtual environment is first on PATH for every step).
the environment path is a non-root relative child of the clone with no `..` segment; setup and
checks create a missing declared environment with `python3.X` on PATH or the newest matching
pyenv version, then put its `bin` first on PATH for every step).
- `setup`: steps `jpdev setup` and CI run once after a clone (`npm ci`, the python
requirements).
- `checks`: the check list, in the order it should run (cheap checks first).
Expand Down Expand Up @@ -58,8 +60,14 @@ together, and copy both whole files again when updating them. `jpdev setup` name
either file is absent.

`scripts/dev/check.sh [--setup] [--only A,B] [--skip A,B] [--all]` runs the manifest with no
caps and prints the summary block. `jpdev check` uses the vendored copy when it exists and
sets `JPDEV_WRAP` so that each step runs under its toolchain's cap.
caps and prints the summary block. Before a setup or check run, it creates a missing declared
Python environment with `python3.X` on PATH, then the newest
`~/.pyenv/versions/3.X.*/bin/python3`; it refuses before any step when neither exists. Every step
runs with that environment's `bin` first on PATH, so the manifest's Python setup step installs its
requirements into the environment. A summary row names the interpreter for a Python step or any
step that invokes `python3`. `jpdev check` uses the vendored copy when it exists and sets
`JPDEV_WRAP` so that each step runs under its toolchain's cap. `jpdev env` shows the interpreter
selected for the current repository's manifest.

## 3. Add the agent instructions

Expand Down Expand Up @@ -109,6 +117,20 @@ jpdev setup . --manifest dev.json
jpdev check --all
```

For a saved Codex review, start from the review skeleton and keep the clone's tracked tree
unchanged:

```sh
jpdev codex brief-template --review > review-brief.md
jpdev codex review --brief review-brief.md --model MODEL --effort medium --dir . --pr NUMBER --repo OWNER/NAME
jpdev record post --pr NUMBER --repo OWNER/NAME --file .jpdev/output/review.md
```

The review command defaults to `workspace-write` so focused probes can create temporary files,
with no network access unless the caller passes `--network`. Its prompt forbids Git writes and it
refuses the result if HEAD or a tracked file changed. It closes stdin, writes only the final
message to the record, prints the reviewed SHA, and never posts.

## Worked example: the agent repository

`examples/judgment-pack-agent.dev.json` is the manifest of `judgment-pack-agent`. Its setup is
Expand Down
7 changes: 7 additions & 0 deletions docs/policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ normal no-new-pull-request Codex path; a carry with no conflicts is signed and p
- One round per pull request. A finding small enough to fix is fixed in the pull request with
its test; a second round is not opened for it.

`jpdev codex review` runs a checked review brief with an explicit model and effort, closes stdin,
and writes the final report to a file without committing, pushing or posting. The default sandbox
is `workspace-write` because read-only prevents shell probes from making temporary files; the brief
has no network unless the caller passes `--network`, forbids Git writes, and the command refuses
the result if HEAD or a tracked file changed. The command itself never posts. A Codex review of a
Codex build is the same vendor (OpenAI), not a cross-vendor claim.

## Severity

| Severity | What happens |
Expand Down
16 changes: 11 additions & 5 deletions lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,23 @@ jpdev_go_bin() {
printf '%s/bin' "$best"
}

# jpdev_python_matches PYTHON MAJOR.MINOR succeeds when PYTHON reports that release.
jpdev_python_matches() {
[ "$("$1" -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null)" = "$2" ]
}

# jpdev_python_bin MAJOR.MINOR prints the path of a working python of that version: python3.N on
# PATH when it runs (a pyenv shim for a version that is not selected does not), else the newest
# one under $PYENV_ROOT/versions (default ~/.pyenv/versions).
# PATH when it reports the release (a pyenv shim for a version that is not selected does not), else
# the newest matching version's bin/python3 under $PYENV_ROOT/versions (default ~/.pyenv/versions).
jpdev_python_bin() {
local found entry best=""
if found=$(command -v "python$1") && "$found" --version > /dev/null 2>&1; then printf '%s' "$found"; return 0; fi
if found=$(command -v "python$1") && jpdev_python_matches "$found" "$1"; then printf '%s' "$found"; return 0; fi
for entry in "${PYENV_ROOT:-$HOME/.pyenv}"/versions/"$1".*; do
[ -x "$entry/bin/python$1" ] && best=$(printf '%s\n%s\n' "$best" "$entry" | sort -V | tail -n 1)
[ -x "$entry/bin/python3" ] && jpdev_python_matches "$entry/bin/python3" "$1" && \
best=$(printf '%s\n%s\n' "$best" "$entry" | sort -V | tail -n 1)
done
[ -n "$best" ] || return 1
printf '%s/bin/python%s' "$best" "$1"
printf '%s/bin/python3' "$best"
}

# jpdev_clone_key ROOT prints a short name for a clone, used for the per-clone Go cache.
Expand Down
6 changes: 6 additions & 0 deletions lib/validate_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
because the fallback without the jsonschema package has no schema. Standard library otherwise. Exit 0 when valid, 1 with one line per problem, 2 on an unreadable file.
"""
import json
import os
import re
import sys

Expand Down Expand Up @@ -72,6 +73,11 @@ def main():
go_version = manifest.get("toolchains", {}).get("go", {}).get("version") if isinstance(manifest.get("toolchains"), dict) and isinstance(manifest["toolchains"].get("go"), dict) else None
if go_version is not None and not (isinstance(go_version, str) and re.fullmatch(r"[0-9]+\.[0-9]+(\.[0-9]+)?", go_version)):
problems.append("toolchains/go/version: %r is not a Go release number such as 1.26 or 1.26.5" % (go_version,))
python = manifest.get("toolchains", {}).get("python", {}) if isinstance(manifest.get("toolchains"), dict) else {}
venv = python.get("venv") if isinstance(python, dict) else None
if venv is not None and (not isinstance(venv, str) or not venv or os.path.isabs(venv)
or ".." in venv.split("/") or os.path.normpath(venv) == "."):
problems.append("toolchains/python/venv: venv must be a relative child path of the clone")
seen = set()
for step in steps:
name = step.get("name")
Expand Down
Loading
Loading