Skip to main content

Orchestrator CLI that drives Claude Code end-to-end to generate whole projects from a requirements.md file.

Project description

code-generator

A Python CLI that orchestrates Claude Code end-to-end to generate a whole project from a requirements.md file. Five commands — init, optimize, generate, review, status. Works for any project type: Python CLI, FastAPI, Angular, NestJS, full-stack, finance, BAML/LLM.

Max subscription only. This tool uses exclusively the Claude Max subscription. It strips every environment variable that could route a call through the API and aborts on startup if any are still present. There is no path to API credit billing — see Safety constraints below.

Install

pipx install code-generator        # recommended
# or
pip install code-generator          # user-site
# or, from source
git clone git@github.com:SilvioBaratto/code-generator.git
cd code-generator && pip install -e ".[dev]"

Optional: codebase graph (graphify)

Phases 0, 1, and 2 inject a knowledge-graph report from graphify for richer code orientation. When the graph is missing, the orchestrator falls back to a sentinel string and continues — nothing breaks.

Install the CLI (required to use graphify at all):

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

Requires graphify >= 0.8.1 (idempotent update, stable node-id format).

Seed the graph (one-time, per project): graphify's full LLM-driven build runs through the /graphify slash-command inside an AI assistant — not through the shell binary. From inside Claude Code (or any other graphify-supported assistant) run:

/graphify .

This produces graphify-out/graph.json and graphify-out/GRAPH_REPORT.md.

Subsequent code-generator runs will automatically refresh the graph with graphify update . — idempotent and AST-only (no LLM cost; graph.json/GRAPH_REPORT.md are only rewritten on real changes). A graph seeded by a pre-0.7.18 graphify is reseeded once automatically via graphify update . --force (still no LLM cost) and stamped. Doc / paper / image changes still need a manual /graphify . re-run inside your assistant.

Add to your project's .gitignore:

graphify-out/

Optional extras

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

Extra Installs Enables
[mcp] mcp (Python client) MCP servers in Phases 3/4 (Codebase-Memory is a separate Rust crate; Serena resolved out-of-band if installed)
[batch] anthropic Anthropic Messages Batches API runner
[quality] bandit, semgrep Phase 6.4 static-analysis gate (security + reliability scanners)
[all] all of the above Full feature set (use for local dev)

The [quality] extra is required for the Phase 6.4 static gate to scan; when bandit/semgrep are absent the gate logs a WARNING and skips (the pipeline never blocks on a missing scanner).

Authentication

# Claude Code (Max subscription)
claude auth login

# GitHub (SSH protocol) — only for the default --github backend
gh auth login -h github.com -p ssh
gh config set git_protocol ssh

GitHub auth is not required in local-only mode (code-generator generate --no-github).

Commands

code-generator init [--template TYPE] [--force]

Scaffold .code-generator/ in the current directory with a ready-to-fill requirements.md. Templates: fastapi, angular, nestjs, fullstack, python-cli, finance. Refuses to clobber an existing .code-generator/ without --force.

code-generator init --template fastapi
$EDITOR .code-generator/requirements.md

code-generator optimize [--dry-run] [--force]

Pre-flight rewrite of .code-generator/requirements.md into the canonical structure the orchestrator expects: # Title## Description## Tech Stack## Goals## Scope (numbered ### N. subsections with per-section Acceptance criteria checklists) → ## Non-goals## Constraints## Global acceptance criteria.

Runs a single Opus 4.8 session with a dedicated prompt, preserves the original intent verbatim when ambiguous (never invents new scope), and writes the rewrite back to disk atomically. The pre-optimize original is saved to .code-generator/requirements.backup-<UTC-timestamp>.md so you can always diff and recover. The command is stateless — it never reads or writes state.json and never calls gh.

Short-circuits and exits 0 if the file is already canonical. Use --force to re-optimize anyway. Use --dry-run to print the proposed rewrite to stdout without touching the file or creating a backup. An output guard rejects any response that is not a well-formed requirements document (e.g. model commentary instead of a rewrite) and aborts with an error rather than overwriting requirements.md with garbage.

code-generator optimize --dry-run    # preview
code-generator optimize              # commit the rewrite
code-generator generate              # then run the pipeline

code-generator generate [flags]

Read .code-generator/requirements.md and orchestrate Claude Code through the 0→8 phase pipeline:

