Skip to main content

DevRelay

Plan with one model, build with Codex, review with another - without copy-pasting between agents.

Supported release: v0.1.0 (see CHANGELOG.md; the CLI reports the same version via devrelay version). Source: https://github.com/YingQiu0871/DevRelay - Issues

DevRelay is a local AI coding orchestrator (CLI) that automates the mechanical handoffs of a multi-model development workflow:

PLAN -> IMPLEMENT -> TEST -> REVIEW -> FIX(loop) -> FINAL_GATE -> DONE

Today most people run this pipeline by copying text between chat windows: ChatGPT plans, Codex implements, another model reviews, someone pastes the findings back into Codex, repeat. DevRelay makes the handoffs structured and persistent: every stage writes human-readable artifacts and a machine-readable state file, so the pipeline can be paused, resumed, audited and reviewed without losing context - and previously approved scope stays closed.

DevRelay is project-agnostic (it does not know or care about any specific codebase) and does not burn subscription quota on re-reviewing the whole repository every round: reviewers receive the current Task Packet, the task-relative delta, test evidence and previous findings - not a repository dump.

DevRelay can start from a dirty Git workspace without confusing pre-existing user changes with agent changes. That is the v0.1-RC core capability: a durable Task Baseline is captured at task start, and every later diff is computed against that baseline - never blindly against HEAD.

Why

  • Stop copy-pasting prompts, diffs and review findings between agents.
  • Keep the state machine in the repo (.devrelay/), not in a chat context that disappears.
  • Enforce a bounded fix loop (default 3 iterations) - never an infinite agent loop.
  • Make reviews structural: P0/P1/P2 findings are blocking by policy; P3-only reviews pass by default; decisions come from the policy, not from natural language.
  • Start tasks in dirty workspaces: pre-existing user edits, staged changes, deletions, renames, untracked files and binary files are baselined, so reviewers only see the current task's delta.
  • Save expensive "re-scan everything" work: current delta first, previously approved scope stays closed unless the current delta can affect it.

Example: dirty workspace in, clean task delta out

Before DevRelay:
  A.py contains uncommitted user work ("USER_CHANGE").

  devrelay start "Fix widget"
  Codex modifies A.py further and creates B.py.

  Reviewer receives ONLY:
    Codex's delta to A.py  (+AGENT_CHANGE)
    + B.py

  ... NOT the user's pre-existing A.py modifications.
@@ -1,3 +1,4 @@
  def widget():            # <- context lines may be shown
      user_work()          # <- never appears as an added (+) line
+     agent_fix()

Architecture

src/devrelay/
├── cli.py                 # Typer CLI (devrelay ...)
├── config.py              # YAML profiles -> validated Pydantic config
├── models.py              # TaskPacket / Finding / ReviewResult / TaskRun,
│                          #   WorkspaceBaseline / PolicyViolation / TaskDelta
├── errors.py              # exception hierarchy
├── redact.py              # secret redaction for logs/artifacts
├── baseline/
│   ├── store.py           # durable baseline artifacts (json/patch/blobs)
│   ├── capture.py         # temporary-GIT_INDEX_FILE tree building
│   ├── comparator.py      # task-relative diff + tree reconstruction
│   ├── guard.py           # repository policy postcondition checks
│   └── service.py         # facade used by the engine/CLI
├── pipeline/
│   ├── states.py          # state machine (enum + transition table)
│   ├── policy.py          # config-driven decisions (severities, caps)
│   └── engine.py          # orchestrator: stages, artifacts, persistence
├── providers/
│   ├── base.py            # ImplementerProvider / ReviewerProvider
│   ├── codex_cli.py       # official Codex CLI adapter (stdin transport)
│   ├── manual.py          # manual reviewer handoff (markdown)
│   └── openai_compatible.py  # DeepSeek-style HTTP reviewer
├── workspace/
│   ├── runner.py          # subprocess runner (shell=False, UTF-8, timeouts)
│   ├── git.py             # GitWorkspace (read-only git access)
│   └── discovery.py       # workspace root discovery
└── artifacts/
    ├── store.py           # .devrelay artifact store (atomic JSON/markdown)
    └── renderer.py        # task.md / review requests / final report

