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
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ Edit `AGENTS.md` and run `scripts/sync-agent-docs.sh` to regenerate `CLAUDE.md`.
- If a check fails: read its first lines in the block, fix the cause, re-run that one check with
`--only NAME`, then run the whole list once more. Do not loop on the whole list.
- Start no CI watch. Push, report once, and stop; CI owns everything deterministic.
- Push with `jpdev push [BRANCH]`, or where the kit is not installed with
`git -c credential.helper= -c 'credential.helper=!gh auth git-credential' push -u origin BRANCH`.
The machine's stored git credential can be another account's, and a plain `git push` is then
refused; the helper form uses the `gh` login. Never store a credential, never change global git
configuration, never push with force (`jpdev push` refuses `--force` and `-f`).

## Rules

Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ Edit `AGENTS.md` and run `scripts/sync-agent-docs.sh` to regenerate `CLAUDE.md`.
- If a check fails: read its first lines in the block, fix the cause, re-run that one check with
`--only NAME`, then run the whole list once more. Do not loop on the whole list.
- Start no CI watch. Push, report once, and stop; CI owns everything deterministic.
- Push with `jpdev push [BRANCH]`, or where the kit is not installed with
`git -c credential.helper= -c 'credential.helper=!gh auth git-credential' push -u origin BRANCH`.
The machine's stored git credential can be another account's, and a plain `git push` is then
refused; the helper form uses the `gh` login. Never store a credential, never change global git
configuration, never push with force (`jpdev push` refuses `--force` and `-f`).

## Rules

Expand Down
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ the same name overrides it. `config/env.example` documents each setting with its
| `JPDEV_NVM_DIR` | `$NVM_DIR` or `~/.nvm` | where node versions live |
| `JPDEV_GO_SDK_DIR` | `~/sdk` | where a Go release the manifest names is looked for, when the go on PATH is another (`sdk/go1.26.5/bin/go`) |
| `JPDEV_NAME_PATTERNS` | `~/.config/jpdev/name-patterns` | regexes the record gate refuses |
| `JPDEV_CODEX_MODEL_DEFAULT` | none (the model is always passed) | the model `jpdev codex run` uses when `--model` is not given |
| `JPDEV_CODEX_SANDBOX` | `workspace-write` | the sandbox mode of `jpdev codex run` |
| `JPDEV_CODEX_WORDS` | `~/.config/jpdev/codex-words.txt` | extra terms for `jpdev codex check-brief` |

## Commands

Expand All @@ -96,14 +99,46 @@ the same name overrides it. `config/env.example` documents each setting with its
remains, a line matches the name patterns, or the name-pattern file is missing or holds a
pattern `grep -E` cannot compile, and posts with `gh ... comment --body-file`. A post that
should go without the name check says `--no-name-check`.
- `jpdev push [BRANCH]`: pushes BRANCH (default: the current branch) to `origin` with exactly
`git -c credential.helper= -c 'credential.helper=!gh auth git-credential' push -u origin BRANCH`,
so the push uses the `gh` login and no stored credential. It refuses `--force`, `-f` and a
`+` refspec.
- `jpdev codex run --brief FILE --model M --effort low|medium|high [--dir CLONE] [--repo OWNER/NAME]
[--base main] [--add PATH]... [--no-commit] [--no-push] [--no-pr] [--sandbox
workspace-write|danger-full-access] [--log FILE]`: checks the brief, then runs `codex exec` in
CLONE (a clone on a work branch that holds an `AGENTS.md`) with `-m`,
`-c model_reasoning_effort=`, `--sandbox` (network access on for `workspace-write`), stdin from
`/dev/null`, and a prompt of one line naming the repository's `AGENTS.md` as the rules followed
by the brief's text. The transcript goes to the log (default under `JPDEV_SCRATCH/logs`); the
command prints the final message and the exit code. The model is always passed: `--model`, or
`JPDEV_CODEX_MODEL_DEFAULT` from the config file.