Phase Action Model
0 Complexity analysis (single vs multi-cycle) Haiku 4.5
1 Planning — creates issues + milestone (GitHub or local store) Opus 4.8
2 Per-issue review Opus 4.8
3-4 Implementation, fresh SDK session per issue (TDD) Sonnet 4.6
5 Closure + cross-module review Opus 4.8
6 Test suite, max 3 retries on failure Haiku 4.5
6.4 Static-analysis gate (bandit + semgrep) — diff-scoped, ≤10-iter strict-shrink repair Sonnet 4.6
6.5 Runtime verification (T0→T2 tiers) + bounded repair loop Haiku 4.5 / Sonnet 4.6
7 Commit message + git add/commit/push Opus 4.8
8 Finalization + CI green-gate (skipped in local mode) Opus 4.8

Every phase logs a completion summary with token counts (in, cache_read, cache_write, out) and persists them to state.json.

Flags:

Flag Description Default
--dry-run Run only Phase 0 + Phase 1 (planning) false
--phase N Resume from phase N
--continue Resume from the last completed phase (reads state.json) false
--mode auto|single|multi-cycle Force a mode (default auto runs Phase 0 first) auto
--cycle N Resume from cycle N (multi-cycle only)
--github / --no-github Issue backend: GitHub (needs gh auth + origin remote) or a local markdown store (no remote required) --github
--commit / --no-commit Phase 7 commit: --commit (default) runs git add/commit each cycle (push gated separately by --github); --no-commit skips the commit entirely, leaving generated files uncommitted in the working tree --commit
--max-turns N Opt-in per-session turn cap for debugging or strict cost control (positive integer); omit for no cap

The --github / --no-github choice persists to state.json (State.github_mode / CycleState.github_mode); --continue honors the saved mode without re-passing the flag (an explicit --github/--no-github still overrides on resume). Available on all four generate commands (generate, generate ollama, generate gemini, generate codex). See Local-only mode.

The --commit / --no-commit choice is orthogonal to --github: --no-github only skips the push; --no-commit skips the commit entirely. The choice persists to state.json (State.commit_enabled) and is honored on --continue in the same way. When --no-commit is active, Phase 8's CI green-gate is also skipped wholesale (a CI run without a pushed commit is meaningless).

Turn budgets