The pipeline is a state machine with real enum states (NEW, PLAN_REQUIRED, PLAN_READY, IMPLEMENTING, TESTING, REVIEWING, FIX_REQUIRED, FIXING, FINAL_GATE_REQUIRED, BLOCKED, DONE, FAILED). Every transition is validated, audited and persisted before the next stage starts - if DevRelay is killed mid-task, devrelay status / devrelay continue resumes from the persisted state.

Task Baseline (RC core)

When devrelay start "..." runs, DevRelay captures the real task-start workspace state before any provider work:

  • HEAD SHA, branch, real-index tree, git status --porcelain=v2 output;
  • pre-existing tracked changes (staged + unstaged, deletions, renames, binaries) as a HEAD -> worktree binary-capable patch;
  • pre-existing non-ignored untracked files as raw content copies with mode/symlink metadata;
  • a worktree tree (HEAD + tracked edits + untracked files) built in a temporary index - stored as baseline_worktree_tree_sha.

Ignored files never enter the baseline; .devrelay/ itself is always excluded. Per-file size limits are enforced: files above baseline.max_untracked_file_bytes fail task creation with an explicit message (file, size, limit) - never a silent skip.

Temporary index strategy - the real index is never modified

Baseline and delta computation run git with GIT_INDEX_FILE=<temp>: git read-tree HEAD, git add -A -- . (excluding .devrelay) and git write-tree all happen against a throwaway index. Index-reading commands that can opportunistically refresh stat data (git status, git diff, git write-tree) run against a clone of the real index under the same temporary mechanism, so even stat refreshes never touch .git/index. DevRelay never runs git add/reset/checkout/stash/commit against the real index. Tests prove .git/index bytes are unchanged across capture and delta computation, and that no temporary index files are left behind.

Binary-exact patch transport

tracked.patch is binary-sensitive durable data, so it never travels through a text-mode pipe: git diff --binary output is captured as raw bytes, stored verbatim (raw git diff bytes == stored tracked.patch bytes, byte-for-byte), and fed back to git apply --cached --binary - as raw bytes. No universal-newline translation and no implicit encode/decode can occur on that path (on Windows, text-mode stdin would otherwise rewrite \n to \r\n and silently corrupt the patch). Repositories whose repo-form content contains CRLF are covered by dedicated fidelity tests.

Durable persistence - no reliance on unreachable trees

A tree SHA written by git write-tree is only a fast path; if git garbage collects it, the baseline must survive. Every task therefore stores durable artifacts:

.devrelay/tasks/DR-0001/
├── task.json  task.md  state.json  config.snapshot.yaml
├── baseline/
│   ├── baseline.json      # validated WorkspaceBaseline (machine truth)
│   ├── manifest.json      # entries + dirty/untracked lists
│   ├── tracked.patch      # HEAD -> task-start worktree (--binary, renames)
│   └── files/             # raw copies of pre-existing untracked files
├── implementation/iteration-01.md ...
├── reviews/review-01-request.md  review-01-result.json ...
├── logs/codex-01.stdout.log  test-01-01.log ...
├── policy/attempt-01.json  pre-run-01.json  post-run-01.json
│            violations-01.json ...
└── final/final-review-request.md  report.md

BaselineStore.load() validates every artifact; corruption raises BaselineCorruptError ("DevRelay does not guess"). If the recorded tree object is gone, the tree is reconstructed from baseline.head_sha + tracked.patch + saved untracked blobs in another temporary index, and its SHA is verified against the recorded one before any diff is produced. The recovery path is regression-tested against a real git gc --prune=now for pre-existing tracked modifications, deletions, renames, binary changes, staged new files, staged+unstaged mixes and CRLF content - and it still fails closed (never a HEAD fallback) when an artifact is corrupt.

