Orchestrator CLI that drives Claude Code end-to-end to generate whole projects from a requirements.md file.
Project description
code-generator
A Python CLI that orchestrates Claude Code end-to-end to generate a whole project from a requirements.md file. Five commands — init, optimize, generate, review, status. Works for any project type: Python CLI, FastAPI, Angular, NestJS, full-stack, finance, BAML/LLM.
Max subscription only. This tool uses exclusively the Claude Max subscription. It strips every environment variable that could route a call through the API and aborts on startup if any are still present. There is no path to API credit billing — see Safety constraints below.
Install
pipx install code-generator # recommended
# or
pip install code-generator # user-site
# or, from source
git clone git@github.com:SilvioBaratto/code-generator.git
cd code-generator && pip install -e ".[dev]"
Optional: codebase graph (graphify)
Phases 0, 1, and 2 inject a knowledge-graph report from graphify for richer code orientation. When the graph is missing, the orchestrator falls back to a sentinel string and continues — nothing breaks.
Install the CLI (required to use graphify at all):
uv tool install --upgrade graphifyy # CLI binary is `graphify`
# or: pip install -U graphifyy
Requires graphify >= 0.8.1 (idempotent update, stable node-id format).
Seed the graph (one-time, per project): graphify's full LLM-driven build runs through the /graphify slash-command inside an AI assistant — not through the shell binary. From inside Claude Code (or any other graphify-supported assistant) run:
/graphify .
This produces graphify-out/graph.json and graphify-out/GRAPH_REPORT.md.
Subsequent code-generator runs will automatically refresh the graph with graphify update . — idempotent and AST-only (no LLM cost; graph.json/GRAPH_REPORT.md are only rewritten on real changes). A graph seeded by a pre-0.7.18 graphify is reseeded once automatically via graphify update . --force (still no LLM cost) and stamped. Doc / paper / image changes still need a manual /graphify . re-run inside your assistant.
Add to your project's .gitignore:
graphify-out/
Optional extras
Install feature groups with pip install "code-generator[<extra>]":
| Extra | Installs | Enables |
|---|---|---|
[mcp] |
mcp (Python client) |
MCP servers in Phases 3/4 (Codebase-Memory is a separate Rust crate; Serena resolved out-of-band if installed) |
[batch] |
anthropic |
Anthropic Messages Batches API runner |
[quality] |
bandit, semgrep |
Phase 6.4 static-analysis gate (security + reliability scanners) |
[all] |
all of the above | Full feature set (use for local dev) |
The [quality] extra is required for the Phase 6.4 static gate to scan; when bandit/semgrep are absent the gate logs a WARNING and skips (the pipeline never blocks on a missing scanner).
Authentication
# Claude Code (Max subscription)
claude auth login
# GitHub (SSH protocol) — only for the default --github backend
gh auth login -h github.com -p ssh
gh config set git_protocol ssh
GitHub auth is not required in local-only mode (code-generator generate --no-github).
Commands
code-generator init [--template TYPE] [--force]
Scaffold .code-generator/ in the current directory with a ready-to-fill requirements.md. Templates: fastapi, angular, nestjs, fullstack, python-cli, finance. Refuses to clobber an existing .code-generator/ without --force.
code-generator init --template fastapi
$EDITOR .code-generator/requirements.md
code-generator optimize [--dry-run] [--force]
Pre-flight rewrite of .code-generator/requirements.md into the canonical structure the orchestrator expects: # Title → ## Description → ## Tech Stack → ## Goals → ## Scope (numbered ### N. subsections with per-section Acceptance criteria checklists) → ## Non-goals → ## Constraints → ## Global acceptance criteria.
Runs a single Opus 4.8 session with a dedicated prompt, preserves the original intent verbatim when ambiguous (never invents new scope), and writes the rewrite back to disk atomically. The pre-optimize original is saved to .code-generator/requirements.backup-<UTC-timestamp>.md so you can always diff and recover. The command is stateless — it never reads or writes state.json and never calls gh.
Short-circuits and exits 0 if the file is already canonical. Use --force to re-optimize anyway. Use --dry-run to print the proposed rewrite to stdout without touching the file or creating a backup. An output guard rejects any response that is not a well-formed requirements document (e.g. model commentary instead of a rewrite) and aborts with an error rather than overwriting requirements.md with garbage.
code-generator optimize --dry-run # preview
code-generator optimize # commit the rewrite
code-generator generate # then run the pipeline
code-generator generate [flags]
Read .code-generator/requirements.md and orchestrate Claude Code through the 0→8 phase pipeline:
| Phase | Action | Model |
|---|---|---|
| 0 | Complexity analysis (single vs multi-cycle) | Haiku 4.5 |
| 1 | Planning — creates issues + milestone (GitHub or local store) | Opus 4.8 |
| 2 | Per-issue review | Opus 4.8 |
| 3-4 | Implementation, fresh SDK session per issue (TDD) | Sonnet 4.6 |
| 5 | Closure + cross-module review | Opus 4.8 |
| 6 | Test suite, max 3 retries on failure | Haiku 4.5 |
| 6.4 | Static-analysis gate (bandit + semgrep) — diff-scoped, ≤10-iter strict-shrink repair | Sonnet 4.6 |
| 6.5 | Runtime verification (T0→T2 tiers) + bounded repair loop | Haiku 4.5 / Sonnet 4.6 |
| 7 | Commit message + git add/commit/push | Opus 4.8 |
| 8 | Finalization + CI green-gate (skipped in local mode) | Opus 4.8 |
Every phase logs a completion summary with token counts (in, cache_read, cache_write, out) and persists them to state.json.
Flags:
| Flag | Description | Default |
|---|---|---|
--dry-run |
Run only Phase 0 + Phase 1 (planning) | false |
--phase N |
Resume from phase N | — |
--continue |
Resume from the last completed phase (reads state.json) |
false |
--mode auto|single|multi-cycle |
Force a mode (default auto runs Phase 0 first) |
auto |
--cycle N |
Resume from cycle N (multi-cycle only) | — |
--github / --no-github |
Issue backend: GitHub (needs gh auth + origin remote) or a local markdown store (no remote required) |
--github |
--commit / --no-commit |
Phase 7 commit: --commit (default) runs git add/commit each cycle (push gated separately by --github); --no-commit skips the commit entirely, leaving generated files uncommitted in the working tree |
--commit |
--max-turns N |
Opt-in per-session turn cap for debugging or strict cost control (positive integer); omit for no cap | — |
--rex-scheduler / --no-rex-scheduler |
R17: Thompson-sampling bandit for Best-of-N candidate scheduling (default-off) | --no-rex-scheduler |
--surrogate-ranker / --no-surrogate-ranker |
R22: run-free surrogate pre-filter before runtime verification (default-off) | --no-surrogate-ranker |
--dars-branching / --no-dars-branching |
R18: dynamic alternative re-sampling on stall/stuck_tier (default-off) | --no-dars-branching |
--orps-scoring / --no-orps-scoring |
R19: execution-grounded beam scoring for BoN candidates (default-off) | --no-orps-scoring |
--gvr / --no-gvr |
R20: Generate-Verify-Refine step gating via prompt-judge (default-off) | --no-gvr |
--swe-debate / --no-swe-debate |
R21: dependency-graph fault localization + bounded multi-agent debate (default-off) | --no-swe-debate |
The --github / --no-github choice persists to state.json (State.github_mode / CycleState.github_mode); --continue honors the saved mode without re-passing the flag (an explicit --github/--no-github still overrides on resume). Available on all four generate commands (generate, generate ollama, generate gemini, generate codex). See Local-only mode.
The --commit / --no-commit choice is orthogonal to --github: --no-github only skips the push; --no-commit skips the commit entirely. The choice persists to state.json (State.commit_enabled) and is honored on --continue in the same way. When --no-commit is active, Phase 8's CI green-gate is also skipped wholesale (a CI run without a pushed commit is meaningless).
Turn budgets
No per-session turn budget is set by default. Sessions terminate via end_turn on the model side; the real safety nets are rate-limit handling, overage abort (non-negotiable #4), and task budgets — not a guessed integer. --max-turns N exists on generate, optimize, and review as an opt-in for debugging or strict cost control; it is not for production.
In multi-cycle mode each cycle is a GitHub Milestone and produces a committable, working increment. Each cycle starts a fresh SDK session and re-reads the committed codebase, so the model never relies on stale conversational context.
If the Max subscription hits a rate limit, the tool pauses — it persists the session id and paused_until timestamp atomically to .code-generator/state.json, sleeps until the window resets, and resumes with --resume <session_id>. A killed process will resume on its own when re-run.
code-generator review [--create-issues] [--severity LEVEL]
Run a standalone Opus 4.8 review of the current codebase against the design principles in requirements.md §4 (SOLID, Clean Code, TDD, YAGNI/KISS/DRY). With --create-issues, every finding becomes a GitHub Issue.
code-generator status
Print a Rich table with the current mode, current cycle, current phase, open/closed issue counts, the rate-limit pause state (with minutes remaining if paused), per-cycle token consumption columns (in | cache_read | cache_write | out), wall-clock elapsed time, and any last_error highlighted in red.
Quality pipeline
Three closed-loop quality mechanisms ground the implementer and gate the commit. All run inside the existing per-cycle SDK client — no new model calls, no new credentials.
Phase 6.4 — static-analysis gate
A deterministic gate (orchestrator/phase6_4_static.py) sits between Phase 6 and Phase 6.5. It runs bandit + semgrep over the cycle diff only (files changed since base_commit, via git diff base..HEAD --name-only) — never the whole tree. bandit HIGH and semgrep ERROR are the blocking severities; lower levels are advisory.
Blocking findings feed a bounded repair loop (≤10 iterations) on the shared client: the top findings (file:line rule-id severity) are injected into prompt-static-repair.md via {STATIC_FINDINGS} and a Sonnet 4.6 pass fixes them. A patch is accepted only if the blocking finding set strictly shrinks (proper-subset semantics) — a repair that resolves one finding but introduces an equal/higher-severity one is rejected and reverted via SDK checkpoint. If a blocking finding survives all 10 iterations, the blocker is persisted atomically to .code-generator/phase6_4_static_blocker.txt, State.static_blocker is set, and Phase 7 skips the commit (so a known-bad tree is never committed); --continue resumes the gate. The gate is mode-agnostic — it honors --no-commit and --no-github through the existing Phase 7 gating. Missing bandit/semgrep binary → WARNING + skip (see the [quality] extra above).
Phase 3/4 — API-signature grounding
To stop the implementer hallucinating internal APIs, orchestrator/signature_context.py extracts the symbols an issue references (backticked `names`, module.func dotted paths, CamelCase identifiers), resolves them against the graphify graph, and injects a compact {API_SIGNATURES} block into prompt-phase-3-implementation.md. The block carries signatures only — name/params/types/return, never function bodies (the reader stops at the signature-terminating :). It is capped at ~3000 tokens via truncate_placeholder. When graphify is absent or any lookup fails, the block degrades to the sentinel "(no signature context available)" — the pipeline never blocks on the graph.
Phase 3/4 — verdict routing into retries
When a Phase 3/4 attempt fails, the retry's feedback (last_err) is composed from structured verdicts the pipeline already computed rather than a bare str(exc) — zero new model calls. A Judge defect verdict contributes its defects list verbatim ("Judge found defects:\n- ..."); a failed MAV aspect contributes its aspect name + notes; a Phase-6.5 tier failure contributes the persisted failure_report tail (exit code, stderr tail, HTTP exchange) read from .code-generator/phase6_5_failure.txt. The composed feedback is truncated to ~1500 tokens. When no structured signal is in hand, it falls back to str(exc) unchanged.
Research-backed quality upgrades (R15–R22)
Eight opt-in techniques shipped in this release. Every one is default-off — a plain code-generator generate run is byte-for-byte unchanged unless at least one flag below is passed. Each flag is persisted to state.json; --continue inherits the saved value without re-passing.
Verification oracle invariant. Runtime verification (T0–T4 tiers in Phase 6.5) remains the sole issue-closing oracle across all eight techniques. No surrogate/judge/self-test verdict may close an issue — they are advisory inputs to repair routing, not substitutes for execution.
| Technique | Flag | What it does | Research source | Measured delta (bench harness) |
|---|---|---|---|---|
| R15 — Runtime feedback routing | (always active) | Repair feedback (last_err) is composed from structured Phase-6.5 / judge / MAV verdicts rather than str(exc) — zero new model calls |
LDB (Shi et al. 2024) — leverages execution feedback for repair | N/A (zero extra tokens) |
| R16 — Shared repair cap | (always active) | A RepairPolicy(max_rounds=2) shared across GVR/DARS/Phase-6.6 caps repair rounds; decide_next() routes "repair" vs "resample" vs "stop" |
AlphaCode 2 (Leblond et al. 2023) — bounded retry budget | N/A |
| R17 — REx bandit scheduler | --rex-scheduler |
Thompson-sampling bandit (orchestrator/rex_bandit.py) schedules Best-of-N candidate slots: pulls the arm with highest posterior mean, updates Beta(α,β) on pass/fail. Greedy fallback when budget fraction > 0.85. |
REx (Kambhampati et al. 2024) — exploration–exploitation for code generation | bench --fake --bandit-mode on: phase 69 adds ~2 600 input / 260 output tokens per cycle |
| R18 — DARS dynamic re-sampling | --dars-branching |
On Phase 3/4 stall or Phase 6.5 stuck_tier, replaces the retry prompt with build_alternative_action() — forces a fundamentally different implementation approach. Budget-bounded (suppressed above 85% wall fraction). |
DARS (Zhang et al. 2024) — dynamic alternative trajectory sampling | N/A (prompt-only change, no extra model calls) |
| R19 — ORPS process scoring | --orps-scoring |
After the BoN loop, run_beam() scores candidates by 0.7 × runtime_score + 0.3 × judge_score (tier/tests/wall + judge self-critique) and returns top-k sorted. No trained reward model. Budget-bounded (suppressed above 80% wall fraction). |
ORPS (Wang et al. 2024) — outcome-reward process scoring | bench --fake --orps-mode on: phase 74 adds ~3 000 input / 300 output tokens per cycle |
| R20 — GVR step gating | --gvr |
verify_step() calls the prompt-judge after each major plan/impl step; refine_until() loops (bounded by R16 cap) until the step passes. Fail-safe: judge exception → "pass" so GVR never blocks downstream work. No trained PRM. |
CodePRM (Peng et al. 2024) — process reward model for stepwise verification | N/A (judge already runs as part of Phase 3/4 when judge_enabled) |
| R21 — SWE-Debate fault localization | --swe-debate |
localize_faults() shells graphify query/explain to derive up to 5 knowledge-graph traces; run_debate() runs ≤3 rounds of judge + MAV debate, merging defects into a RankedFixPlan. Falls back gracefully when graphify is absent. No trained model. |
SWE-Debate (Chen et al. 2024) — multi-agent debate for software repair | N/A (uses existing judge + MAV calls) |
| R22 — Surrogate ranker pre-filter | --surrogate-ranker |
Before runtime verification, the prompt-judge ranks BoN candidates without executing any tier; tiers run top-k-first with short-circuit on first pass — skips expensive T2/T3 execution for obviously low-quality candidates. | Surrogate ranking (Li et al. 2024) — run-free candidate pre-filtering | bench --fake --surrogate-mode on: phase 70 adds ~2 400 input / 240 output tokens per cycle |
Bench harness A/B measurement
The bench command measures per-phase telemetry with --fake (fixture data, no LLM calls) or --real (live SDK):
# R17 REx bandit: compare with/without
code-generator bench --fake --output baseline.json
code-generator bench --fake --bandit-mode on --output rex.json
code-generator bench compare baseline.json rex.json
# R19 ORPS: same pattern
code-generator bench --fake --orps-mode on --output orps.json
code-generator bench compare baseline.json orps.json
# R22 surrogate ranker
code-generator bench --fake --surrogate-mode on --output surrogate.json
code-generator bench compare baseline.json surrogate.json
The compare subcommand diffs two report files and flags regressions in cache-hit ratio.
Local-only mode (--no-github)
GitHub is the default issue backend and is byte-for-byte unchanged. --no-github is an opt-in that runs the full 0→8 pipeline with zero gh subprocesses and no remote required — useful offline, in CI sandboxes, or for a quick local spike.
code-generator generate --no-github # local markdown store, no gh, no remote
code-generator generate --github # default: GitHub issues/milestones via gh
code-generator generate --continue # resumes the persisted mode (no flag needed)
How it differs (every other invariant is preserved):
- Backend. The orchestrator binds against the
IssueBackendprotocol, notgh/directly.--no-githubbuilds a network-freeLocalBackend;--githubbuilds aGitHubBackendover theghCLI. The same backend object is threaded through every issue-aware phase (1/2/3-4/5/7/8). - Store layout (
.code-generator/issues/):<NNNN>.md(YAML frontmatter + body),<NNNN>.comments.json(sidecar so comment bodies round-trip losslessly),.next(monotonic counter),milestones.json. All writes are atomic and path-traversal safe. - Preflight. SDK/CLI self-checks only — no
git remote, nogh auth status/gh repo view.state.repostaysNone. - Phase 1. Model emits issues as one fenced JSON array (same six body sections + label taxonomy as GitHub mode) instead of shelling out to
gh issue create; the orchestrator parses and writes them deterministically. - Phase 7. Always commits locally; the push is skipped (INFO log) when in local mode or the checkout has no remote.
Closes #Nfooters and the checklistcommittedtick still fire. - Phase 8. The CI green-gate is skipped wholesale (
ci_status = skipped); any remediation issue routes through the local backend.
Phases 7 and 8 are the only orchestrator modules that read github_mode (push and CI-gate are git/CI concerns with no IssueBackend equivalent). Anthropic credentials and cache-workspace invariants are untouched.
Running against Ollama Pro
An opt-in subcommand, code-generator generate ollama, runs the full 0→7 pipeline against one Ollama-hosted model instead of the Anthropic Max subscription. This is the only supported way to bypass the Max-subscription invariant and is gated by provider == "ollama" inside env.py.
One-time setup
-
Install the Ollama daemon (≥ 0.14.0) and start it on the default endpoint
http://localhost:11434. -
Sign the daemon into your Ollama Pro account — required for any
:cloudmodel:ollama signin -
Export your existing
OLLAMA_API_KEY(the same secret you already use elsewhere — no new secret is introduced):export OLLAMA_API_KEY=... # typically set once in ~/.zshrc
Preflight refuses to run if the daemon is unreachable, not signed in, or OLLAMA_API_KEY is empty — fix the message it prints and re-run.
Worked examples
# Plan + implement a whole repo on a cloud-hosted 480B coding model
code-generator generate ollama --model "qwen3-coder:480b:cloud"
# Same pipeline on a different cloud model
code-generator generate ollama --model "gpt-oss:120b-cloud"
The --model tag you pass drives every phase (0 through 7). There is no per-phase override on this codepath: single-model is the whole point. The tag is persisted to state.json as CycleState.ollama_model so code-generator generate ollama --continue resumes with the same model without re-typing it.
What is overridden vs. preserved
- Overridden under
provider == "ollama": non-negotiable #1 (Max-only env strip is narrowed toANTHROPIC_AUTH_TOKEN/ANTHROPIC_BASE_URLpassthrough), #4 (no Anthropic overage telemetry — replaced by turn-count + wall-clock budgets), #8 (fixed-model-per-phase — replaced by single-model routing). - Preserved on every path: #2 (never
--bare), #3 (YOLO permissions), #5 (wait-and-resume on 429), #6 (fresh context per issue), #7 (atomic state writes), #9 (no defaultmax_turns).
Running against OpenAI Codex
An opt-in subcommand, code-generator generate codex, runs the full 0→7 pipeline against one OpenAI Codex CLI model instead of the Anthropic Max subscription — structurally identical to the Gemini/Ollama provider paths and gated by provider == "codex" inside env.py.
One-time setup
-
Install the Codex CLI:
npm install -g @openai/codex. -
Authenticate with your ChatGPT subscription (zero API credits):
codex login # browser OAuth → ~/.codex/auth.json
Preflight refuses to run if the codex binary is missing or no ~/.codex/auth.json exists, and idempotently seeds the project dir as trusted in ~/.codex/config.toml (so headless codex exec never hangs on the trust prompt — openai/codex #14547). OPENAI_API_KEY / CODEX_API_KEY are stripped before every spawn: a confirmed Codex bug (openai/codex #15151, #20099) silently bills API credits whenever OPENAI_API_KEY is set even with a valid OAuth session.
Worked examples
# Plan + implement a whole repo on the default 2026 Codex model
code-generator generate codex --model gpt-5.5
# Resume without re-typing the model (resolved from state.json)
code-generator generate codex --continue
The --model tag drives every phase (0 through 7). It is persisted to state.json as CycleState.codex_model / State.codex_model so --continue resumes with the same model.
What is overridden vs. preserved
- Overridden under
provider == "codex": #1 (strips the Anthropic list andOPENAI_API_KEY/CODEX_API_KEY; only~/.codex/auth.jsonauthenticates), #4 (no Anthropic overage telemetry — turn/wall budgets +usage_limit_reached/429wait-and-resume), #8 (single-model routing). - Preserved on every path: #2 (never
--bare), #3 (YOLO via--dangerously-bypass-approvals-and-sandbox), #5 (wait-and-resume on usage-limit/429), #6 (fresh context per issue — headless Codex is stateless), #7 (atomic state writes), #9 (no defaultmax_turns).
Model portability
By default the orchestrator pins a canonical Anthropic model ID per phase (claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5-20251001). Those exact IDs are not portable — a Max subscription, a custom gateway, Bedrock, or Vertex each serve a different ID for the same capability tier, and a pinned ID the active account doesn't expose is rejected at startup.
models.resolve_model() maps each canonical ID down to its family alias (opus / sonnet / haiku) before passing it to the CLI or SDK. Both the claude CLI (--model opus) and the Claude Agent SDK accept these aliases, and they resolve to whatever the active account/provider serves for that tier — so the tool works on any subscription with zero extra configuration.
If you need a specific pinned version, set the official env vars:
export ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-8
export ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-6
export ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-4-5-20251001
These vars are not in the dangerous-variables strip list, so they survive build_agent_env() and reach both runners automatically — no extra configuration in this codebase is required.
The Ollama and Codex single-model paths pass through untouched (non-Claude tags like qwen3-coder:480b:cloud or gpt-5.5 are never rewritten).
Cache TTL behaviour
On the Max path the extended-cache-ttl-2025-04-11 beta is honoured by the Anthropic API, so BP1/BP2/BP3 land at 1h and BP4 at 5m (see runner/cache_breakpoints.py _TTLS_BY_PROBE).
runner.options.probe_cache_ttl_support detects this via the emitted anthropic-beta header — not a constructor-kwarg probe (which was a permanent false negative on SDK 0.2.91+, where cache_control is set as a post-init attribute rather than a constructor kwarg). The WARNING "cache TTL degraded to 5m" fires only when an older SDK genuinely strips the beta from the header; on 0.2.91+ it is silent, correctly indicating that 1h is active.
Observability (OpenTelemetry)
OTel forwarding is default-OFF. Enable it by setting the project-scoped gate variable:
export CODE_GENERATOR_ENABLE_OTEL=1
When the gate is active, runner/otel.py::build_otel_env builds an env dict that is merged onto ClaudeAgentOptions.env via make_agent_options(otel_enabled=True, ...). The bundled Claude CLI then receives and honours the following standard monitoring variables — forwarded verbatim when present in the calling shell, and never fabricated when absent:
| Variable | Purpose |
|---|---|
CLAUDE_CODE_ENABLE_TELEMETRY |
Set to 1 to activate OTel in the CLI (always injected when opt-in is on) |
OTEL_EXPORTER_OTLP_ENDPOINT |
OTLP collector URL (e.g. http://localhost:4317) |
OTEL_EXPORTER_OTLP_PROTOCOL |
Transport protocol (grpc or http/protobuf) |
OTEL_EXPORTER_OTLP_HEADERS |
Auth headers for the collector |
OTEL_METRICS_EXPORTER |
Metrics exporter backend |
OTEL_LOGS_EXPORTER |
Logs exporter backend |
OTEL_METRIC_EXPORT_INTERVAL |
Metrics export interval |
OTEL_LOGS_EXPORT_INTERVAL |
Logs export interval |
session.id correlation. Pass otel_session_id=<id> to make_agent_options to inject session.id=<id> into OTEL_RESOURCE_ATTRIBUTES. When the calling shell already sets OTEL_RESOURCE_ATTRIBUTES, the existing attributes are preserved and session.id is appended — they are never clobbered.
Safety guarantee. The OTel variables are listed in env.py::PRESERVED_ENV_VARS, the hardcoded whitelist that documents which vars are intentionally not stripped by build_agent_env(). A module-level assertion (_validate_preserve_disjoint) enforces at import time that PRESERVED_ENV_VARS can never intersect DANGEROUS_VARS or CODEX_CREDIT_VARS — so the whitelist seam can structurally never un-strip a credit variable and non-negotiable #1 remains intact.
Cross-phase correlation caveat. Phases 0/1/6/7 each run as separate subprocesses — full cross-phase session.id correlation requires the same id threaded to every make_agent_options call site. This release lands the seam and builder; per-phase wiring is deferred to a follow-up issue.
Tool-search policy (MCP deferral)
When MCP servers are registered (build_mcp_servers returns a non-None dict), the shared client for the non-Haiku phases (2/3/4/5 — Sonnet and Opus) sets ENABLE_TOOL_SEARCH=auto:5 on ClaudeAgentOptions.env via runner/tool_search.py::build_tool_search_env. This tells the bundled Claude CLI to defer full MCP tool schemas at a 5% context threshold — stricter than the implicit 10% default, so deferral activates earlier and saves more per-turn token cost.
Determination. Before this change, ENABLE_TOOL_SEARCH was unset, meaning the CLI applied its implicit auto (10%) policy whenever the beta was active. The explicit auto:5 policy preserves that default-on behaviour while lowering the threshold from 10% to 5%, causing MCP schemas to defer in more cases.
Eligibility rules in build_tool_search_env. The policy env dict is returned only when all three conditions hold:
mcp_attached=True— no MCP registered means nothing to defer.- Model is non-Haiku (
model_family(model) in ("opus", "sonnet")) — Haiku lackstool_referenceblock support, so deferral is structurally impossible. - Model is a recognised Claude model or
None(the Anthropic Max default) — Ollama/Codex tags (model_familyreturnsNone) indicate a non-first-partyANTHROPIC_BASE_URL; the CLI disables tool-search there, so the env var would be ignored.
Haiku caveat. The one-shot Haiku phases (0, 6, 6.5, 6.8) never open the shared MCP client — they run as subprocesses with their own options. This makes the scoping automatic: the policy is set on the shared-client options only, which physically cannot reach Haiku phases. MCP servers must not be attached to Haiku phases solely to defer their tool schemas (see runner/mcp.py module docstring).
Ollama/non-first-party path. When effective_model is an Ollama or Codex tag (model_family returns None), build_tool_search_env returns {} and the policy is not set. The CLI disables ENABLE_TOOL_SEARCH upstream when ANTHROPIC_BASE_URL is a non-first-party host anyway; this guard is belt-and-suspenders.
Env sanitization. ENABLE_TOOL_SEARCH is listed in env.py::PRESERVED_ENV_VARS so it survives build_agent_env(). The same import-time _validate_preserve_disjoint() assertion that guards the OTel vars enforces that this entry can never overlap DANGEROUS_VARS or CODEX_CREDIT_VARS.
SessionStore transcript mirror (opt-in)
runner/session_store.py implements an optional, audit-grade transcript mirror backed by append-only JSONL files under .code-generator/sessions/. It is default-OFF — all existing call sites pass nothing and the default path is byte-for-byte unchanged.
Design contract. FileSessionStore is a duck-typed adapter (not a subclass) that satisfies the SDK SessionStore Protocol structurally: append(key, entries) and load(key). The SDK appends a secondary copy of every session entry to the store; it does not auto-replay the mirror into the live session. Critically, import_session_to_store and get_session_messages_from_store are never called by this module — the store is write-only. Non-negotiable #6 (fresh-context-per-issue) is therefore structurally preserved: prior-issue transcript entries cannot leak into the next issue through this store.
Mutual exclusion. session_store and enable_file_checkpointing are mutually exclusive — the SDK rejects both being active simultaneously. assert_session_store_checkpoint_exclusive is called inside _build_cycle_options before any SDK session opens, raising SessionStoreCheckpointConflictError with a clear, actionable message when both are requested. Since file checkpointing is currently always-on by default and session_store_enabled defaults to False, the default run is never affected.
Capability gating. probe_session_store_support() checks whether the installed claude_agent_sdk exposes a SessionStore symbol at runtime. When absent, it logs a single INFO line per process and the store is simply not wired onto options — the pipeline continues without the mirror.
JSONL layout. Each transcript is stored as <project_key>.jsonl (or <project_key>__<subpath>.jsonl for subagent transcripts) under .code-generator/sessions/. Both components are sanitised so ../ or separator characters cannot escape the store root. Writes are atomic (memory._atomic_write → tmp → os.replace), matching the §7 invariant.
PreCompact hook (opt-in)
runner/precompact_hook.py registers an optional SDK PreCompact hook that fires before the server-side auto/manual compactor runs. It is default-OFF — all existing call sites pass nothing (precompact_hook_enabled=False on _build_cycle_options) and the default pipeline is byte-for-byte unchanged.
What it does. When enabled, the hook callback:
- Performs an idempotent fsync-durable rewrite of
.code-generator/memories/cycle-summary.mdso that the file is durably on disk before the summarizer processes the transcript. - Writes a lightweight JSON marker
{"event": "PreCompact", "trigger": "<auto|manual>"}to.code-generator/precompact/precompact-<trigger>.jsonfor post-mortem debugging of multi-hour runs. Thetriggervalue ("auto"or"manual") is always recorded.
No-op-safe. The hook always returns {} (an empty HookJSONOutput) — it never blocks, modifies, or cancels the compaction. Any internal error is caught and logged; it never propagates out of the callback.
Capability gating. probe_hook_support() checks whether the installed claude_agent_sdk exposes HookMatcher and PreCompactHookInput at runtime. When either is absent, build_precompact_hooks_config returns None, no hooks kwarg is set on options, and the run is byte-for-byte unchanged. A single INFO line is emitted per process when support is absent.
Ordering. The PreCompact hook fires before the SDK compactor runs (pre-summarizer). CompactionPauseHandler (runner/compaction_pause.py, issue #236) handles the pause_after_compaction signal after the compactor finishes — they are sequential by SDK design with no interleave or race.
Session isolation: setting_sources and auto-memory (T10, issue #10)
Determined leak (default behaviour before issue #10). With setting_sources=None (the SDK default), the SDK loads ALL of user + project + local settings, which means the host operator's ~/.claude/CLAUDE.md, skills, hooks, and auto-memory (~/.claude/projects/<project>/memory/) bleed into every generator session. On a multi-tenant or CI host this can inject unintended prompts and tools from a completely different operator context.
Fix: setting_sources=["project"]. The generator's shared-client options (built by _client_lifecycle._build_cycle_options) pass setting_sources=GENERATOR_SETTING_SOURCES (["project"]) to make_agent_options. This scopes the session to the target repo's own .claude/ directory, dropping the host user + local scopes entirely.
Self-generation safety. When the target repo IS this repo (code-generator generating itself), the project scope still loads this repo's own .claude/ — graphify, project prompts, and project config are unaffected. Only the host operator's personal ~/.claude/ config is dropped, which is exactly the isolation goal.
Auto-memory disable. setting_sources=["project"] alone does not block auto-memory (~/.claude/projects/<hash>/memory/). The env var CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 is injected on every generator env build:
- Subprocess path:
env.py::_build_anthropic_max_env()callsenable_auto_memory_isolation(env)to set the key in the returned dict. - In-process SDK path:
env.py::strip_dangerous_env()callsos.environ.setdefault("CLAUDE_CODE_DISABLE_AUTO_MEMORY", "1")so the in-process SDK session inherits the flag.
CLAUDE_CODE_DISABLE_AUTO_MEMORY is listed in env.py::PRESERVED_ENV_VARS. The same _validate_preserve_disjoint() assertion that guards OTel vars enforces at import time that this entry can never intersect DANGEROUS_VARS or CODEX_CREDIT_VARS — so the seam structurally cannot weaken non-negotiable #1.
CLI argv mirror. The subprocess runner reads setting_sources = getattr(options, "setting_sources", None) and, when non-empty, appends --setting-sources project to the claude argv (the --setting-sources flag was confirmed present in claude --help on SDK 0.2.91).
Phase coverage. All generator-session phases are now project-scoped:
- Shared-client phases (2, 3/4, 5):
setting_sources=GENERATOR_SETTING_SOURCESis set at_client_lifecycle._build_cycle_options, which opens the singleClaudeSDKClientfor the entire cycle. Every query on that client inherits the scope. - One-shot phases (0, 1, 6, 6.5, 6.6/repair, 6.4/static, 6.7/judge, 6.8/MAV, 7, 8, 8-repair, analyze-gate, cycle-prompts, test-designer): each
make_agent_options(...)call now explicitly passessetting_sources=GENERATOR_SETTING_SOURCES. - Command sessions (
review,optimize,clarify,clarify-answer): all passsetting_sources=GENERATOR_SETTING_SOURCES. - Auto-memory disable is process-wide (via
CLAUDE_CODE_DISABLE_AUTO_MEMORY=1inenv.py) and covers all phases.
The only deliberate exceptions are commands/bench.py (fake/fixture runner, not a generator session) and _cache_warmup.py (empty no-op pre-warm on the already-scoped shared client).
Default callers unaffected. make_agent_options's own default for setting_sources is None, so any call site that does not pass the kwarg (all existing tests and non-generator callers) continues to behave byte-for-byte as before.
Safety constraints
The tool enforces nine non-negotiables (see CLAUDE.md for the full list):
-
Max subscription only, zero API credits. Before any SDK or subprocess call, the tool strips
ANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN,ANTHROPIC_BEDROCK_API_KEY,ANTHROPIC_VERTEX_PROJECT_ID,CLAUDE_CODE_USE_BEDROCK,CLAUDE_CODE_USE_VERTEXfrom the environment. A startup check refuses to run if any are present. -
Never
--bare. The CLI flag--bareskips OAuth and forces API credits — it is never passed by this tool. If Anthropic ever makes--barethe default forclaude -p, pin the CLI version or pass the opposite flag explicitly. -
YOLO mode always. Every SDK call uses
permission_mode="bypassPermissions"; the subprocess fallback uses--dangerously-skip-permissions. -
Overage protection. On every
RateLimitEvent, the tool checksinfo.overage_statusand aborts immediately if it is anything other thanNoneor"disabled". Reference incident: anthropics/claude-code#37686 — a user burned $1,800 in two days because billing overage was silently enabled.Agent-SDK credit pool (effective 2026-06-15). Subscription Agent-SDK /
claude -pusage now draws from a separate monthly credit pool, distinct from the interactive five-hour / seven-day windows. When that pool is exhausted, requests stop with no paid spill unless usage credits are explicitly enabled. Detection: aRateLimitInfo.rate_limit_typetoken inrunner.message_parsing._AGENT_SDK_CREDIT_TYPES(best-known candidates pending live confirmation after the date). Handling: a dedicated, separately-logged branch raisesAgentCreditExhausted(a subclass ofOverageAbort, so it inherits the never-retry clean-stop path), persists an actionable message + the creditrate_limit_typetostate.json, and stops —code-generator generate --continueresumes once the pool resets. If usage credits are enabled (overage available), the existing overage gate above fires first, so the credit branch never spills into paid spend. -
Wait-and-resume rate-limit handling. Rate limits are not retried with exponential backoff — the tool sleeps until
resets_at + 60sand resumes the same session. Backoff (10→20→40→80→120s) only applies to non-rate-limit transient errors. -
Fresh SDK session per issue. The implementation phase never reuses conversational context across issues — each issue is a
/clear-equivalent fresh session. -
Atomic state writes.
.code-generator/state.jsonis updated viatmp → os.replace(tmp, STATE)so a crash mid-write cannot corrupt it. -
Fixed model per phase. Opus 4.8 for phases 1/2/5/7/8; Sonnet 4.6 for phases 3/4; Haiku 4.5 for phases 0/6. The canonical IDs are resolved to portable family aliases (
opus/sonnet/haiku) before being sent to the CLI/SDK — see Model portability.
Troubleshooting
Startup fails with "dangerous environment variables present". A previous shell session exported ANTHROPIC_API_KEY (or another billing-routing variable). unset it and try again — the tool is intentionally strict so a stray export never costs you money.
Rate limit reached. Run code-generator status to see how many minutes remain on the current pause. The next code-generator generate --continue will wait out the rest of the window and resume the same session automatically.
gh issue create fails. Confirm gh auth status reports an authenticated SSH session for github.com, and that the current directory is inside a repo with a default remote configured.
Tests don't pass after Phase 6. The orchestrator skips Phase 7 (commit) when the test runner reports persistent failures. Fix the failing tests manually, then run code-generator generate --continue to re-enter from where it stopped.
Project layout
src/code_generator/
├── cli.py # Typer entry point: init, generate, review, status
├── env.py # DANGEROUS_VARS, build_agent_env, assert_safe_environment
├── state.py # State dataclasses + atomic load/save
├── prompts/ # Packaged prompt-phase-*.md files + load_prompt
├── templates/ # Project templates for `init`
├── logging_setup.py # Per-phase log files under .code-generator/logs/
├── gh/ # gh CLI wrapper package (core, issues, labels, milestones)
├── backends/ # IssueBackend protocol + GitHubBackend / LocalBackend
├── agents.py # Tech-stack detector + agent selection matrix
├── runner/
│ ├── sdk_runner.py # claude-agent-sdk wrapper, RateLimitEvent handling
│ ├── subprocess_runner.py # CLI fallback, stream-json parser
│ ├── rate_limit.py # Wait-and-resume main loop
│ └── retry.py # Backoff + circuit breaker
├── orchestrator/
│ ├── phase0_complexity.py # single vs multi-cycle decision
│ ├── phase1_plan.py # milestone + issues
│ ├── phase2_review.py # per-issue review
│ ├── phase3_4_implement.py # TDD loop, fresh session per issue
│ ├── phase5_closure.py # closure + fix-issue feedback loop
│ ├── phase6_test.py # test runner, max 3 retries
│ ├── phase7_commit.py # Opus commit message + local commit / guarded push
│ ├── phase8_finalization.py # finalization + CI green-gate (skipped in local mode)
│ └── cycle_loop.py # multi-cycle driver
└── commands/ # init, optimize, status, generate, review (Typer commands)
Development
git clone git@github.com:SilvioBaratto/code-generator.git
cd code-generator
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -q
ruff check src tests
Benchmarking
code-generator bench --fake --cycles 3 --output bench.json
Reference
The full specification lives in requirements.md. Each prompt file (prompt-phase-*.md, prompt-review.md) is loaded verbatim by the orchestrator with {NAME} placeholders substituted at runtime — edit the prompts there, not inline in Python.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claude_code_generator-0.6.9.tar.gz.
File metadata
- Download URL: claude_code_generator-0.6.9.tar.gz
- Upload date:
- Size: 1.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2dd58e5a2ded79314e359154bd5580dc140b077186146c02c0aa866c81adbe51
|
|
| MD5 |
a9696268072382e08d986a1d5cff8a1e
|
|
| BLAKE2b-256 |
00af5213124d151628ecb39a7b16e1a6028a455981f6663bed0a1138175d0cde
|
File details
Details for the file claude_code_generator-0.6.9-py3-none-any.whl.
File metadata
- Download URL: claude_code_generator-0.6.9-py3-none-any.whl
- Upload date:
- Size: 556.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41a3a44189b2d54d57c86718b8fd29b0bda9ab641303aeca7f8368e1ec129627
|
|
| MD5 |
112e4ea219b6eaf04318d5d145639bf3
|
|
| BLAKE2b-256 |
69f9690e4f948325d334d0436fde8124c16879140abb2c926903b150d3e66e4c
|