No per-session turn budget is set by default. Sessions terminate via end_turn on the model side; the real safety nets are rate-limit handling, overage abort (non-negotiable #4), and task budgets — not a guessed integer. --max-turns N exists on generate, optimize, and review as an opt-in for debugging or strict cost control; it is not for production.

In multi-cycle mode each cycle is a GitHub Milestone and produces a committable, working increment. Each cycle starts a fresh SDK session and re-reads the committed codebase, so the model never relies on stale conversational context.

If the Max subscription hits a rate limit, the tool pauses — it persists the session id and paused_until timestamp atomically to .code-generator/state.json, sleeps until the window resets, and resumes with --resume <session_id>. A killed process will resume on its own when re-run.

code-generator review [--create-issues] [--severity LEVEL]

Run a standalone Opus 4.8 review of the current codebase against the design principles in requirements.md §4 (SOLID, Clean Code, TDD, YAGNI/KISS/DRY). With --create-issues, every finding becomes a GitHub Issue.

code-generator status

Print a Rich table with the current mode, current cycle, current phase, open/closed issue counts, the rate-limit pause state (with minutes remaining if paused), per-cycle token consumption columns (in | cache_read | cache_write | out), wall-clock elapsed time, and any last_error highlighted in red.

Quality pipeline

Three closed-loop quality mechanisms ground the implementer and gate the commit. All run inside the existing per-cycle SDK client — no new model calls, no new credentials.

Phase 6.4 — static-analysis gate

A deterministic gate (orchestrator/phase6_4_static.py) sits between Phase 6 and Phase 6.5. It runs bandit + semgrep over the cycle diff only (files changed since base_commit, via git diff base..HEAD --name-only) — never the whole tree. bandit HIGH and semgrep ERROR are the blocking severities; lower levels are advisory.

Blocking findings feed a bounded repair loop (≤10 iterations) on the shared client: the top findings (file:line rule-id severity) are injected into prompt-static-repair.md via {STATIC_FINDINGS} and a Sonnet 4.6 pass fixes them. A patch is accepted only if the blocking finding set strictly shrinks (proper-subset semantics) — a repair that resolves one finding but introduces an equal/higher-severity one is rejected and reverted via SDK checkpoint. If a blocking finding survives all 10 iterations, the blocker is persisted atomically to .code-generator/phase6_4_static_blocker.txt, State.static_blocker is set, and Phase 7 skips the commit (so a known-bad tree is never committed); --continue resumes the gate. The gate is mode-agnostic — it honors --no-commit and --no-github through the existing Phase 7 gating. Missing bandit/semgrep binary → WARNING + skip (see the [quality] extra above).

Phase 3/4 — API-signature grounding

To stop the implementer hallucinating internal APIs, orchestrator/signature_context.py extracts the symbols an issue references (backticked `names`, module.func dotted paths, CamelCase identifiers), resolves them against the graphify graph, and injects a compact {API_SIGNATURES} block into prompt-phase-3-implementation.md. The block carries signatures only — name/params/types/return, never function bodies (the reader stops at the signature-terminating :). It is capped at ~3000 tokens via truncate_placeholder. When graphify is absent or any lookup fails, the block degrades to the sentinel "(no signature context available)" — the pipeline never blocks on the graph.

Phase 3/4 — verdict routing into retries

When a Phase 3/4 attempt fails, the retry's feedback (last_err) is composed from structured verdicts the pipeline already computed rather than a bare str(exc)zero new model calls. A Judge defect verdict contributes its defects list verbatim ("Judge found defects:\n- ..."); a failed MAV aspect contributes its aspect name + notes; a Phase-6.5 tier failure contributes the persisted failure_report tail (exit code, stderr tail, HTTP exchange) read from .code-generator/phase6_5_failure.txt. The composed feedback is truncated to ~1500 tokens. When no structured signal is in hand, it falls back to str(exc) unchanged.

Local-only mode (--no-github)

GitHub is the default issue backend and is byte-for-byte unchanged. --no-github is an opt-in that runs the full 0→8 pipeline with zero gh subprocesses and no remote required — useful offline, in CI sandboxes, or for a quick local spike.

code-generator generate --no-github     # local markdown store, no gh, no remote
code-generator generate --github        # default: GitHub issues/milestones via gh
code-generator generate --continue      # resumes the persisted mode (no flag needed)

How it differs (every other invariant is preserved):

  • Backend. The orchestrator binds against the 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:

    ollama signin
    
  3. 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 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):

    codex login   # browser OAuth → ~/.codex/auth.json
    