Task-relative delta

Reviewers, implementation reports, devrelay diff and the final report all use:

task-start baseline tree  ->  current worktree tree

Outputs: text diff, rename-aware name-status, name-only, stat, changed-file list, and a binary-safe patch (devrelay diff --binary --out task.patch). Binary files are detected from the tree diff and listed separately; raw content is never force-decoded as text. The review request and final report explicitly separate:

  • PRE-EXISTING workspace changes (baseline, listed by name; they are never attributed as task additions) from
  • TASK changes (the true agent delta).

A unified diff necessarily shows surrounding context lines, so pre-existing content can appear unmarked as context; it never appears as an added (+) task modification.

Legacy tasks created before baselines have baseline: null and an explicit state_schema_version guard: DevRelay refuses to guess a diff for them and reports baseline_error (BLOCKED for pipeline paths, clear error for CLI commands). Create a new task to obtain precise deltas.

Repository policy guard (post-run)

Prompts tell Codex not to commit/push/tag/switch/reset and to preserve unrelated user changes - but prompts are not guarantees. Every Codex pass is therefore wrapped:

PRE SNAPSHOT (HEAD, branch, tags, refs, real-index tree)
   -> provider run
POST SNAPSHOT
   -> structured PolicyViolation records -> BLOCKED on violation
Check Enforced locally Outcome
HEAD changed yes (git.allow_commit=false) BLOCKED, never auto-reset
branch switched yes BLOCKED
tags created/deleted/moved yes (git.allow_tag=false) BLOCKED
other local refs mutated yes BLOCKED
real git index staging mutated yes (git.allow_index_mutation=false) BLOCKED, never auto-unstage
index state cannot be captured reliably (unresolved merge conflict, unreadable index) yes - fail closed BLOCKED before the provider runs (pre-check) or after the run (post-check); never treated as "no change"
push performed BEST-EFFORT / PROVIDER-LIMITED see below

Violations are structured (id/type/severity/before/after/description), persisted under policy/ and in state.json, and survive restarts. DevRelay never automatically repairs repository state; the human decides and may use devrelay unblock --reason ....

Interrupted provider attempts (fail closed)

Every implement/fix run is a persisted attempt (policy/attempt-NN.json). The PRE snapshot and an in-flight crash marker are written before the provider starts, and the POST snapshot + policy guard run for every outcome - success, non-zero exit, timeout, KeyboardInterrupt or any Python exception. Consequences:

  • A provider that raises after modifying the workspace still gets POST evidence and policy verification; a policy violation takes priority over the provider error, and the task goes BLOCKED (no auto-repair of the repo).
  • If an attempt raised without policy violations, the task is BLOCKED as interrupted_attempt: the workspace may have been modified, so a human must acknowledge with devrelay unblock --reason "..." before DevRelay will run a provider again.
  • If the process died hard (kill -9 / power loss) the in-flight marker stays persisted; on restart DevRelay refuses to auto-rerun the provider and reports "Previous provider attempt may have modified the workspace. Manual acknowledgement is required."

Repositories with unresolved merge conflicts must be resolved (or the index made readable) before an automated provider run: DevRelay blocks because it cannot guarantee index-mutation detection. Baseline capture itself still works in such repos.

Push enforcement limitation (stated honestly): a local postcondition check cannot mathematically prove that no git push occurred. The installed Codex CLI (0.150.1) exposes sandbox modes (--sandbox read-only | workspace-write | danger-full-access) but no network-disable option. With git.allow_push=false DevRelay therefore enforces the prompt and the sandbox it configures, and documents this as BEST-EFFORT - it does not invent a guarantee it cannot verify.

Codex CLI mode (no OpenAI API key)

