Metadata-Version: 2.4
Name: claude-code-generator
Version: 0.7.2
Summary: Orchestrator CLI that drives Claude Code end-to-end to generate whole projects from a requirements.md file.
Author: Silvio Baratto
License: MIT
Project-URL: Homepage, https://github.com/SilvioBaratto/code-generator
Project-URL: Repository, https://github.com/SilvioBaratto/code-generator
Project-URL: Issues, https://github.com/SilvioBaratto/code-generator/issues
Keywords: claude,claude-code,code-generation,llm,cli,orchestrator
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Code Generators
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: claude-agent-sdk==0.2.110
Requires-Dist: typer==0.26.8
Requires-Dist: rich==15.0.0
Requires-Dist: pyyaml==6.0.3
Requires-Dist: bandit==1.9.4
Provides-Extra: dev
Requires-Dist: pytest==9.1.1; extra == "dev"
Requires-Dist: pytest-asyncio==1.4.0; extra == "dev"
Requires-Dist: pytest-timeout==2.4.0; extra == "dev"
Requires-Dist: hypothesis==6.155.7; extra == "dev"
Requires-Dist: ruff==0.15.20; extra == "dev"
Provides-Extra: mcp
Requires-Dist: mcp==1.28.1; extra == "mcp"
Provides-Extra: batch
Requires-Dist: anthropic==0.113.0; extra == "batch"
Provides-Extra: quality
Requires-Dist: semgrep==1.168.0; extra == "quality"
Requires-Dist: hypothesis==6.155.7; extra == "quality"
Requires-Dist: mutmut==3.6.0; extra == "quality"
Provides-Extra: all
Requires-Dist: claude-code-generator[batch,mcp,quality]; extra == "all"
Dynamic: license-file

# 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](#safety-constraints) below.

## Install

```bash
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](https://github.com/safishamsi/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):

```bash
uv tool install --upgrade graphifyy   # CLI binary is `graphify`
# or: pip install -U graphifyy
```

Requires graphify **>= 0.9.1** (idempotent `update` with stale-edge pruning, full-repo-relative-path 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

```bash
# 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](#local-only-mode---no-github) (`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`.

```bash
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.

```bash
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](#local-only-mode---no-github).

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)](https://arxiv.org/abs/2411.02509) — 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)](https://arxiv.org/abs/2312.09910) — 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)](https://arxiv.org/abs/2404.12902) — 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)](https://arxiv.org/abs/2407.12854) — 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)](https://arxiv.org/abs/2410.12552) — 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)](https://arxiv.org/abs/2410.04536) — 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)](https://arxiv.org/abs/2406.12952) — 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)](https://arxiv.org/abs/2408.03314) — 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):

```bash
# 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.

```bash
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 `IssueBackend` protocol, not `gh/` directly. `--no-github` builds a network-free `LocalBackend`; `--github` builds a `GitHubBackend` over the `gh` CLI. 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`, no `gh auth status`/`gh repo view`. `state.repo` stays `None`.
- **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 #N` footers and the checklist `committed` tick 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

1. Install the Ollama daemon (≥ 0.14.0) and start it on the default endpoint `http://localhost:11434`.
2. Sign the daemon into your Ollama Pro account — required for any `:cloud` model:

   ```bash
   ollama signin
   ```

3. Export your existing `OLLAMA_API_KEY` (the same secret you already use elsewhere — no new secret is introduced):

   ```bash
   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

```bash
# 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 to `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL` passthrough), #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 default `max_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

1. Install the Codex CLI: `npm install -g @openai/codex`.
2. Authenticate with your ChatGPT subscription (zero API credits):

   ```bash
   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

```bash
# 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 **and** `OPENAI_API_KEY` / `CODEX_API_KEY`; only `~/.codex/auth.json` authenticates), #4 (no Anthropic overage telemetry — turn/wall budgets + `usage_limit_reached`/`429` wait-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 default `max_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:

```bash
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:

```bash
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:

1. `mcp_attached=True` — no MCP registered means nothing to defer.
2. Model is non-Haiku (`model_family(model) in ("opus", "sonnet")`) — Haiku lacks `tool_reference` block support, so deferral is structurally impossible.
3. Model is a recognised Claude model or `None` (the Anthropic Max default) — Ollama/Codex tags (`model_family` returns `None`) indicate a non-first-party `ANTHROPIC_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:

1. Performs an idempotent fsync-durable rewrite of `.code-generator/memories/cycle-summary.md` so that the file is durably on disk before the summarizer processes the transcript.
2. Writes a lightweight JSON marker `{"event": "PreCompact", "trigger": "<auto|manual>"}` to `.code-generator/precompact/precompact-<trigger>.json` for post-mortem debugging of multi-hour runs.  The `trigger` value (`"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`) 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

**Determined leak (prior default behaviour).** 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()` calls `enable_auto_memory_isolation(env)` to set the key in the returned dict.
- **In-process SDK path**: `env.py::strip_dangerous_env()` calls `os.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_SOURCES` is set at `_client_lifecycle._build_cycle_options`, which opens the single `ClaudeSDKClient` for 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 passes `setting_sources=GENERATOR_SETTING_SOURCES`.
- **Command sessions** (`review`, `optimize`, `clarify`, `clarify-answer`): all pass `setting_sources=GENERATOR_SETTING_SOURCES`.
- **Auto-memory disable** is process-wide (via `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1` in `env.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):

1. **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_VERTEX` from the environment. A startup check refuses to run if any are present.
2. **Never `--bare`.** The CLI flag `--bare` skips OAuth and forces API credits — it is **never** passed by this tool. If Anthropic ever makes `--bare` the default for `claude -p`, pin the CLI version or pass the opposite flag explicitly.
3. **YOLO mode always.** Every SDK call uses `permission_mode="bypassPermissions"`; the subprocess fallback uses `--dangerously-skip-permissions`.
4. **Overage protection.** On every `RateLimitEvent`, the tool checks `info.overage_status` and aborts immediately if it is anything other than `None` or `"disabled"`. Reference incident: [anthropics/claude-code#37686](https://github.com/anthropics/claude-code/issues/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 -p` usage 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: a `RateLimitInfo.rate_limit_type` token in `runner.message_parsing._AGENT_SDK_CREDIT_TYPES` (best-known candidates pending live confirmation after the date). Handling: a dedicated, separately-logged branch raises `AgentCreditExhausted` (a subclass of `OverageAbort`, so it inherits the never-retry clean-stop path), persists an actionable message + the credit `rate_limit_type` to `state.json`, and stops — `code-generator generate --continue` resumes 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.
5. **Wait-and-resume rate-limit handling.** Rate limits are not retried with exponential backoff — the tool sleeps until `resets_at + 60s` and resumes the same session. Backoff (10→20→40→80→120s) only applies to non-rate-limit transient errors.
6. **Fresh SDK session per issue.** The implementation phase never reuses conversational context across issues — each issue is a `/clear`-equivalent fresh session.
7. **Atomic state writes.** `.code-generator/state.json` is updated via `tmp → os.replace(tmp, STATE)` so a crash mid-write cannot corrupt it.
8. **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](#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

```bash
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

```bash
code-generator bench --fake --cycles 3 --output bench.json
```

## Reference

The full specification lives in [`requirements.md`](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.