Preflight refuses to run if the codex binary is missing or no ~/.codex/auth.json exists, and idempotently seeds the project dir as trusted in ~/.codex/config.toml (so headless codex exec never hangs on the trust prompt — openai/codex #14547). OPENAI_API_KEY / CODEX_API_KEY are stripped before every spawn: a confirmed Codex bug (openai/codex #15151, #20099) silently bills API credits whenever OPENAI_API_KEY is set even with a valid OAuth session.

Worked examples

# Plan + implement a whole repo on the default 2026 Codex model
code-generator generate codex --model gpt-5.5

# Resume without re-typing the model (resolved from state.json)
code-generator generate codex --continue

The --model tag drives every phase (0 through 7). It is persisted to state.json as CycleState.codex_model / State.codex_model so --continue resumes with the same model.

What is overridden vs. preserved

  • Overridden under provider == "codex": #1 (strips the Anthropic list 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:

export ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-8
export ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-6
export ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-4-5-20251001

These vars are not in the dangerous-variables strip list, so they survive build_agent_env() and reach both runners automatically — no extra configuration in this codebase is required.

The Ollama and Codex single-model paths pass through untouched (non-Claude tags like qwen3-coder:480b:cloud or gpt-5.5 are never rewritten).

Cache TTL behaviour

On the Max path the extended-cache-ttl-2025-04-11 beta is honoured by the Anthropic API, so BP1/BP2/BP3 land at 1h and BP4 at 5m (see runner/cache_breakpoints.py _TTLS_BY_PROBE).

runner.options.probe_cache_ttl_support detects this via the emitted anthropic-beta header — not a constructor-kwarg probe (which was a permanent false negative on SDK 0.2.91+, where cache_control is set as a post-init attribute rather than a constructor kwarg). The WARNING "cache TTL degraded to 5m" fires only when an older SDK genuinely strips the beta from the header; on 0.2.91+ it is silent, correctly indicating that 1h is active.

Observability (OpenTelemetry)

OTel forwarding is default-OFF. Enable it by setting the project-scoped gate variable:

export CODE_GENERATOR_ENABLE_OTEL=1

When the gate is active, runner/otel.py::build_otel_env builds an env dict that is merged onto ClaudeAgentOptions.env via make_agent_options(otel_enabled=True, ...). The bundled Claude CLI then receives and honours the following standard monitoring variables — forwarded verbatim when present in the calling shell, and never fabricated when absent:

Variable Purpose
CLAUDE_CODE_ENABLE_TELEMETRY Set to 1 to activate OTel in the CLI (always injected when opt-in is on)
OTEL_EXPORTER_OTLP_ENDPOINT OTLP collector URL (e.g. http://localhost:4317)
OTEL_EXPORTER_OTLP_PROTOCOL Transport protocol (grpc or http/protobuf)
OTEL_EXPORTER_OTLP_HEADERS Auth headers for the collector
OTEL_METRICS_EXPORTER Metrics exporter backend
OTEL_LOGS_EXPORTER Logs exporter backend
OTEL_METRIC_EXPORT_INTERVAL Metrics export interval
OTEL_LOGS_EXPORT_INTERVAL Logs export interval

session.id correlation. Pass otel_session_id=<id> to make_agent_options to inject session.id=<id> into OTEL_RESOURCE_ATTRIBUTES. When the calling shell already sets OTEL_RESOURCE_ATTRIBUTES, the existing attributes are preserved and session.id is appended — they are never clobbered.

Safety guarantee. The OTel variables are listed in env.py::PRESERVED_ENV_VARS, the hardcoded whitelist that documents which vars are intentionally not stripped by build_agent_env(). A module-level assertion (_validate_preserve_disjoint) enforces at import time that PRESERVED_ENV_VARS can never intersect DANGEROUS_VARS or CODEX_CREDIT_VARS — so the whitelist seam can structurally never un-strip a credit variable and non-negotiable #1 remains intact.

Cross-phase correlation caveat. Phases 0/1/6/7 each run as separate subprocesses — full cross-phase session.id correlation requires the same id threaded to every make_agent_options call site. This release lands the seam and builder; per-phase wiring is deferred to a follow-up issue.

Tool-search policy (MCP deferral)

When MCP servers are registered (build_mcp_servers returns a non-None dict), the shared client for the non-Haiku phases (2/3/4/5 — Sonnet and Opus) sets ENABLE_TOOL_SEARCH=auto:5 on ClaudeAgentOptions.env via runner/tool_search.py::build_tool_search_env. This tells the bundled Claude CLI to defer full MCP tool schemas at a 5% context threshold — stricter than the implicit 10% default, so deferral activates earlier and saves more per-turn token cost.

Determination. Before this change, ENABLE_TOOL_SEARCH was unset, meaning the CLI applied its implicit auto (10%) policy whenever the beta was active. The explicit auto:5 policy preserves that default-on behaviour while lowering the threshold from 10% to 5%, causing MCP schemas to defer in more cases.

Eligibility rules in build_tool_search_env. The policy env dict is returned only when all three conditions hold:

  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_writetmp → 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, issue #236) handles the pause_after_compaction signal after the compactor finishes — they are sequential by SDK design with no interleave or race.

Session isolation: setting_sources and auto-memory (T10, issue #10)

Determined leak (default behaviour before issue #10). With setting_sources=None (the SDK default), the SDK loads ALL of user + project + local settings, which means the host operator's ~/.claude/CLAUDE.md, skills, hooks, and auto-memory (~/.claude/projects/<project>/memory/) bleed into every generator session. On a multi-tenant or CI host this can inject unintended prompts and tools from a completely different operator context.

Fix: setting_sources=["project"]. The generator's shared-client options (built by _client_lifecycle._build_cycle_options) pass setting_sources=GENERATOR_SETTING_SOURCES (["project"]) to make_agent_options. This scopes the session to the target repo's own .claude/ directory, dropping the host user + local scopes entirely.

Self-generation safety. When the target repo IS this repo (code-generator generating itself), the project scope still loads this repo's own .claude/ — graphify, project prompts, and project config are unaffected. Only the host operator's personal ~/.claude/ config is dropped, which is exactly the isolation goal.

Auto-memory disable. setting_sources=["project"] alone does not block auto-memory (~/.claude/projects/<hash>/memory/). The env var CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 is injected on every generator env build:

  • Subprocess path: env.py::_build_anthropic_max_env() 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 — 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.

Troubleshooting

Startup fails with "dangerous environment variables present". A previous shell session exported ANTHROPIC_API_KEY (or another billing-routing variable). unset it and try again — the tool is intentionally strict so a stray export never costs you money.

Rate limit reached. Run code-generator status to see how many minutes remain on the current pause. The next code-generator generate --continue will wait out the rest of the window and resume the same session automatically.

gh issue create fails. Confirm gh auth status reports an authenticated SSH session for github.com, and that the current directory is inside a repo with a default remote configured.

Tests don't pass after Phase 6. The orchestrator skips Phase 7 (commit) when the test runner reports persistent failures. Fix the failing tests manually, then run code-generator generate --continue to re-enter from where it stopped.

Project layout

src/code_generator/
├── cli.py                # Typer entry point: init, generate, review, status
├── env.py                # DANGEROUS_VARS, build_agent_env, assert_safe_environment
├── state.py              # State dataclasses + atomic load/save
├── prompts/              # Packaged prompt-phase-*.md files + load_prompt
├── templates/            # Project templates for `init`
├── logging_setup.py      # Per-phase log files under .code-generator/logs/
├── gh/                   # gh CLI wrapper package (core, issues, labels, milestones)
├── backends/             # IssueBackend protocol + GitHubBackend / LocalBackend
├── agents.py             # Tech-stack detector + agent selection matrix
├── runner/
│   ├── sdk_runner.py     # claude-agent-sdk wrapper, RateLimitEvent handling
│   ├── subprocess_runner.py  # CLI fallback, stream-json parser
│   ├── rate_limit.py     # Wait-and-resume main loop
│   └── retry.py          # Backoff + circuit breaker
├── orchestrator/
│   ├── phase0_complexity.py  # single vs multi-cycle decision
│   ├── phase1_plan.py        # milestone + issues
│   ├── phase2_review.py      # per-issue review
│   ├── phase3_4_implement.py # TDD loop, fresh session per issue
│   ├── phase5_closure.py     # closure + fix-issue feedback loop
│   ├── phase6_test.py        # test runner, max 3 retries
│   ├── phase7_commit.py      # Opus commit message + local commit / guarded push
│   ├── phase8_finalization.py # finalization + CI green-gate (skipped in local mode)
│   └── cycle_loop.py         # multi-cycle driver
└── commands/             # init, optimize, status, generate, review (Typer commands)

Development

git clone git@github.com:SilvioBaratto/code-generator.git
cd code-generator
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -q
ruff check src tests

Benchmarking

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

Reference

The full specification lives in requirements.md. Each prompt file (prompt-phase-*.md, prompt-review.md) is loaded verbatim by the orchestrator with {NAME} placeholders substituted at runtime — edit the prompts there, not inline in Python.

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

claude_code_generator-0.6.8.tar.gz (980.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

claude_code_generator-0.6.8-py3-none-any.whl (529.3 kB view details)

Uploaded Python 3

File details

Details for the file claude_code_generator-0.6.8.tar.gz.

File metadata

  • Download URL: claude_code_generator-0.6.8.tar.gz
  • Upload date:
  • Size: 980.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.7

File hashes

Hashes for claude_code_generator-0.6.8.tar.gz
Algorithm Hash digest
SHA256 1d35e902a91b2bc73a5e4b5147493a65e67c624b89ebd2b08642a322715c309f
MD5 8327b8a458851a3000d3bdce67a72ae2
BLAKE2b-256 5911569dea6d321148b9b85fb61a425c56b47f01b852cbd87381a75b8d1a751e

See more details on using hashes here.

File details

Details for the file claude_code_generator-0.6.8-py3-none-any.whl.

File metadata

File hashes

Hashes for claude_code_generator-0.6.8-py3-none-any.whl
Algorithm Hash digest
SHA256 24f5d0e2bb940fdc499127d934516b036f7bbb5c5cc5c3dd78e62d5f22c2cb32
MD5 9f088cff45b7454f06725ec9e3aa93da
BLAKE2b-256 0f28cc05e519b54dba0e2994aec40f6dd7f260e6be2a9085594f78a3c6f1ddf3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page