DevRelay invokes the official Codex CLI; the Codex CLI may be authenticated using the user's own ChatGPT subscription login:

  • DevRelay does not read or manage ChatGPT credentials - no cookie grabbing, no session tokens, no web automation, no undocumented auth.
  • No OpenAI API key is required for the Codex CLI execution path.
  • You are responsible for installing and authenticating the official Codex CLI (codex --version, codex login).
  • Codex usage is governed by your own Codex/ChatGPT plan and limits; DevRelay makes no claim about how much usage any plan provides, and it never retries a failed real invocation on its own - a failed or interrupted attempt is surfaced as BLOCKED and requires explicit human acknowledgement.

Prompt transport: stdin is canonical

Verified against the installed CLI (codex exec --help, codex-cli 0.150.1):

If not provided as an argument (or if - is used), instructions are read from stdin. If stdin is piped and a prompt is also provided, stdin is appended as a <stdin> block.

DevRelay therefore streams the full Task Packet + findings + constraints on stdin by default (never in argv - Windows argv length limits and shell interpretation never apply). The stdin bytes are written byte-exactly (LF stays LF; no text-mode \r\n translation). argv survives only as an explicit compatibility fallback (codex.prompt_mode: argv).

Important: this CLI version does not offer --full-auto; the shipped default is the documented sandbox invocation:

codex:
  executable: codex
  args: ["exec", "--sandbox", "workspace-write"]
  prompt_mode: stdin
  timeout_seconds: 1800

devrelay doctor verifies the configured codex.args against the installed CLI's codex exec --help and rejects unknown flags before any run starts. Sandbox/approval semantics (e.g. whether --approve-for-me or a danger-full-access policy is needed for your repo) are provider-runtime behavior to verify on your machine with a no-op task; DevRelay refuses to pretend otherwise.

Quick start

Requirements: Python >= 3.11, git, and a repository with at least one commit (baselines snapshot HEAD; DevRelay reports a clear error instead of a Git error when the repository has no commits yet).

DevRelay v0.1.0 is distributed from source (a PyPI release is planned):

# from a checkout or an unpacked sdist/wheel
python -m pip install .
# or isolated CLI install
pipx install .

Build the wheel/sdist yourself with python -m build (dev extra: python -m pip install -e ".[dev]").

cd your-project
devrelay init                 # creates .devrelay/config.yaml (never overwrites)
devrelay start "Fix Wear tile rendering"     # -> DR-0001, state PLAN_REQUIRED

Even when your working tree is dirty (uncommitted user work, staged changes, untracked scratch files), start captures the baseline and proceeds.

devrelay plan show            # show the current task.md
devrelay plan import task.md  # or task.json -> state PLAN_READY
devrelay continue             # run the next pipeline stage(s)
devrelay status               # Planner/Implementer/Tests/Reviewer/Final Gate/Baseline
devrelay diff                 # TASK-RELATIVE diff (baseline -> now)
devrelay diff --binary --out task.patch   # binary-safe task patch
devrelay logs                 # codex + test logs for the current task

If start fails because a pre-existing file exceeds baseline.max_untracked_file_bytes, the error names the file, its size and the config knob - nothing is silently dropped.

Manual mode & example workflow

The full manual workflow (no API reviewer configured):

devrelay start "Add PK chart interaction"          # DR-0001 (baseline captured)
devrelay plan import task.md                        # PLAN_READY
devrelay continue                                   # Codex implements, tests run,
                                                    # stops at REVIEWING (manual)
# reviewer (human or another model) reads reviews/review-01-request.md
devrelay review import review.json                  # PASS -> FINAL_GATE_REQUIRED
                                                    # or blocking finding -> FIX_REQUIRED
devrelay final export                               # final gate package (task-relative)
devrelay final approve --note "verified on device"  # DONE + final/report.md

review.json shape:

{
  "decision": "REQUEST_CHANGES",
  "summary": "one real bug, one nit",
  "findings": [
    {"severity": "P2", "title": "...", "description": "...",
     "files": ["src/x.py"], "evidence": "...", "recommendation": "..."}
  ]
}

