Metadata-Version: 2.4
Name: claude-code-generator
Version: 0.8.0
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.128
Requires-Dist: typer==0.27.0
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: pytest-cov==7.1.0; extra == "dev"
Requires-Dist: hypothesis==6.161.0; extra == "dev"
Requires-Dist: ruff==0.15.22; extra == "dev"
Provides-Extra: quality
Requires-Dist: semgrep==1.171.0; extra == "quality"
Requires-Dist: hypothesis==6.161.0; extra == "quality"
Requires-Dist: mutmut==3.6.0; extra == "quality"
Provides-Extra: all
Requires-Dist: claude-code-generator[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 extras

Install feature groups with `pip install "code-generator[<extra>]"`:

| Extra | Installs | Enables |
|---|---|---|
| `[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) — the only credential the tool needs
claude auth login
```

Issues live in a network-free local markdown store under `.code-generator/issues/`, so **no `gh` auth and no `origin` remote are required**. If the checkout happens to have a remote, Phase 7 pushes to it; otherwise the push is skipped with an INFO log and everything else runs unchanged.

## 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 in the 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 only if a remote exists) | Opus 4.8 |
| 8 | Finalization + managed-workflow file writing | 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) | — |
| `--commit` / `--no-commit` | Phase 7 commit: `--commit` (default) runs `git add/commit` each cycle (push happens only if the checkout has a remote); `--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` |

The `--commit` / `--no-commit` choice persists to `state.json` (`State.commit_enabled`) and is honored on `--continue` (an explicit `--commit`/`--no-commit` still overrides on resume). `--no-commit` makes Phase 7 a complete no-op — no `git add`/`commit`/`push`, the working tree is left dirty, and the checklist `committed` tick does not fire. It is available on both generate commands (`generate`, `generate ollama`).

#### 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 milestone in the local store 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 is filed as an issue (this command still shells out to `gh issue create` from its prompt, so it requires `gh` auth; the generate pipeline does not).

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

Two 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 honors `--no-commit` through the existing Phase 7 gating. Missing bandit/semgrep binary → WARNING + skip (see the `[quality]` extra above).

### 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)

Seven 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 seven 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) |
| **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 issue store

Issues and milestones live in a network-free markdown/JSON store under `.code-generator/issues/` — there is **no GitHub backend and no `gh` dependency**. The full 0→8 pipeline runs with zero `gh` subprocesses and no remote required, which works offline, in CI sandboxes, or for a quick local spike.

**How it works** (every safety invariant is preserved):

- **Backend.** The orchestrator binds against the `IssueBackend` protocol and always builds a single `LocalBackend`, 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`.
- **Phase 1.** The model emits issues as one fenced JSON array (six body sections + label taxonomy); the orchestrator parses and writes them deterministically to the store.
- **Phase 7.** Always commits locally; the push runs only when the checkout has a remote (`git_has_remote`), otherwise it is skipped with an INFO log. `Closes #N` footers and the checklist `committed` tick still fire.
- **Phase 8.** Writes the managed-workflow files; there is no CI green-gate.

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

## 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 single-model path passes through untouched (non-Claude tags like `qwen3-coder:480b:cloud` 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` — 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.

## 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/` — 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` — 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, 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.

**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/
├── backends/             # IssueBackend protocol + LocalBackend (network-free store)
├── 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 / remote-gated push
│   ├── phase8_finalization.py # finalization + managed-workflow file writing
│   └── 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

# recommended: uv (reproducible from uv.lock)
uv sync --all-extras          # create .venv + install from the lockfile
uv run pytest -q
uv run ruff check src tests

# or classic pip
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -q
ruff check src tests
```

`pyproject.toml` is the single source of truth for dependencies; `uv.lock` pins
the fully-resolved graph (hashes, all platforms) for reproducible installs. After
editing dependencies in `pyproject.toml`, run `uv lock` to refresh the lockfile.
`requirements.txt` is retained as a pip-only fallback (core + dev pins).

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