Codex in `workspace-write` cannot write `.git`, so it does not commit. It writes `msg.txt` (the
commit message) and `pr.md` (the pull request body) in the clone, and after `codex exec` returns
the command does the git part: it stages the changes to tracked files (`git add -u`, plus the
paths named with `--add`; no other untracked file, and never `msg.txt` or `pr.md`), commits with
`-s` and the message of `msg.txt` with the `Co-Authored-By: Codex <noreply@openai.com>` trailer
added when `msg.txt` lacks it, pushes as `jpdev push` does, and opens the pull request with
`gh pr create --repo OWNER/NAME --base BASE --head BRANCH`, the subject of `msg.txt` as the
title and `pr.md` as the body. `--no-push` stops after the commit, `--no-pr` after the push and
`--no-commit` runs Codex alone. It stops with exit 1 and commits nothing when Codex changed no
tracked file or wrote no `msg.txt` (or no `pr.md`, when a pull request is to be opened), and
refuses a clone on `main`. The exit status is Codex's own when it fails, else 0 when every step
asked for was done. `--sandbox danger-full-access` is an explicit option only.
- `jpdev codex brief-template`: prints `templates/codex-brief.md`, a brief skeleton that tells
Codex to write `msg.txt` and `pr.md` and not to commit.
- `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
refusal.
- `jpdev env`: the effective settings and where each came from.

## The caps

| Toolchain | Cap |
| --- | --- |
| `node` | `lib/rsscap.sh`: the process group's resident memory and a timeout; never `ulimit -v`, which breaks vite and vitest |
| `go` | `flock` on the machine lock, `ulimit -v`, `GOFLAGS=-mod=mod`, a per-clone `GOCACHE`, `timeout` |
| `go` | `flock` on the machine lock, `ulimit -v`, `GOFLAGS=-mod=readonly` (a step never rewrites `go.mod`), a per-clone `GOCACHE`, `timeout` |
| `python` | `ulimit -v` and `timeout` |
| `shell` | `timeout` only |

Expand All @@ -113,9 +148,12 @@ script uses the environment's interpreter and packages.

## What is here

- `bin/jpdev`, `lib/`: the command, the caps, the manifest check.
- `bin/jpdev`, `lib/`: the command, the caps, the manifest check, the brief check.
- `config/env.example`, `config/codex-words.txt`: the settings by example, and the base word list of
`jpdev codex check-brief`.
- `templates/dev.json`, `templates/dev.schema.json`: the manifest by example and its JSON Schema.
- `templates/check.sh`: the public runner a repository vendors as `scripts/dev/check.sh`.
- `templates/codex-brief.md`: the brief skeleton `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
215 changes: 215 additions & 0 deletions bin/jpdev
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ usage: jpdev COMMAND [ARGS]
record post (--pr N | --issue N) --file FILE [--repo OWNER/NAME] [--dry-run] [--no-name-check]
reduce local paths, refuse a path, a name pattern, a missing or invalid
name-pattern file, then post the file
push [BRANCH] push BRANCH (default: the current branch) to origin with the gh credential
helper and no stored credential; refuses --force and -f
codex run --brief FILE --model M --effort low|medium|high [--dir CLONE] [--repo OWNER/NAME]
[--base main] [--add PATH]... [--no-commit] [--no-push] [--no-pr]
[--sandbox workspace-write|danger-full-access] [--log FILE]
check the brief, run `codex exec` on it in CLONE (a work branch) with the
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
print the brief skeleton: what Codex writes (msg.txt, pr.md) and does not do
codex check-brief FILE
refuse a brief that holds a word of the filter list, and suggest the
neutral phrasing
env print the effective settings and where each came from
--version print the version
USAGE
Expand Down Expand Up @@ -254,6 +268,205 @@ cmd_record() {
gh "$kind" comment "$number" ${repo:+--repo "$repo"} --body-file "$reduced"
}

# cmd_push [BRANCH]: the push form with the gh credential helper, never a stored credential.
cmd_push() {
local branch="" arg
for arg in "$@"; do
case $arg in
--force* | -f* | +*) jpdev_die "push: refused: a force push is never made (jpdev push takes a branch name only)" ;;
-*) jpdev_die "push: unknown argument $arg (jpdev push takes a branch name only)" ;;
*) [ -z "$branch" ] || jpdev_die "push: one branch only"; branch=$arg ;;
esac
done
repo_root > /dev/null
if [ -z "$branch" ]; then
branch=$(git symbolic-ref --short -q HEAD) || jpdev_die "push: HEAD is not on a branch; name the branch"
fi
git check-ref-format --branch "$branch" > /dev/null 2>&1 || jpdev_die "push: '$branch' is not a branch name"
jpdev_git_net push -u origin "$branch"
}

# cmd_codex run|check-brief ...
cmd_codex() {
local sub=${1:-}
[ $# -eq 0 ] || shift
case $sub in
run) codex_run "$@" ;;
check-brief) codex_check_brief "$@" ;;
brief-template) codex_brief_template ;;
-h | --help | help) usage; exit 0 ;;
*) jpdev_die "codex: the subcommands are run, check-brief and brief-template" ;;
esac
}