If a fix round is required, devrelay continue embeds the findings into the next Codex prompt automatically. After max_fix_iterations (default 3) blocking rounds the task goes BLOCKED; devrelay unblock --reason "..." is the only, audited way to override it - it never silently skips findings.

Reviewer configuration

Reviewers never modify code. v0.1 ships two providers:

  1. Manual (default) - DevRelay stops at REVIEWING and writes reviews/review-01-request.md containing the task-relative delta (the request header states the diff scope explicitly).
  2. OpenAI-compatible HTTP reviewer (DeepSeek and friends) via env:
export DEVRELAY_REVIEWER_BASE_URL=https://api.deepseek.com/v1
export DEVRELAY_REVIEWER_API_KEY=...   # required; never stored
export DEVRELAY_REVIEWER_MODEL=deepseek-chat
review:
  provider: openai_compatible
  blocking_severities: [P0, P1, P2]
  retries: 1                    # invalid-JSON retries before BLOCKED
  max_diff_bytes: 300000        # oversized diffs are truncated + flagged

The reviewer must answer with a single JSON object matching the ReviewResult schema. Output is schema-validated; invalid JSON is retried and never guessed - persistent failure routes the task to BLOCKED.

Policy (default profile)

pipeline:
  max_fix_iterations: 3
  auto_fix_tests: false
review:
  provider: manual
  blocking_severities: [P0, P1, P2]
  retries: 1
  max_diff_bytes: 300000
baseline:
  max_untracked_file_bytes: 52428800   # per-file cap for untracked copies
tests:
  targeted: []              # project commands, e.g. ["./gradlew test"]
  full: []
  full_regression: release_only
scope:
  reuse_approved_scope: true
  current_delta_first: true
git:
  allow_commit: false       # v0.1 hard-disables commit/push/tag/reset and
  allow_push: false         #   any real-index mutation: setting any of
  allow_tag: false          #   these true is a CONFIG ERROR
  allow_reset_hard: false
  allow_index_mutation: false
codex:
  executable: codex
  args: ["exec", "--sandbox", "workspace-write"]
  prompt_mode: stdin        # canonical; argv is an explicit fallback
  timeout_seconds: 1800

The policy is enforced, not decorative: severity lists drive pass/fix decisions, caps drive the BLOCKED state, test commands drive the TESTING stage, and any git.allow_*: true is rejected at config load. Test commands run with shell=False and timeouts; every execution result is saved under logs/.

Security model

  • No credential handling: Codex uses the user's own login; reviewer keys come from environment variables only.
  • Redaction is best-effort, not DLP: secret-shaped values (sk-..., Bearer ..., Authorization: ..., API_KEY=..., token=..., and values of secret-looking environment variables) are pattern-redacted from logs, from everything a reviewer receives, and from the manual review-request artifact. This is pattern-based protection applied at a single sanitize_review_bundle() boundary shared by the remote and manual reviewer paths - it is not a full data-loss-prevention system and cannot guarantee that no secret ever leaves the machine (the task diff is your own code).
  • No shell execution of reviewer output or agent reports: commands come exclusively from the validated profile; all subprocesses use argv lists (shell=False) with byte-exact or explicitly decoded pipes.
  • No destructive git: DevRelay never commits, pushes, merges, tags, resets, or touches the user's real index. Repository control state is snapshotted before/after every provider run and violations BLOCK the task; nothing is auto-repaired.
  • Reviewers only see the task delta: Task Packet + task-relative diff + changed files + test evidence + reports (size capped), never the full repository. Pre-existing changes are never attributed as task additions; unified-diff context lines may still show surrounding pre-existing content.
  • Workspace boundary: tasks operate only inside the initialized workspace.
  • Bounded automation: fix loops capped, no auto git actions, BLOCKED requires an explicit human unblock with a reason (including interrupted provider attempts).
  • Provider limits are stated, not hidden: push prohibition is BEST-EFFORT/PROVIDER-LIMITED and documented as such.

