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 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
# 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.
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 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) — 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) |
| 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 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
IssueBackendprotocol and always builds a singleLocalBackend, 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. - 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 #Nfooters and the checklistcommittedtick 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
-
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).
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 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:
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:
- 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) 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()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 — 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, 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.
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
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
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.
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.8.0.tar.gz.
File metadata
- Download URL: claude_code_generator-0.8.0.tar.gz
- Upload date:
- Size: 564.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e324d5b9ac34ffca01f91b7b22fa137b8459089da89db23c8927ab72c508e020
|
|
| MD5 |
de4615152a95590d3d9f02386c032701
|
|
| BLAKE2b-256 |
2bec29a36ce0bf3197bf7c22f484e46abf7a0b4fa5f7f0342382e80587451e03
|
File details
Details for the file claude_code_generator-0.8.0-py3-none-any.whl.
File metadata
- Download URL: claude_code_generator-0.8.0-py3-none-any.whl
- Upload date:
- Size: 650.8 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 |
ac4bc212b58c62632fbc98c9b9f9142fd8ab1ffe9d82096bf513a2ec7373af9d
|
|
| MD5 |
d7a7c494fa50276c0b2163faa224a14a
|
|
| BLAKE2b-256 |
f557750777c002580f8a59a083c2fc470369a0d37b700e4f0c73444936827592
|