# codex_check_brief FILE: exit 1 and the neutral phrasing for each word of the filter list in FILE.
codex_check_brief() {
local file=""
while [ $# -gt 0 ]; do
case $1 in
-h | --help) usage; exit 0 ;;
-*) jpdev_die "codex check-brief: unknown argument $1" ;;
*) [ -z "$file" ] || jpdev_die "codex check-brief: one brief only"; file=$1; shift ;;
esac
done
[ -f "$file" ] || jpdev_die "codex check-brief: give the brief file"
local out status
out=$(python3 -I "$kit/lib/codex_words.py" "$file" "$kit/config/codex-words.txt" "$JPDEV_CODEX_WORDS")
status=$?
case $status in
0) echo "jpdev codex check-brief: no word of the filter list in $file" ;;
1)
printf 'jpdev codex check-brief: refused: %s holds words the content filter trips on:\n' "$file" >&2
printf ' %s\n' "${out//$'\n'/$'\n' }" >&2
echo "describe the documented refusal instead (the input, and the exact refusal the code must produce)" >&2
exit 1
;;
*) jpdev_die "codex check-brief: the check failed (exit $status)" ;;
esac
}

# codex_run: the Codex CLI build from a brief, then the git part (commit, push, pull request).
# Codex runs in workspace-write and cannot write .git, so the wrapper commits, pushes and opens
# the pull request itself from the msg.txt and pr.md that Codex wrote.
codex_run() {
local brief="" model=$JPDEV_CODEX_MODEL_DEFAULT effort="" dir=. sandbox=$JPDEV_CODEX_SANDBOX log=""
local repo="" base=main commit=1 push=1 pr=1 adds=()
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 ;;
--log) need_value "$@"; log=$2; shift 2 ;;
--repo) need_value "$@"; repo=$2; shift 2 ;;
--base) need_value "$@"; base=$2; shift 2 ;;
--add) need_value "$@"; adds+=("$2"); shift 2 ;;
--no-commit) commit=0; shift ;;
--no-push) push=0; shift ;;
--no-pr) pr=0; shift ;;
-h | --help) usage; exit 0 ;;
*) jpdev_die "codex run: unknown argument $1" ;;
esac
done
[ -f "$brief" ] || jpdev_die "codex run: --brief must name a file"
[ -n "$model" ] || jpdev_die "codex run: --model is needed (the model is always passed; JPDEV_CODEX_MODEL_DEFAULT can name one)"
case $effort in
low | medium | high) ;;
*) jpdev_die "codex run: --effort is low, medium or high" ;;
esac
case $sandbox in
workspace-write | danger-full-access) ;;
*) jpdev_die "codex run: --sandbox is workspace-write or danger-full-access" ;;
esac
[ -d "$dir" ] || jpdev_die "codex run: --dir is not a directory"
dir=$(cd "$dir" && pwd)
[ -f "$dir/AGENTS.md" ] || jpdev_die "codex run: $dir holds no AGENTS.md, which the prompt names as the rules"
command -v codex > /dev/null || jpdev_die "codex run: codex is not on PATH"
local branch=""
if [ "$commit" -eq 1 ]; then
git -C "$dir" rev-parse --show-toplevel > /dev/null 2>&1 || jpdev_die "codex run: $dir is not a git clone (--no-commit runs codex alone)"
branch=$(git -C "$dir" symbolic-ref --short -q HEAD) || jpdev_die "codex run: HEAD is not on a branch"
case $branch in
main | master) jpdev_die "codex run: the clone is on $branch; make a work branch first (the wrapper commits and pushes the current branch)" ;;
esac
if [ "$push" -eq 1 ] && [ "$pr" -eq 1 ] && [ -z "$repo" ]; then
jpdev_die "codex run: --repo OWNER/NAME is needed to open the pull request (--no-pr stops after the push)"
fi
local path
for path in "${adds[@]}"; do
case $path in
msg.txt | pr.md | ./msg.txt | ./pr.md) jpdev_die "codex run: --add $path: msg.txt and pr.md are read, never committed" ;;
/* | .. | ../* | */../*) jpdev_die "codex run: --add takes a path inside the clone" ;;
esac
done
fi
brief=$(cd "$(dirname "$brief")" && pwd)/$(basename "$brief")
codex_check_brief "$brief"
if [ -z "$log" ]; then
mkdir -p "$JPDEV_SCRATCH/logs"
log=$JPDEV_SCRATCH/logs/codex-$(basename "$dir")-$(date +%Y%m%dT%H%M%S).log
fi
mkdir -p "$(dirname "$log")"
log=$(cd "$(dirname "$log")" && pwd)/$(basename "$log")
local final prompt
final=$(mktemp)
# a msg.txt or pr.md left by an earlier run is not this run's
rm -f -- "$dir/msg.txt" "$dir/pr.md"
prompt="The rules are this repository's AGENTS.md; follow them.
$(cat "$brief")"
local args=(exec -m "$model" -c "model_reasoning_effort=$effort" --sandbox "$sandbox")
if [ "$sandbox" = workspace-write ]; then args+=(-c sandbox_workspace_write.network_access=true); fi
args+=(-C "$dir" -o "$final" "$prompt")
jpdev_log "codex run: model $model, effort $effort, sandbox $sandbox, in $dir; transcript $log"
(cd "$dir" && codex "${args[@]}" < /dev/null > "$log" 2>&1)
local status=$?
echo "== codex final message =="
if [ -s "$final" ]; then cat "$final"; else echo "(none written)"; fi
rm -f -- "$final"
echo "== codex exit $status (transcript: $log) =="
if [ "$status" -ne 0 ] || [ "$commit" -eq 0 ]; then exit "$status"; fi
codex_publish "$dir" "$branch" "$push" "$pr" "$repo" "$base" "${adds[@]}"
}

# codex_publish DIR BRANCH PUSH PR REPO BASE [ADD...]: stage the tracked changes (and the paths
# named with --add; never an untracked file otherwise, never msg.txt or pr.md), commit them signed
# off with the message in msg.txt plus the Codex trailer, push the branch, open the pull request
# from pr.md. Exits 0 when every step asked for was done, 1 when there was nothing to commit or a
# file was missing, and the failing command's status otherwise.
codex_publish() {
local dir=$1 branch=$2 push=$3 pr=$4 repo=$5 base=$6
shift 6
local trailer="Co-Authored-By: Codex <noreply@openai.com>" tmpd
cd "$dir" || exit 2
if [ ! -s msg.txt ]; then
echo "jpdev codex run: refused: codex wrote no msg.txt (the commit message); nothing was committed" >&2
exit 1
fi
if [ "$pr" -eq 1 ] && [ "$push" -eq 1 ] && [ ! -s pr.md ]; then
echo "jpdev codex run: refused: codex wrote no pr.md (the pull request body); nothing was committed" >&2
exit 1
fi
git add -u || exit 1
local path
for path in "$@"; do
git add -- "$path" || { echo "jpdev codex run: --add $path could not be staged" >&2; exit 1; }
done
if git diff --cached --name-only | grep -qxE 'msg\.txt|pr\.md'; then
git reset -q -- msg.txt pr.md
echo "jpdev codex run: refused: msg.txt or pr.md is tracked and changed; they are read, never committed" >&2
exit 1
fi
if git diff --cached --quiet; then
echo "jpdev codex run: nothing to commit: codex made no change to a tracked file" >&2
exit 1
fi
tmpd=$(mktemp -d)
git interpret-trailers --if-exists addIfDifferent --trailer "$trailer" msg.txt > "$tmpd/message" || { rm -rf -- "${tmpd:?}"; exit 1; }
git commit -q -s -F "$tmpd/message" || { local st=$?; rm -rf -- "${tmpd:?}"; echo "jpdev codex run: the commit failed" >&2; exit "$st"; }
local subject sha
subject=$(head -n 1 "$tmpd/message")
rm -rf -- "${tmpd:?}"
sha=$(git rev-parse --short HEAD)
echo "== jpdev codex run: committed $sha on $branch =="
git diff-tree --no-commit-id --name-status -r HEAD
local stray
stray=$(git ls-files --others --exclude-standard | grep -vxE 'msg\.txt|pr\.md')
if [ -n "$stray" ]; then
echo "untracked files left out of the commit (pass --add PATH to include one):"
printf ' %s\n' "$stray"
fi
[ "$push" -eq 1 ] || exit 0
(cmd_push "$branch") || { local st=$?; echo "jpdev codex run: the push failed; the commit $sha is local" >&2; exit "$st"; }
echo "== jpdev codex run: pushed $branch =="
[ "$pr" -eq 1 ] || exit 0
gh pr create --repo "$repo" --base "$base" --head "$branch" --title "$subject" --body-file pr.md || { local st=$?; echo "jpdev codex run: opening the pull request failed; the branch is pushed" >&2; exit "$st"; }
exit 0
}

# codex_brief_template prints the brief skeleton a Codex build starts from.
codex_brief_template() { cat "$kit/templates/codex-brief.md"; }

cmd_env() {
local v cfg
cfg=$(jpdev_config_file)
Expand All @@ -274,6 +487,8 @@ case ${1:-} in
check) shift; cmd_check "$@" ;;
mutants) shift; cmd_mutants "$@" ;;
record) shift; cmd_record "$@" ;;
push) shift; cmd_push "$@" ;;
codex) shift; cmd_codex "$@" ;;
env) shift; cmd_env "$@" ;;
--version | version) echo "jpdev $JPDEV_VERSION" ;;
-h | --help | help | "") usage; [ -n "${1:-}" ] || exit 2 ;;
Expand Down
Loading
Loading