mirror of
https://github.com/amElnagdy/delegate-skills.git
synced 2026-09-18 21:13:06 +08:00
Add codex-delegate skill and package scaffolding
First skill in the delegate-skills umbrella: drive the OpenAI Codex CLI as a background implementer (write a self-contained brief, dispatch via the bundled relay.mjs codex-exec wrapper, poll, review the diff, and commit it yourself — the orchestrator commits because Codex's sandbox can't reliably write .git). Includes SKILL.md, the relay.mjs helper (Node built-ins only; no network, no credentials, no telemetry), four references, README positioning it vs. the openai-codex plugin, MIT license, and skills.sh grouping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
# OS / editor cruft
|
||||
.DS_Store
|
||||
*.swp
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
# relay.mjs run artifacts, if a user points --out-dir back into the repo
|
||||
.delegate/
|
||||
|
||||
# local agent config symlink (see AGENTS.md)
|
||||
CLAUDE.md
|
||||
@@ -0,0 +1,34 @@
|
||||
# Working on delegate-skills
|
||||
|
||||
This repo is a [Skills CLI](https://github.com/vercel-labs/skills) package of **agent-relay skills** —
|
||||
skills that let an orchestrating agent drive a separate CLI coding agent as an implementer, then review
|
||||
and land the result. The first skill is `codex-delegate` (OpenAI Codex); siblings like `gemini-delegate`
|
||||
can be added later without renaming the repo.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **One skill per directory** under `skills/<name>/`, each with a `SKILL.md` plus optional
|
||||
`references/` and `scripts/`. The verb is the repo (`delegate`); the target agent is the skill name
|
||||
(`codex-delegate`), mirroring `guard-skills` → `clean-code-guard`.
|
||||
- **`SKILL.md` frontmatter:** `name` (must equal the directory), `description`, and optionally
|
||||
`license`, `compatibility`, `metadata.version`, `allowed-tools`. The **`description` is the only
|
||||
triggering signal** — keep it to what the skill does and when to use it, phrased to trigger reliably.
|
||||
Provenance, status caveats, and how-it-works detail go in the body or here, never in the description.
|
||||
- **Progressive disclosure:** keep `SKILL.md` lean; push depth into `references/*.md` that load only
|
||||
when needed.
|
||||
- **Executables:** keep them minimal and inspectable. The only one today is
|
||||
`skills/codex-delegate/scripts/relay.mjs` — Node built-ins only, no dependencies, no network calls of
|
||||
its own, no credentials, no telemetry. New scripts must hold the same line, and the README's trust
|
||||
section must stay accurate.
|
||||
|
||||
## Before publishing a change
|
||||
|
||||
- Validate the package locally: `npx skills add . --list`.
|
||||
- Smoke-test any changed script directly (e.g. `node skills/codex-delegate/scripts/relay.mjs --help`,
|
||||
and a `--read-only` run against a throwaway repo) before relying on it.
|
||||
- Keep the README's "Verification status" honest — claim only what's been run.
|
||||
|
||||
## Local Claude Code config
|
||||
|
||||
Claude Code reads `CLAUDE.md`, not `AGENTS.md`. If you want this file active while working here in
|
||||
Claude Code, symlink it (it's gitignored): `ln -s AGENTS.md CLAUDE.md`.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Ahmed Mohammed (amElnagdy)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,125 @@
|
||||
# delegate-skills
|
||||
|
||||
[](https://skills.sh/amElnagdy/delegate-skills)
|
||||
|
||||
Skills for **delegating coding work to a separate CLI agent and landing it yourself**. Your agent (the
|
||||
orchestrator) writes a self-contained brief, hands it to an implementer CLI, then reviews the diff and
|
||||
commits — staying the reviewer the whole way.
|
||||
|
||||
The first skill, **`codex-delegate`**, drives the OpenAI Codex CLI. A `gemini-delegate` and other
|
||||
implementers can live alongside it later — the repo name is the verb, the target agent lives in the
|
||||
skill name (mirroring how [`guard-skills`](https://github.com/amElnagdy/guard-skills) holds
|
||||
`clean-code-guard`, `test-guard`, …).
|
||||
|
||||
## Install
|
||||
|
||||
Browse first:
|
||||
|
||||
```bash
|
||||
npx skills add amElnagdy/delegate-skills --list
|
||||
```
|
||||
|
||||
Install the package, or just one skill:
|
||||
|
||||
```bash
|
||||
npx skills add amElnagdy/delegate-skills
|
||||
npx skills add amElnagdy/delegate-skills --skill codex-delegate
|
||||
```
|
||||
|
||||
Install for a specific agent, or globally:
|
||||
|
||||
```bash
|
||||
npx skills add amElnagdy/delegate-skills --skill codex-delegate --agent claude-code
|
||||
npx skills add amElnagdy/delegate-skills --global
|
||||
```
|
||||
|
||||
Works with any orchestrating agent the [Skills CLI](https://github.com/vercel-labs/skills) supports.
|
||||
|
||||
## What it does
|
||||
|
||||
The loop, for `codex-delegate`:
|
||||
|
||||
1. **Write a brief** — a self-contained task spec; Codex sees only what you send.
|
||||
2. **Dispatch** it with the bundled `relay.mjs` (a thin `codex exec` wrapper).
|
||||
3. **Wait** for completion — the helper writes a structured `result.json`.
|
||||
4. **Review** the diff — re-run the project's gates yourself; pair with guard skills.
|
||||
5. **Land** it — *you* commit, because the implementer's sandbox can't reliably write `.git`.
|
||||
|
||||
```text
|
||||
Use $codex-delegate to have Codex implement the refactor in services/billing/, then review and commit it.
|
||||
Use $codex-delegate to run this queue of migration tasks through Codex while I review each one.
|
||||
```
|
||||
|
||||
## How this differs from the OpenAI Codex plugin
|
||||
|
||||
The official [openai-codex Claude Code plugin](https://github.com/openai/codex) is excellent and
|
||||
**complementary** — this skill builds on the same `codex` CLI, it doesn't replace the plugin. They
|
||||
point in different directions:
|
||||
|
||||
- The plugin's `codex:codex-rescue` agent is a **forwarder**: it hands one task to Codex and returns
|
||||
the output. It deliberately does not poll, review, or commit.
|
||||
- The plugin's review command and stop-gate run the **inverse** direction: **Codex reviews your work**.
|
||||
- `codex-delegate` is the **orchestration loop in the other direction**: *you* drive Codex to
|
||||
implement across one task or a queue, and *you* review and land each result. That loop — brief →
|
||||
dispatch → poll → review → commit, with the orchestrator owning the commit — is what the plugin
|
||||
leaves to you, and what this skill encodes.
|
||||
|
||||
If you have the plugin installed, its companion CLI is an optional alternative dispatch backend; the
|
||||
bundled `relay.mjs` is the default because it needs nothing but the `codex` binary.
|
||||
|
||||
## The skills
|
||||
|
||||
### codex-delegate
|
||||
|
||||
Drive the OpenAI Codex CLI as a background implementer: write the brief, dispatch via `relay.mjs`,
|
||||
review the diff, commit it yourself. Ships four references (writing the brief, dispatch/poll, review/
|
||||
land, multi-task queues) loaded only when needed, and one small helper script.
|
||||
|
||||
**You'll feel it when:** a bounded task — a migration, a mechanical refactor, a removal sweep — gets
|
||||
handed to Codex, comes back as a clean diff with a structured report, and you commit it after re-running
|
||||
the gates yourself instead of typing it all by hand.
|
||||
|
||||
### gemini-delegate
|
||||
|
||||
*Planned.* A relay for the Gemini CLI, if and when it gains a comparable non-interactive mode. Reserved
|
||||
so the umbrella can grow without a rename.
|
||||
|
||||
## Requirements
|
||||
|
||||
- The [`codex` CLI](https://github.com/openai/codex) installed and authenticated (`codex login`).
|
||||
- Node 18+ and `git`.
|
||||
- An orchestrating agent that can run shell commands and read files.
|
||||
|
||||
## Trust and validation
|
||||
|
||||
This package is intentionally inspectable:
|
||||
|
||||
- All skill content is Markdown, plus exactly **one** executable: `skills/codex-delegate/scripts/relay.mjs`.
|
||||
- `relay.mjs` itself makes no network calls, reads or writes no credentials, sends no telemetry, and
|
||||
has no dependencies (Node built-ins only). It shells out only to `codex` and `git`. The `codex`
|
||||
process it launches authenticates exactly as you do at the terminal. Read the script before you run it.
|
||||
- It never commits — committing is always the orchestrator's job, after review.
|
||||
|
||||
**Verification status:** the loop is verified on Claude Code. Other shell-capable orchestrators
|
||||
(OpenCode, Cursor, …) are designed-for but not yet verified — the skill is written orchestrator-neutral
|
||||
so they should work, and that line gets upgraded to "verified" with evidence, not assumption.
|
||||
|
||||
## Repository shape
|
||||
|
||||
```text
|
||||
skills/
|
||||
└── codex-delegate/
|
||||
├── SKILL.md
|
||||
├── scripts/relay.mjs
|
||||
└── references/
|
||||
├── writing-the-brief.md
|
||||
├── dispatch-and-poll.md
|
||||
├── review-and-land.md
|
||||
└── multi-task-queues.md
|
||||
```
|
||||
|
||||
The `SKILL.md` stays small so it loads cheaply; the references load only when the task needs them.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://skills.sh/schemas/skills.sh.schema.json",
|
||||
"groupings": [
|
||||
{
|
||||
"title": "Agent Relays",
|
||||
"description": "Delegate a coding task to a CLI implementer agent, then review the diff and commit it yourself.",
|
||||
"skills": [
|
||||
"codex-delegate"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: codex-delegate
|
||||
description: >-
|
||||
Delegate a coding task to the OpenAI Codex CLI as a background implementer, then review its diff and
|
||||
land it. Use this whenever the user wants to hand implementation work to Codex — phrasings like "have
|
||||
Codex do X", "delegate this to Codex", "run it through Codex", or "use Codex to implement/fix/refactor"
|
||||
— or wants to run a queue of coding tasks through Codex while staying the reviewer. Also reach for it
|
||||
proactively when the user wants a separate implementation pass on a bounded, well-specified task (an
|
||||
implementation sweep, a migration, a mechanical refactor, parallel work) and will review and commit
|
||||
the result themselves. Covers writing the Codex brief, dispatching it via the bundled relay.mjs
|
||||
helper, waiting for completion, reviewing the result, and committing. DO NOT USE for tasks small
|
||||
enough to do inline, when the codex CLI is not installed or authenticated, or when the user wants the
|
||||
code written directly without delegating.
|
||||
license: MIT
|
||||
compatibility: Requires the `codex` CLI (OpenAI Codex) installed and authenticated, Node 18+, and git. The orchestrating agent must be able to run shell commands and read files.
|
||||
metadata:
|
||||
version: 0.1.0
|
||||
allowed-tools: Bash(node:*) Bash(codex:*) Bash(git:*) Read
|
||||
---
|
||||
|
||||
# Codex Delegate
|
||||
|
||||
You are the **orchestrator**. This skill lets you hand a bounded coding task to a separate
|
||||
**implementer** — the OpenAI Codex CLI — then review what it produced and land it yourself. You write
|
||||
the brief and own the judgment; Codex does the typing in its own sandbox; you verify and commit.
|
||||
|
||||
Nothing here is specific to one orchestrating agent. The loop needs only the ability to run a shell
|
||||
command and read a file, so it works the same whether you are Claude Code, OpenCode with a selected
|
||||
model, or any comparable agent. (It has been verified on Claude Code; treat other orchestrators as
|
||||
designed-for, not yet proven.)
|
||||
|
||||
## When to use this
|
||||
|
||||
- You want to delegate a **bounded, well-specified** coding task and stay the reviewer.
|
||||
- You have a **queue** of such tasks (a removal, a migration, a refactor sweep) to run one at a time.
|
||||
- You want a second implementation pass from a different model while you keep control of what merges.
|
||||
|
||||
## When NOT to use this
|
||||
|
||||
- The task is small enough to just do inline — delegation overhead is not worth it.
|
||||
- The `codex` CLI is not installed or not authenticated (run `codex login`).
|
||||
- You want to write the code yourself, or you only need a review (use Codex's own `review` command).
|
||||
|
||||
## Prerequisites (check once)
|
||||
|
||||
1. `codex --version` succeeds. If not, install (`npm i -g @openai/codex`) and `codex login`.
|
||||
2. **Confirm which `codex` is on PATH.** Multiple installs are common (e.g. a current npm/nvm copy and
|
||||
a stale Homebrew one). `which -a codex` and `codex --version` — an old binary predates flags this
|
||||
skill relies on (`codex exec --json`, `-o`, `exec resume`). The relay records the version it ran
|
||||
into `result.json`, so a stale binary is visible after the fact.
|
||||
3. You are in (or will point `--cd` at) the target git repository.
|
||||
|
||||
## The loop
|
||||
|
||||
Run these five steps per task. Steps 1, 4, and 5 are your judgment; 2 and 3 are mechanical.
|
||||
|
||||
### 1. Write the brief
|
||||
|
||||
Codex sees **only** the text you send — no repo memory, no chat history, no devdocs. Everything the
|
||||
task needs goes in the brief: the goal, the current state, what to change, what to leave untouched,
|
||||
the project's **actual** gate commands (discover them from the repo's CLAUDE.md/AGENTS.md/Makefile —
|
||||
do not assume), and a report contract. Tell Codex it will **not** commit (you will). Keep one task per
|
||||
brief. Full guidance and a template: [references/writing-the-brief.md](references/writing-the-brief.md).
|
||||
|
||||
### 2. Dispatch
|
||||
|
||||
Send the brief to Codex with the bundled helper. It wraps `codex exec`, captures the run, and writes a
|
||||
structured `result.json` — so your only job is "run a command, read a file."
|
||||
|
||||
```bash
|
||||
node "<skill-dir>/scripts/relay.mjs" --brief brief.txt --cd /path/to/repo
|
||||
# read-only (review/diagnosis, no edits): add --read-only
|
||||
# continue the previous Codex session: add --resume-last (send only the delta brief)
|
||||
# see all options: node .../relay.mjs --help
|
||||
```
|
||||
|
||||
The helper defaults to a write-capable (`workspace-write`) sandbox and writes its artifacts to a temp
|
||||
dir, so the repo under review stays clean. It **never commits** — see step 5. Mechanics, flags, and the
|
||||
`result.json` shape: [references/dispatch-and-poll.md](references/dispatch-and-poll.md).
|
||||
|
||||
### 3. Wait for completion
|
||||
|
||||
The helper blocks until Codex finishes, so back it with whatever your orchestrator offers and resume
|
||||
when it returns:
|
||||
|
||||
- **Claude Code:** run the Bash call with `run_in_background: true`; you are notified on completion.
|
||||
- **Plain shell / other agents:** run it in the foreground for short tasks, or `… &` and poll the
|
||||
result file. The run is done when `result.json` exists with a `status`. (A pre-run usage error —
|
||||
bad args or an empty brief — instead exits non-zero with a stderr message and writes no result
|
||||
file, so check the exit code too, not just the file.)
|
||||
|
||||
Do not trust progress trackers over reality: a run is finished when `result.json` is written and the
|
||||
process has exited. Read the working tree, not a status line.
|
||||
|
||||
### 4. Review — do not trust the self-report
|
||||
|
||||
Codex's `result.json` includes its own summary and gate claims. **Re-verify, don't accept:**
|
||||
|
||||
- **Re-run the project's gates yourself** (the test/lint/build commands from step 1). Never take
|
||||
"gates passed" on faith.
|
||||
- **Read the diff** against the brief: did Codex do what was asked, nothing more (scope creep) and
|
||||
nothing less? `touchedFiles` in the result is your starting point.
|
||||
- **Run the relevant guard skills** on the diff if you have them installed (clean-code-guard,
|
||||
test-guard, etc. from `guard-skills`) — this skill produces the work; those skills judge it.
|
||||
- For schema/migration changes, round-trip them; for removals, grep for dangling references.
|
||||
|
||||
Full checklist: [references/review-and-land.md](references/review-and-land.md).
|
||||
|
||||
### 5. Land it
|
||||
|
||||
Because Codex's sandbox cannot reliably write `.git` (it varies by version, OS, and path), **the
|
||||
orchestrator commits.** Only after the gates pass and the diff holds:
|
||||
|
||||
- Commit the verified work yourself, with a clear message.
|
||||
- If it needs changes, send a delta brief with `--resume-last` (don't restate the whole task) and
|
||||
review again.
|
||||
|
||||
## Non-negotiables
|
||||
|
||||
- **Re-run the gates yourself.** The self-report is a claim, not evidence.
|
||||
- **The orchestrator commits, never Codex.** Don't assume Codex committed; it didn't.
|
||||
- **One task = one brief = one commit.** Split unrelated work into separate runs.
|
||||
- **Trust the working tree and process state** over any progress tracker.
|
||||
|
||||
## Authorization model
|
||||
|
||||
Delegation is something the human opts into. Once they have ("run this queue", "proceed"), committing
|
||||
verified, gate-passing work is the agreed contract — that is the whole point. But:
|
||||
|
||||
- **Surface, don't absorb.** When Codex makes a design decision, takes a defensible-but-unasked turn,
|
||||
or you spot a non-blocking nitpick, report it to the human rather than silently keeping it.
|
||||
- **Stop for scope changes.** If correct completion requires going beyond the brief, stop and ask;
|
||||
don't expand the mandate on your own.
|
||||
|
||||
## Trust and safety
|
||||
|
||||
`scripts/relay.mjs` itself makes no network calls, reads or writes no credentials, and sends no
|
||||
telemetry; it has no dependencies (Node built-ins only) and shells out only to `codex` and `git`. The
|
||||
`codex` process it launches does authenticate — exactly as you do at the terminal. Read the script
|
||||
before you run it. It is the one executable in this package; everything else is Markdown.
|
||||
|
||||
## References
|
||||
|
||||
- [references/writing-the-brief.md](references/writing-the-brief.md) — how to write a brief Codex can
|
||||
execute blind: structure, XML blocks, the report contract, embedding the real gate commands.
|
||||
- [references/dispatch-and-poll.md](references/dispatch-and-poll.md) — `relay.mjs` flags, the
|
||||
`result.json` contract, backgrounding per orchestrator, and recovery when a run misbehaves.
|
||||
- [references/review-and-land.md](references/review-and-land.md) — the review checklist, the commit
|
||||
boundary, and the rework cycle via `--resume-last`.
|
||||
- [references/multi-task-queues.md](references/multi-task-queues.md) — running a sequential queue:
|
||||
carrying constraints forward, progress tracking, and the end-of-run coherence check.
|
||||
|
||||
## What this skill does NOT do
|
||||
|
||||
- It does not commit for you — that is deliberate (step 5).
|
||||
- It does not review the code's quality itself — pair it with guard skills.
|
||||
- It does not run your tests — you re-run the project's own gates in step 4.
|
||||
- It is not the inverse direction (Codex reviewing your work). For that, use the openai-codex plugin's
|
||||
review command or stop-review gate.
|
||||
@@ -0,0 +1,109 @@
|
||||
# Dispatch and poll
|
||||
|
||||
`scripts/relay.mjs` is the dispatch layer. It wraps `codex exec`, runs the brief in a sandbox, captures
|
||||
everything, and writes a structured `result.json`. Your job collapses to: run one command, then read
|
||||
one file. Everything Codex-specific lives in the helper, which is what keeps the loop portable across
|
||||
orchestrators.
|
||||
|
||||
## Before the first run: check the binary
|
||||
|
||||
Two gotchas, both worth 30 seconds:
|
||||
|
||||
```bash
|
||||
which -a codex # more than one? a stale install (e.g. Homebrew) may shadow a current one
|
||||
codex --version # an old binary predates `exec --json`, `-o`, and `exec resume`
|
||||
codex login status # must be authenticated
|
||||
```
|
||||
|
||||
The Codex CLI moves fast and behavior shifts between versions, so the helper records the version it
|
||||
actually ran into `result.json` — if something behaves oddly, check which binary answered.
|
||||
|
||||
## Dispatching
|
||||
|
||||
```bash
|
||||
node "<skill-dir>/scripts/relay.mjs" --brief brief.txt --cd /path/to/repo
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
| Flag | Effect |
|
||||
| --- | --- |
|
||||
| `--brief <file>` | The brief. Omit it to read the brief from stdin (`cat brief.txt \| node relay.mjs …`). |
|
||||
| `--cd <dir>` | Working root for Codex (default: current directory). |
|
||||
| `--model <name>` | Codex model (default: Codex's own configured default). |
|
||||
| `--sandbox <mode>` | `read-only` \| `workspace-write` \| `danger-full-access` (default: `workspace-write`). |
|
||||
| `--read-only` | Shortcut for `--sandbox read-only` — review/diagnosis with no edits. |
|
||||
| `--resume-last` | Continue the most recent Codex session; send only the delta brief (see review-and-land). |
|
||||
| `--skip-git-repo-check` | Allow running outside a git repo. |
|
||||
| `--out-dir <dir>` | Where artifacts go (default: a fresh dir under the system temp dir). |
|
||||
|
||||
Artifacts default to the system temp dir on purpose: the repo under review stays clean, so the
|
||||
touched-files report shows only Codex's edits and nothing of the helper's own.
|
||||
|
||||
## The result
|
||||
|
||||
`<out-dir>/result.json` is the contract. Fields:
|
||||
|
||||
- `status` — `completed` | `failed` | `codex_unavailable`
|
||||
- `exitCode` — mirrors Codex's exit code; `127` if `codex` isn't on PATH
|
||||
- `codexVersion` — the binary that actually ran
|
||||
- `threadId` — feed this to a later `codex exec resume <id>` (or use `--resume-last`)
|
||||
- `finalMessage` — Codex's own final report (the `<structured_output_contract>` you asked for)
|
||||
- `touchedFiles` — `git status --porcelain` lines in the working root: your review starting point
|
||||
- `eventsPath` / `finalPath` — the raw JSONL event stream and the final-message file
|
||||
- `workdir`, `sandbox`, `model`, `resumeLast`, `startedAt`, `finishedAt`
|
||||
|
||||
The helper also prints a summary to stdout and exits with Codex's exit code, so a wrapping script can
|
||||
branch on success/failure directly.
|
||||
|
||||
## Waiting for completion
|
||||
|
||||
The helper blocks until Codex finishes. Back it with whatever your orchestrator offers:
|
||||
|
||||
- **Claude Code:** run the `Bash` call with `run_in_background: true`; you're notified on completion,
|
||||
then read `result.json`.
|
||||
- **Plain shell / other agents:** foreground for short tasks, or `node relay.mjs … &` and poll. A run
|
||||
is done when `result.json` exists with a `status`. **But** a pre-run usage error (bad args, empty
|
||||
brief) exits non-zero *before* writing any file — so check the exit code too, don't only watch for
|
||||
the file.
|
||||
|
||||
Trust the working tree and the process state over any progress display. A run is finished when the
|
||||
process has exited and `result.json` is written — not when a status line says so.
|
||||
|
||||
## When a run misbehaves
|
||||
|
||||
- **`status: codex_unavailable` (exit 127):** `codex` isn't on PATH or isn't found. Install
|
||||
(`npm i -g @openai/codex`) and `codex login`, then re-dispatch.
|
||||
- **`status: failed`:** read `result.json`'s `stderrTail` and the tail of `eventsPath` for the cause.
|
||||
Common causes: an auth lapse, an invalid `--model`, or a sandbox that blocked something the task
|
||||
needed. Fix the cause and re-dispatch; don't paper over it by doing the work yourself unless that's
|
||||
what the user wants.
|
||||
- **Empty `finalMessage`:** Codex exited before producing a final message. Treat as a failed run;
|
||||
the events log usually shows where it stopped.
|
||||
|
||||
## What the helper is doing (and the alternatives)
|
||||
|
||||
Under the hood the helper runs roughly:
|
||||
|
||||
```bash
|
||||
codex exec --json -o <final.txt> -s workspace-write [-m model] - < brief.txt # fresh run
|
||||
codex exec resume --last --json -o <final.txt> - < delta-brief.txt # resume (no -s/-C)
|
||||
```
|
||||
|
||||
`resume` deliberately gets no `-s`/`-C` — it inherits the original session's sandbox and working root —
|
||||
which is why the helper sets the child process's working directory instead.
|
||||
|
||||
Two alternatives exist if you ever want them, but the helper is the recommended path:
|
||||
|
||||
- **Raw `codex exec`** — fine for one-offs; you give up the captured `result.json`, touched-files
|
||||
summary, and thread-id extraction the helper does for you.
|
||||
- **The openai-codex Claude Code plugin's companion CLI** (`task`/`status`/`result`) — richer job
|
||||
tracking if you have that plugin installed, but it depends on the plugin and its background dispatch
|
||||
can occasionally stall a job in a `queued` state with no worker. The helper sidesteps that by running
|
||||
in-process.
|
||||
|
||||
## The commit boundary
|
||||
|
||||
The helper never commits — by design, not omission. Whether Codex's sandbox can write `.git` varies by
|
||||
version, OS, and execution path, so relying on it is a coin flip. The robust contract is: Codex edits
|
||||
the working tree, the orchestrator reviews and commits. See [review-and-land.md](review-and-land.md).
|
||||
@@ -0,0 +1,66 @@
|
||||
# Multi-task queues
|
||||
|
||||
The single-task loop scales to a queue, and that's where delegation pays off most — a removal split
|
||||
across layers, a migration touching many files, a refactor sweep. The discipline that makes a queue
|
||||
trustworthy is sequencing and bookkeeping, not parallelism.
|
||||
|
||||
## Run sequentially, one commit per task
|
||||
|
||||
Resist the urge to fan out the whole queue at once. Run tasks **one at a time, in dependency order**,
|
||||
landing each (review + gates + commit) before dispatching the next. Three reasons:
|
||||
|
||||
- **Later tasks assume earlier ones landed.** Task 3's brief can say "the X added in the previous step
|
||||
exists" only if the previous step actually committed.
|
||||
- **One commit per task** keeps the history reviewable and any single step revertible.
|
||||
- **Each review is honest.** A clean working tree before each dispatch means the next task's
|
||||
`touchedFiles` shows only *its* changes, not a pile-up from earlier tasks.
|
||||
|
||||
Parallelism is occasionally worth it for genuinely independent tasks on separate files, but it
|
||||
sacrifices the clean-tree-per-task property and makes review harder. Default to sequential.
|
||||
|
||||
## Carry decided constraints forward
|
||||
|
||||
Implementation surfaces facts the original plan didn't have: a helper got named, a fixture lives in a
|
||||
specific place, an interface was chosen. When a later task depends on one of those, **fold it into that
|
||||
task's brief** as an explicit line. Codex has no memory of the earlier run, so a constraint that
|
||||
emerged in task 2 must be restated in task 5's brief or it won't hold. This is the queue equivalent of
|
||||
keeping briefs self-contained.
|
||||
|
||||
## Keep a progress file
|
||||
|
||||
For anything longer than two or three tasks — especially a run the human steps away from — maintain a
|
||||
single progress file alongside the work. It's the durable record that survives your own context limits
|
||||
and lets the human catch up at a glance. A shape that works:
|
||||
|
||||
- **Status table** — each task: queued / at-implementer / reviewed+committed (with the commit hash).
|
||||
- **Per-task review notes** — what landed, what you verified, the gate outcome. One short paragraph.
|
||||
- **"Needs your eyes"** — design decisions Codex made, non-blocking nitpicks, anything you want the
|
||||
human to overrule or confirm. This is the section they read first.
|
||||
- **End-of-run checklist** — what happens after the last task (push, open/update the PR, manual checks
|
||||
the human should do).
|
||||
|
||||
Update it as each task lands, not in a batch at the end — if the run is interrupted, the file is still
|
||||
accurate.
|
||||
|
||||
## Close with a coherence check
|
||||
|
||||
Per-task review proves each step in isolation; it doesn't prove the steps cohere. After the last task,
|
||||
verify the whole:
|
||||
|
||||
- Run the full test/build once more on the final tree — not just the last task's slice.
|
||||
- Do a repo-wide check for the thing the queue was about (e.g. after a removal, grep the entire tree
|
||||
for any surviving reference; after a rename, confirm no stragglers).
|
||||
- For schema work, replay all the new migrations from a clean state and check for drift.
|
||||
- Then push and open or update the PR, with a description that reflects what actually shipped.
|
||||
|
||||
## When to stop and ask
|
||||
|
||||
Proceed without asking on anything that follows from the agreed plan — that's the point of the human
|
||||
opting into the queue. Stop and surface when:
|
||||
|
||||
- A task can't be completed correctly within its brief's scope (a scope change is the human's call).
|
||||
- A review finds something that calls the *plan* into question, not just the implementation.
|
||||
- The gates reveal a problem that affects tasks already "done."
|
||||
|
||||
Then report where you are, what's committed, and what the open question is — and wait. A queue that
|
||||
quietly works around a broken assumption produces a lot of commits in the wrong direction.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Review and land
|
||||
|
||||
Codex did the typing; you own the judgment. This is where delegation earns its keep or quietly ships a
|
||||
mistake. The discipline is simple to state and easy to skip under time pressure: **verify against
|
||||
reality, never against the self-report, then commit it yourself.**
|
||||
|
||||
## Re-run the gates yourself
|
||||
|
||||
`result.json` carries Codex's own claim that the gates passed. Treat that as a claim, not evidence —
|
||||
re-run the project's actual test/lint/build commands in the working tree and read the output. A run
|
||||
that "passed" in Codex's report but fails when you run it is exactly the failure this step exists to
|
||||
catch, and it happens often enough to be worth the minute every time.
|
||||
|
||||
For changes with their own verification shape, go further:
|
||||
|
||||
- **Migrations / schema:** round-trip them (apply, reverse, re-apply on a scratch target) and check for
|
||||
drift, rather than trusting that "the migration is reversible."
|
||||
- **Removals / renames:** grep the codebase for dangling references to whatever was removed.
|
||||
- **Anything stateful:** exercise the actual behavior, don't just confirm it compiles.
|
||||
|
||||
## Read the diff against the brief
|
||||
|
||||
Open the diff (`touchedFiles` in the result is your starting list) and hold it against what you asked
|
||||
for:
|
||||
|
||||
- **Scope creep** — did Codex change things the brief said to leave untouched? Unasked refactors,
|
||||
renames, "while I was here" edits. These are the most common quality problem in delegated work.
|
||||
- **Scope shortfall** — did it do the whole task, including the edge cases and cleanup, or stop at the
|
||||
first plausible version?
|
||||
- **Quiet judgment calls** — sometimes Codex makes a defensible decision the brief didn't anticipate.
|
||||
Don't just accept it because it looks reasonable; understand it and decide.
|
||||
|
||||
## Compose with guard skills
|
||||
|
||||
This skill produces the work; it doesn't judge code quality. If you have the `guard-skills` package
|
||||
installed, run the relevant guard on Codex's diff before you commit — `clean-code-guard` on production
|
||||
code, `test-guard` on any tests it wrote, `docs-guard` on documentation. They catch the systematic
|
||||
failure modes of generated code that a quick read misses. The two packages are designed to pair:
|
||||
delegate-skills delegates and lands; guard-skills reviews.
|
||||
|
||||
## The commit boundary
|
||||
|
||||
When the gates pass and the diff holds, **you commit** — the orchestrator, never Codex. This isn't a
|
||||
workaround for a missing feature; it's the deliberate boundary. Codex's sandbox can't reliably write
|
||||
`.git`, and more importantly, committing should be the act of the party that verified the work. Write
|
||||
a clear message describing what landed. If your project attributes co-authorship, that's the place
|
||||
for it.
|
||||
|
||||
## Reworking: send the delta, not the whole task
|
||||
|
||||
If the review turns up problems, don't restate the entire brief. Continue the same Codex session with
|
||||
just the correction:
|
||||
|
||||
```bash
|
||||
echo "The fix is right, but the test mocks the DB session — use the real migrated fixture instead, and
|
||||
drop the now-unused import." | node "<skill-dir>/scripts/relay.mjs" --resume-last --cd /path/to/repo
|
||||
```
|
||||
|
||||
`--resume-last` keeps Codex's context from the first run, so a short delta is enough. Then review
|
||||
again — rework gets the same gate-rerun and diff-read as the original, no shortcuts. Repeat until it's
|
||||
right, then commit.
|
||||
|
||||
## Surface, don't absorb
|
||||
|
||||
The human opted into delegation, so committing verified, gate-passing work is the agreed contract.
|
||||
But keep them in the loop on anything that changes the shape of the work:
|
||||
|
||||
- **Report design decisions** Codex made, and any defensible-but-unrequested turns it took.
|
||||
- **Note non-blocking nitpicks** you chose not to block on, so the human can overrule you.
|
||||
- **Stop and ask** if correct completion requires going beyond the brief — don't expand the mandate on
|
||||
your own. A scope change is the human's call, not yours or Codex's.
|
||||
|
||||
For a multi-task run, capture these in the progress file rather than letting them scroll past — see
|
||||
[multi-task-queues.md](multi-task-queues.md).
|
||||
@@ -0,0 +1,114 @@
|
||||
# Writing the brief
|
||||
|
||||
A brief is the entire task as Codex will see it. Codex runs in a fresh process with **no memory of
|
||||
your conversation, no devdocs, and no shared context** — only the text you send and whatever it can
|
||||
read from the working tree (including the repo's own `AGENTS.md`, which it picks up automatically).
|
||||
If a constraint isn't in the brief or discoverable in the repo, it doesn't exist for Codex. The single
|
||||
most common failure is a brief that assumes context Codex doesn't have.
|
||||
|
||||
## The shape that works
|
||||
|
||||
Codex (a GPT-5.x-class model) responds best to compact, block-structured prompts with XML tags rather
|
||||
than long prose. State the task, what "done" looks like, how to behave by default, and the few
|
||||
constraints that actually matter. Add a block only when the task needs it — don't ship empty ceremony.
|
||||
|
||||
```xml
|
||||
<task>
|
||||
One or two sentences: the concrete job and where it lives. Then the specifics — current state, what to
|
||||
change, and explicitly what to leave untouched. The "leave untouched" list is what keeps Codex from
|
||||
wandering into unrelated refactors.
|
||||
</task>
|
||||
|
||||
<verification_loop>
|
||||
Run these before finishing and fix anything they surface, don't just report it:
|
||||
<the project's real test command>
|
||||
<the project's real lint/format command>
|
||||
<the project's real build/typecheck command>
|
||||
Confirm the working tree shows only the intended changes afterward.
|
||||
</verification_loop>
|
||||
|
||||
<action_safety>
|
||||
Keep changes scoped to the task. No unrelated refactors, renames, or cleanup unless required for
|
||||
correctness. Do NOT run git add or git commit — you cannot reliably write .git, and the orchestrator
|
||||
commits after reviewing. Leave the work uncommitted in the working tree.
|
||||
</action_safety>
|
||||
|
||||
<structured_output_contract>
|
||||
End with a report in this exact shape:
|
||||
1. What changed and why
|
||||
2. Files touched
|
||||
3. Gate outcomes (paste the test/lint counts)
|
||||
4. Anything you deviated on, left open, or want a decision on
|
||||
</structured_output_contract>
|
||||
```
|
||||
|
||||
That four-block skeleton covers most implementation tasks. Reach for the extra blocks when the task
|
||||
profile calls for them:
|
||||
|
||||
- **Debugging / open-ended fixes** — add `<completeness_contract>` (resolve fully, don't stop at the
|
||||
first plausible fix) and `<missing_context_gating>` (don't guess missing repo facts; find them or
|
||||
state what's unknown).
|
||||
- **Review / diagnosis (read-only)** — add `<grounding_rules>` (ground every claim in evidence; label
|
||||
inferences) and run with `--read-only` so Codex can't edit.
|
||||
- **Research / recommendations** — add `<research_mode>` (separate observed facts, inferences, open
|
||||
questions).
|
||||
|
||||
## Discover the real gates — don't hardcode
|
||||
|
||||
`<verification_loop>` is only useful if it names the project's *actual* commands. Read the repo's
|
||||
`CLAUDE.md` / `AGENTS.md` / `Makefile` / `package.json` first and copy the real ones in (`make test`,
|
||||
`npm run lint`, `cargo test`, `pytest -q`, whatever it is). A brief that says "run the tests" without
|
||||
naming them gets you a Codex that guesses — or skips.
|
||||
|
||||
## Honor the repo's conventions
|
||||
|
||||
Codex reads the repo's `AGENTS.md` automatically, so house rules there (style, forbidden patterns,
|
||||
commit conventions) already apply. If the project forbids certain things in code — spec/ticket IDs in
|
||||
comments, process language like "MVP"/"for now"/"phase N", specific test conventions — restate the
|
||||
load-bearing ones in the brief too, because Codex's compliance is only as reliable as what's in front
|
||||
of it.
|
||||
|
||||
## One task per brief
|
||||
|
||||
Keep each brief to a single, bounded job. "Review this, fix what you find, update the docs, and
|
||||
suggest a roadmap" produces a muddled run; split it into separate dispatches. One brief → one Codex
|
||||
run → one commit keeps review and rollback clean, and lets a later task assume the earlier one landed.
|
||||
|
||||
## Expect environment preamble in the reply
|
||||
|
||||
Codex's final message may carry environment noise on top of your requested report — a memory-tool
|
||||
status line, an `AGENTS.md`-injected banner, etc. That's Codex's local setup, not a relay defect. The
|
||||
`<structured_output_contract>` is your defense: ask for a clearly delimited report section so you can
|
||||
find the real output regardless of what wraps it.
|
||||
|
||||
## A worked example
|
||||
|
||||
```xml
|
||||
<task>
|
||||
In the payments service at services/billing/, the refund path double-charges when a refund is retried
|
||||
after a network timeout (the idempotency key isn't checked before re-submitting). Make the refund
|
||||
submission idempotent: check for an existing refund by idempotency key before creating a new one.
|
||||
Touch only services/billing/refund.py and its tests. Leave the charge path, the API routes, and the
|
||||
data models untouched.
|
||||
</task>
|
||||
|
||||
<verification_loop>
|
||||
Run and make green before finishing:
|
||||
pytest tests/billing/ -q
|
||||
ruff check services/billing/
|
||||
Confirm git status shows only refund.py and its test file changed.
|
||||
</verification_loop>
|
||||
|
||||
<action_safety>
|
||||
Scope strictly to the refund idempotency fix. No unrelated refactors. Do NOT git add or commit; leave
|
||||
changes in the working tree for review.
|
||||
</action_safety>
|
||||
|
||||
<structured_output_contract>
|
||||
Report: (1) the root cause and your fix, (2) files touched, (3) pytest + ruff outcomes with counts,
|
||||
(4) anything you left open or want decided.
|
||||
</structured_output_contract>
|
||||
```
|
||||
|
||||
Send this with `relay.mjs` (see [dispatch-and-poll.md](dispatch-and-poll.md)); review the result and
|
||||
commit it yourself (see [review-and-land.md](review-and-land.md)).
|
||||
@@ -0,0 +1,316 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* delegate-skills · codex-delegate · relay.mjs
|
||||
*
|
||||
* Dispatch a self-contained brief to the OpenAI Codex CLI (`codex exec`),
|
||||
* capture the run, and write a structured result the orchestrating agent can
|
||||
* review. The orchestrator runs this one command and reads the result JSON —
|
||||
* every Codex-specific mechanic lives in here, which keeps the skill
|
||||
* orchestrator-agnostic. Verified on Claude Code; other shell-capable agents
|
||||
* (OpenCode, Cursor, …) are designed-for but not yet verified.
|
||||
*
|
||||
* Trust posture: relay.mjs itself makes no network calls, reads or writes no
|
||||
* credentials, and sends no telemetry; it has no dependencies (Node built-ins
|
||||
* only). It shells out only to `codex` and `git`. The `codex` process it
|
||||
* launches does authenticate — exactly as you do at the terminal. Read this
|
||||
* file before you run it.
|
||||
*
|
||||
* It deliberately does NOT commit. Whether Codex's sandbox can write `.git`
|
||||
* varies by Codex version, OS, and execution path, so committing is always the
|
||||
* orchestrator's job — after it reviews the diff and re-runs the project gates.
|
||||
*
|
||||
* Usage:
|
||||
* node relay.mjs --brief <file> [options]
|
||||
* cat brief.txt | node relay.mjs [options]
|
||||
*
|
||||
* Options:
|
||||
* --brief <file> Path to the brief. If omitted, the brief is read from stdin.
|
||||
* --cd <dir> Working root for Codex (default: current directory).
|
||||
* --model <name> Codex model (default: Codex's own configured default).
|
||||
* --sandbox <mode> read-only | workspace-write | danger-full-access
|
||||
* (default: workspace-write).
|
||||
* --read-only Shortcut for --sandbox read-only (review/diagnosis, no edits).
|
||||
* --resume-last Continue the most recent Codex session; send only the delta brief.
|
||||
* (Inherits the original session's sandbox and working root.)
|
||||
* --skip-git-repo-check Allow running outside a git repository.
|
||||
* --out-dir <dir> Where to write run artifacts (default: a fresh dir under
|
||||
* the system temp dir, so the repo under review stays clean).
|
||||
* -h, --help Show this help.
|
||||
*
|
||||
* Result: written to <out-dir>/result.json and summarized on stdout —
|
||||
* status, exitCode, codexVersion, threadId (for a later resume), finalMessage
|
||||
* (Codex's own report), touchedFiles (git porcelain), and the paths to
|
||||
* events.jsonl and final.txt.
|
||||
*
|
||||
* Exit codes: a pre-run usage error (bad/missing args, empty brief) exits 2
|
||||
* before any run and writes no result file; a missing `codex` binary exits 127;
|
||||
* otherwise the exit code mirrors Codex's own (0 success, non-zero failure).
|
||||
* Once the brief validates, `result.json` is written on every outcome —
|
||||
* completed, failed, or codex_unavailable. An orchestrator that polls for the
|
||||
* file must therefore also treat a non-zero exit with no file as a usage error.
|
||||
*/
|
||||
|
||||
import { spawn, execFileSync } from "node:child_process";
|
||||
import { mkdirSync, writeFileSync, readFileSync, existsSync, appendFileSync } from "node:fs";
|
||||
import { join, resolve, basename } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const SANDBOX_MODES = new Set(["read-only", "workspace-write", "danger-full-access"]);
|
||||
|
||||
function fail(message, code = 2) {
|
||||
process.stderr.write(`relay: ${message}\n`);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const opts = {
|
||||
brief: null,
|
||||
cd: process.cwd(),
|
||||
model: null,
|
||||
sandbox: "workspace-write",
|
||||
resumeLast: false,
|
||||
skipGitRepoCheck: false,
|
||||
outDir: null,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
const next = () => {
|
||||
const value = argv[i + 1];
|
||||
if (value === undefined) fail(`${arg} requires a value`);
|
||||
i += 1;
|
||||
return value;
|
||||
};
|
||||
switch (arg) {
|
||||
case "-h":
|
||||
case "--help":
|
||||
process.stdout.write(headerComment());
|
||||
process.exit(0);
|
||||
break;
|
||||
case "--brief": opts.brief = next(); break;
|
||||
case "--cd": opts.cd = resolve(next()); break;
|
||||
case "--model": opts.model = next(); break;
|
||||
case "--sandbox": opts.sandbox = next(); break;
|
||||
case "--read-only": opts.sandbox = "read-only"; break;
|
||||
case "--resume-last": opts.resumeLast = true; break;
|
||||
case "--skip-git-repo-check": opts.skipGitRepoCheck = true; break;
|
||||
case "--out-dir": opts.outDir = resolve(next()); break;
|
||||
default:
|
||||
fail(`unknown option: ${arg}`);
|
||||
}
|
||||
}
|
||||
if (!SANDBOX_MODES.has(opts.sandbox)) {
|
||||
fail(`invalid --sandbox "${opts.sandbox}" (expected: ${[...SANDBOX_MODES].join(", ")})`);
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
function headerComment() {
|
||||
// The leading block comment doubles as --help text.
|
||||
const src = readFileSync(new URL(import.meta.url), "utf8");
|
||||
const match = src.match(/\/\*\*([\s\S]*?)\*\//);
|
||||
if (!match) return "relay.mjs — dispatch a brief to codex exec\n";
|
||||
return match[1].replace(/^\s*\* ?/gm, "").trim() + "\n";
|
||||
}
|
||||
|
||||
function readBrief(opts) {
|
||||
if (opts.brief) {
|
||||
if (!existsSync(opts.brief)) fail(`brief file not found: ${opts.brief}`);
|
||||
return readFileSync(opts.brief, "utf8");
|
||||
}
|
||||
// No --brief: read from stdin (fd 0). Empty stdin is an error.
|
||||
let stdin = "";
|
||||
try {
|
||||
stdin = readFileSync(0, "utf8");
|
||||
} catch {
|
||||
stdin = "";
|
||||
}
|
||||
return stdin;
|
||||
}
|
||||
|
||||
function codexVersion() {
|
||||
try {
|
||||
return execFileSync("codex", ["--version"], { encoding: "utf8" }).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function gitTouchedFiles(cwd) {
|
||||
try {
|
||||
const out = execFileSync("git", ["status", "--porcelain"], { cwd, encoding: "utf8" });
|
||||
return out.split("\n").map((line) => line.trimEnd()).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function timestamp() {
|
||||
// Local script (not a workflow): Date is available and fine here.
|
||||
return new Date().toISOString().replace(/[:.]/g, "-");
|
||||
}
|
||||
|
||||
function buildArgv(opts, finalPath) {
|
||||
const argv = ["exec"];
|
||||
if (opts.resumeLast) argv.push("resume", "--last");
|
||||
argv.push("--json", "-o", finalPath);
|
||||
// `-s`/`-C` are not accepted by `exec resume`; resume inherits the original
|
||||
// session's sandbox and working root, and we set the child process cwd below.
|
||||
if (!opts.resumeLast) {
|
||||
argv.push("-s", opts.sandbox);
|
||||
}
|
||||
if (opts.model) argv.push("-m", opts.model);
|
||||
if (opts.skipGitRepoCheck) argv.push("--skip-git-repo-check");
|
||||
argv.push("-"); // read the prompt from stdin
|
||||
return argv;
|
||||
}
|
||||
|
||||
function extractThreadId(event) {
|
||||
return (
|
||||
event.thread_id ??
|
||||
event.threadId ??
|
||||
(event.thread && (event.thread.thread_id ?? event.thread.id)) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const opts = parseArgs(process.argv.slice(2));
|
||||
const brief = readBrief(opts);
|
||||
if (!brief.trim()) fail("empty brief (pass --brief <file> or pipe the brief on stdin)");
|
||||
|
||||
const version = codexVersion();
|
||||
const startedAt = new Date().toISOString();
|
||||
// Default the run dir to system temp so the repo under review stays pristine —
|
||||
// the touched-files report must show only Codex's edits, not relay's artifacts.
|
||||
const outDir = opts.outDir || join(tmpdir(), "delegate-relay", `${basename(opts.cd) || "repo"}-${timestamp()}`);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const eventsPath = join(outDir, "events.jsonl");
|
||||
const finalPath = join(outDir, "final.txt");
|
||||
const briefPath = join(outDir, "brief.txt");
|
||||
const resultPath = join(outDir, "result.json");
|
||||
writeFileSync(briefPath, brief, "utf8");
|
||||
writeFileSync(eventsPath, "", "utf8");
|
||||
|
||||
const writeResult = (extra) => {
|
||||
const finishedAt = new Date().toISOString();
|
||||
const result = {
|
||||
schema: "delegate-relay.result.v1",
|
||||
workdir: opts.cd,
|
||||
sandbox: opts.resumeLast ? "(inherited from resumed session)" : opts.sandbox,
|
||||
model: opts.model,
|
||||
resumeLast: opts.resumeLast,
|
||||
codexVersion: version,
|
||||
startedAt,
|
||||
finishedAt,
|
||||
briefPath,
|
||||
eventsPath,
|
||||
finalPath: existsSync(finalPath) ? finalPath : null,
|
||||
...extra,
|
||||
};
|
||||
writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
||||
return result;
|
||||
};
|
||||
|
||||
if (!version) {
|
||||
const result = writeResult({ status: "codex_unavailable", exitCode: 127, threadId: null, finalMessage: "", touchedFiles: [] });
|
||||
printSummary(result, resultPath);
|
||||
process.stderr.write("relay: `codex` not found on PATH. Install it (npm i -g @openai/codex) and run `codex login`.\n");
|
||||
process.exit(127);
|
||||
}
|
||||
|
||||
const argv = buildArgv(opts, finalPath);
|
||||
const child = spawn("codex", argv, { cwd: opts.cd, stdio: ["pipe", "pipe", "pipe"] });
|
||||
|
||||
let threadId = null;
|
||||
let stdoutBuf = "";
|
||||
const stderrTail = [];
|
||||
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdoutBuf += chunk.toString();
|
||||
let nl;
|
||||
while ((nl = stdoutBuf.indexOf("\n")) !== -1) {
|
||||
const line = stdoutBuf.slice(0, nl);
|
||||
stdoutBuf = stdoutBuf.slice(nl + 1);
|
||||
if (!line.trim()) continue;
|
||||
appendFileSync(eventsPath, `${line}\n`, "utf8");
|
||||
try {
|
||||
const event = JSON.parse(line);
|
||||
const tid = extractThreadId(event);
|
||||
if (tid) threadId = tid;
|
||||
} catch {
|
||||
// Non-JSON progress line; it is preserved in events.jsonl regardless.
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on("data", (chunk) => {
|
||||
const text = chunk.toString();
|
||||
process.stderr.write(text); // surface Codex progress live for the orchestrator
|
||||
for (const line of text.split("\n")) {
|
||||
if (line.trim()) stderrTail.push(line.trimEnd());
|
||||
}
|
||||
while (stderrTail.length > 20) stderrTail.shift();
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
const result = writeResult({ status: "failed", exitCode: 1, threadId, finalMessage: "", touchedFiles: gitTouchedFiles(opts.cd), error: String(err && err.message ? err.message : err) });
|
||||
printSummary(result, resultPath);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (stdoutBuf.trim()) {
|
||||
appendFileSync(eventsPath, `${stdoutBuf}\n`, "utf8");
|
||||
try {
|
||||
const tid = extractThreadId(JSON.parse(stdoutBuf));
|
||||
if (tid) threadId = tid;
|
||||
} catch {
|
||||
// A newline-less final line that isn't valid JSON; preserved in the log only.
|
||||
}
|
||||
}
|
||||
const finalMessage = existsSync(finalPath) ? readFileSync(finalPath, "utf8").trim() : "";
|
||||
const result = writeResult({
|
||||
status: code === 0 ? "completed" : "failed",
|
||||
exitCode: code === null ? 1 : code,
|
||||
threadId,
|
||||
finalMessage,
|
||||
touchedFiles: gitTouchedFiles(opts.cd),
|
||||
...(code === 0 ? {} : { stderrTail: stderrTail.slice(-20) }),
|
||||
});
|
||||
printSummary(result, resultPath);
|
||||
process.exit(result.exitCode);
|
||||
});
|
||||
|
||||
// If the child failed to launch, writing to its stdin can emit a stray 'error'
|
||||
// on the pipe; the 'error' handler above owns that outcome, so swallow it here.
|
||||
child.stdin.on("error", () => {});
|
||||
child.stdin.write(brief);
|
||||
child.stdin.end();
|
||||
}
|
||||
|
||||
function printSummary(result, resultPath) {
|
||||
const lines = [];
|
||||
lines.push("");
|
||||
lines.push(`relay: ${result.status} (exit ${result.exitCode}) · codex ${result.codexVersion ?? "?"}`);
|
||||
if (result.resumeLast) lines.push("mode: resumed most recent session");
|
||||
if (result.threadId) lines.push(`thread id (resume with: codex exec resume ${result.threadId}): ${result.threadId}`);
|
||||
const touched = result.touchedFiles || [];
|
||||
lines.push(`touched files: ${touched.length}`);
|
||||
for (const file of touched.slice(0, 40)) lines.push(` ${file}`);
|
||||
if (touched.length > 40) lines.push(` … and ${touched.length - 40} more`);
|
||||
if (result.stderrTail && result.stderrTail.length) {
|
||||
lines.push("last stderr:");
|
||||
for (const line of result.stderrTail.slice(-8)) lines.push(` ${line}`);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push("--- codex final report ---");
|
||||
lines.push(result.finalMessage || "(no final message captured)");
|
||||
lines.push("--- end report ---");
|
||||
lines.push("");
|
||||
lines.push(`result: ${resultPath}`);
|
||||
lines.push("relay does not commit. Review the diff, re-run the project gates yourself, then commit from the orchestrator.");
|
||||
process.stdout.write(`${lines.join("\n")}\n`);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user