Current limitations (v0.1)

  • One implementer: official Codex CLI (stdin transport). Codex argv/sandbox behavior must be validated on your machine (DevRelay rejects flags the installed CLI does not document).
  • Codex structured event output (--json / --output-schema) is not parsed in v0.1 - recorded on the v0.2 roadmap. Exit codes and full stdout are captured today.
  • One active task at a time; no concurrency.
  • Repositories with unresolved merge conflicts are refused by the policy guard (fail closed) until the index is resolvable again; baseline capture itself still works in such repos.
  • Real-git index byte-immutability is guaranteed for baseline/delta paths (tested); other git read commands use --no-optional-locks where possible.
  • Secret redaction is best-effort pattern matching (see Security model), not a DLP guarantee.
  • Test commands are profile-configured whole commands; no per-language auto-detection.
  • OpenAI-compatible reviewer requires a JSON-object-capable endpoint.
  • Push prohibition cannot be mathematically enforced with the current Codex CLI (no network-disable flag) - see the policy-guard section.
  • No ChatGPT plugin / MCP / web UI / GitHub app yet - see Roadmap.

Roadmap

  • v0.1 - local orchestration core with task baselines, task-relative deltas and repository policy guards (this release)
  • v0.2 - structured Codex event output (codex exec --json), improved provider adapters / project profiles / state-driven UI
  • v0.3 - MCP server
  • v0.4 - ChatGPT integration / plugin
  • v0.5 - optional release automation with explicit approvals

Development

python -m pip install -e ".[dev]"
python -m pytest

142 tests run fully offline with mocked subprocesses/HTTP where needed and real (tiny) git repositories for baseline/guard/recovery coverage - no real Codex, DeepSeek or network required. Coverage includes: state transitions and rejected invalid transitions; max-iteration blocking; P0/P1/P2 -> FIX_REQUIRED; P3-only pass policy; Codex missing/success/failure/timeout; stdin transport (long/Unicode/shell-metacharacter prompts stay on stdin, and stdin/stdout are byte-exact - no newline translation); config parsing and invalid config; artifact persistence and resume; dirty workspace baselines (same-file user+agent edits, untracked, staged + staged/unstaged mixes, deletions, renames, binary, Unicode and space filenames, ignored files); byte-faithful tracked.patch (LF, CRLF and binary); baseline tree reconstruction after a real git gc --prune=now for tracked modify/delete/rename/binary/staged/CRLF changes; manifest and patch corruption as explicit errors (never a HEAD fallback); real-index immutability; temp-index cleanup; HEAD/branch/tag/index-mutation policy violations -> BLOCKED without auto-repair; conflicted-index fail-closed (blocked before the provider runs); provider exception / KeyboardInterrupt still persisting post-run evidence and blocking auto-rerun after restart; reviewer payload and manual artifact redaction; review request and final report separation of PRE-EXISTING vs TASK changes; shell safety.

License

MIT - see LICENSE.

Metadata

Release files for devrelay 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for devrelay 0.1.0
File Size Uploaded
devrelay-0.1.0.tar.gz 107.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for devrelay 0.1.0
File Interpreter ABI Platform
devrelay-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 182.1 kB

Release files / devrelay-0.1.0.tar.gz

Download URL devrelay-0.1.0.tar.gz
Size 107.3 kB
Tags Source
SHA-256 checksum
How to use checksums
aea142a7975d2d9f651b94db252547be23d91742a862df0aef9848d9409da005
BLAKE2b-256 checksum
How to use checksums
6c57ca5a354b799e1de25e28227bc677a7206da7b1a9dc351fa40b99cf58d6c2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / devrelay-0.1.0-py3-none-any.whl

Download URL devrelay-0.1.0-py3-none-any.whl
Size 74.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8c9b3261455758a60eb95fa51c77100530e759eb688fc2ecd11447791e03a7c5
BLAKE2b-256 checksum
How to use checksums
16c641c59ae042e177f9fc3b52b9d3635dfde477a2e4ba2b1be1a7fee44ee4c8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page