Metadata-Version: 2.5
Name: carcara-sdlc
Version: 1.0.0
Summary: Token-balanced agentic SDLC framework for Claude Code
Requires-Python: >=3.10
Requires-Dist: claude-agent-sdk<0.3,>=0.2.163
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# carcara
An agentic SDLC framework for Claude Code.

`carcara` sets up [Claude Code](https://docs.claude.com/en/docs/claude-code)
in any directory with a team of specialised subagents, SDLC slash commands and
project instructions. Each agent is pinned to the cheapest model that does its
job well, so you get most of the quality of running everything on the biggest
model with maximum thinking ("ultracode"), at a fraction of the token cost.

## Why

Running a single top-tier model with extended thinking for an entire session
spends premium tokens on everything — grepping, reading logs, running tests,
writing docs — and keeps all of that output in one ever-growing context.
carcara balances this by:

- **Model routing** — Opus only where reasoning pays off (planning), Sonnet
  for orchestration, coding and review, Haiku for search, test runs and docs.
- **Context isolation** — verbose work (code search, build/test logs) happens
  inside subagents, which return compact, fixed-format summaries.
- **Triage** — `/sdlc` sizes every task (S/M/L) and only spins up the
  explorer/architect/reviewer when the task warrants it.
- **Bounded loops** — at most two fix iterations before asking you.
- **Targeted thinking** — extended thinking is reserved for design questions.

## Install

Requirements: Python 3.10 or newer, on Linux, macOS or Windows (native; WSL
is not required). On Windows, Claude Code runs carcara's hooks through Git
Bash, so install Git for Windows.

```sh
pipx install carcara-sdlc    # or: uv tool install carcara-sdlc
```

For the latest unreleased code, install from source:

```sh
git clone https://github.com/oshogun/carcara.git && cd carcara
pipx install .          # or: uv tool install .
# or straight from git: pipx install git+https://github.com/oshogun/carcara.git
```

## Using carcara from Claude Code

After `carcara install` (and `carcara` on your `PATH`), start Claude Code in
the project and **just ask for a change** ("add rate limiting to the public
API"). No slash command is needed.

What happens:

- The `CLAUDE.md` routing rule and the `carcara` skill hand code-change
  requests to `carcara run`, which runs in the background (explore → plan →
  implement → test → review, one Agent SDK session per stage). Questions and
  explanations are answered normally.
- The main session cannot edit project files itself (a hook blocks it), and no
  session can change `.claude/settings*.json` or `.claude/skills/carcara/`.
- When the run finishes, Claude summarises the result and points you to
  `carcara diff <id>` / `carcara status <id>`. Paused runs (plan approval,
  failing tests, budget) are listed in each prompt's context and offered for
  resume.

Approvals: carcara's PreToolUse hook is the only approver of carcara commands.
It allows the exact `carcara run|status|diff` forms the skill uses and asks you
for every other command that invokes carcara. Approving a plan (`--yes`),
accepting failures (`--accept-failures`), API billing (`--use-api-key`),
changing the budget (`--max-budget-usd`) and resuming a run that hit its budget
always show you a permission prompt; that prompt is your decision.

Opt-outs:

- `carcara routing off` / `carcara routing on` / `carcara routing status`
  (per project, a `.carcara/routing-off` flag file);
- `CARCARA_OFF=1` in the environment (per session);
- `carcara install --no-routing` (no skill, no hooks; `settings.json` and
  `CLAUDE.md` as installed by 0.2.0).

Trust: interactive Claude Code asks you to trust the folder on first start, and
project permission rules apply only after that. carcara's own commands do not
depend on trust (the hook approves them), but a malicious repository can ship
its own `.claude/` config, so only trust folders you would run code from.
`claude -p` works too.

The `/sdlc*` slash commands stay available as an optional manual path.

## Usage

```sh
carcara [install] [options] [target-dir]   # target-dir defaults to the current directory
carcara uninstall [options] [target-dir]   # remove what install added (see below)
carcara profiles                           # list profiles
carcara run "<task>"                       # run the pipeline headlessly (see below)
carcara status [RUN_ID] [--json|--plan]    # show a run (default: active, else latest)
carcara diff [RUN_ID] [--stat]             # a run's changes since its base (secrets excluded)
carcara routing on|off|status              # Claude Code routing for this project
```

`install`, `uninstall`, `profiles`, `run`, `status`, `diff`, `routing` and `hook` as the
first argument are subcommands; to
target a directory with one of those names use `carcara -- <dir>` or
`carcara install <dir>`.

| Option | Description |
|---|---|
| `-p, --profile NAME` | `economy`, `balanced` (default), `quality`, or a path to a custom profile file |
| `-f, --force` | overwrite existing carcara files in `.claude/` |
| `-n, --dry-run` | show what would be done without writing |
| `--no-routing` | don't route code changes through `carcara run` (no skill or hooks; removes them) |
| `--strict-policy` | also apply the carcara tool policy to carcara subagents in interactive sessions |
| `-l, --list-profiles` | list profiles and their model routing |
| `-V, --version` / `-h, --help` | version / help |

Then start Claude Code in the target directory and ask for a change (see
[Using carcara from Claude Code](#using-carcara-from-claude-code)), or run
`/sdlc add rate limiting to the public API` explicitly.

### What gets installed

```
<target>/
├── CLAUDE.md                     # managed section between carcara markers
└── .claude/
    ├── settings.json             # main-session model, safe permissions, routing hooks (merged)
    ├── agents/
    │   ├── explorer.md           # read-only code search → file:line findings
    │   ├── architect.md          # plans non-trivial changes
    │   ├── implementer.md        # writes code + tests
    │   ├── test-runner.md        # runs build/lint/tests, summarises failures
    │   ├── reviewer.md           # high-signal diff review
    │   └── doc-writer.md         # updates affected docs
    └── commands/
        ├── sdlc.md               # /sdlc <task>: triaged full pipeline
        ├── sdlc-plan.md          # /sdlc-plan <task>: explore + plan only
        ├── sdlc-build.md         # /sdlc-build [plan]: implement + test
        ├── sdlc-test.md          # /sdlc-test [scope]
        └── sdlc-review.md        # /sdlc-review [focus]
    └── skills/carcara/SKILL.md   # routing skill (omitted with --no-routing)
```

`settings.json` denies the `Read` tool on `.env*` and `secrets/**`. This is a
guard rail, not a sandbox: agents with `Bash` could still read such files via
shell commands, so keep real secrets out of the working tree or add your own
`Bash(...)` deny rules.

Re-running is safe: existing files in `.claude/` are skipped unless
`--force` is given; an existing `settings.json` is merged (your permissions,
hooks and model are kept; carcara's hook entries are replaced); and an
existing `CLAUDE.md` keeps its content — carcara
only appends or updates the section between `<!-- carcara:begin -->` and
`<!-- carcara:end -->`.

### Uninstalling

`carcara uninstall [target-dir]` removes what `carcara install` added: the
carcara agents, commands and `carcara` skill, carcara's hooks, permissions and
`model` in `settings.json`, the marked `CLAUDE.md` section and carcara's files
in `.carcara/`. Files and empty directories that install created are deleted;
install followed by uninstall gives back the original files (an existing
`settings.json` is rewritten as 2-space-indented JSON if anything in it changed).

| Option | Description |
|---|---|
| `-n, --dry-run` | show what would be removed without changing anything |
| `-f, --force` | also remove carcara agents and commands you edited |
| `--purge` | also delete the run history in `.carcara/runs` |

Kept: agents and commands you edited (`keep ... (modified)`, unless `-f`), a
`carcara` skill that carcara didn't write, your own `settings.json` entries
(including permissions identical to carcara's that you had before installing,
and a `model` you changed), and `.carcara/runs` unless `--purge`. Like
install, it refuses to run in your home directory or `~/.claude`.

Install records what it added in `.carcara/install.json`. Projects installed
before that file existed have no record: uninstall then removes every
permission from carcara's template (also identical ones you added yourself;
it prints a warning), keeps `model`, and treats a blank line before the
`CLAUDE.md` section as the separator install added.

### Pipeline (`/sdlc`)

| Size | Flow |
|---|---|
| S | implementer → test-runner (→ reviewer if security, data handling or public APIs are touched) |
| M | explorer → implementer → test-runner → reviewer |
| L | explorer → architect → **your approval** → implementer → test-runner → reviewer → doc-writer |

## carcara run

`carcara run "<task>"` runs the same pipeline headlessly from the terminal,
one Claude Agent SDK session per stage.

Requirements: the [Claude Code CLI](https://code.claude.com/docs/en/setup) on
`PATH` and authenticated.

**Billing.** Each stage runs through the `claude` CLI using your Claude Code
login, so by default runs draw on your Claude subscription (Pro/Max) usage
limits. `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` are hidden from the CLI
(with a notice) unless you pass `--use-api-key`, which bills the pay-per-token
API instead; on `--resume` the run keeps the choice it was started with, unless
`--use-api-key` is passed (the run then stays on API billing).
With `--project-settings`, the project's `.claude/settings.json` is loaded too, and
an `env.ANTHROPIC_API_KEY` or `apiKeyHelper` there can still switch the CLI to
API billing.
Explicit provider settings (`CLAUDE_CODE_USE_BEDROCK`, `..._VERTEX`,
`..._FOUNDRY`) are left alone. Reported costs are the SDK's estimate at API
prices: on a subscription they are not charged, but they approximate how much
plan usage the run consumed.

```sh
carcara run "add rate limiting to the public API"
carcara run --dry-run                 # print the stage table, no backend calls
```

Stages per size (triage picks the size unless `--size S|M|L` is given):

| Size | Stages |
|---|---|
| S | implement (→ **approval** if low-verifiability paths changed) → test (→ review with `--review` or when the verifiability gate fired) |
| M | explore → plan (→ **approval** with `--approve-plan` or low-verifiability paths) → implement → test → review |
| L | explore → plan (architect) → **approval** → implement per plan step → test → review → docs |

**Approval gate.** The run waits for you when one of these triggers fires:

| Trigger | When |
|---|---|
| `size` | every L plan |
| `flag` | M plans with `--approve-plan` |
| `revision` | a plan re-made from `--feedback` |
| `verifiability` | an M/L plan step's files, or (for S) the files changed since the run base, match a low-verifiability path pattern |

The default patterns are `.github/**`, `**/migrations/**`, `**/auth/**`,
`**/policy*` and `**/policy/**`. To change them, see [Project config](#project-config-carcaraconfigjson).
S runs have no plan, so the verifiability gate runs after implement and before
test/review. It runs again after each fix-round implement, so a path first
touched by a fix still gates and forces review. It checks the files git shows as changed since the run base, plus
any the implementer reported, so an omitted file still gates. The changes are
then already in the working tree (uncommitted;
inspect them with `carcara diff <id>`). Approving continues with test and
review. Rejecting fails the run but does not revert the changes. `--feedback`
is refused at this gate. The same post-implement check also catches M/L
changes that touch a pattern even though no plan step listed it. The gate
prompt starts with `Gate: <reason>`, and `state.json` records
`gate: {trigger, paths, stage}`, where `stage` is `plan` or `post-implement`.
When the verifiability gate fires, review runs whatever the size (the split
review under `--ultra`), so the change gets an `unverified` inventory. The
report's `review:` line then ends with `(forced: verifiability gate)`, and
`state.json` records `review_reason`. While the current diff touches a gated
path, review runs once even when the fix loop ends with failing tests (or before
`--accept-failures` finishes the run), on S, M and L runs alike; its findings
are informational.
A gate from an earlier fix round of the same run keeps forcing review.

On a TTY you are prompted; `--yes` auto-approves; without a TTY the run stops with
exit code 3 and is continued later with `--resume <id> --yes`. If tests still
fail, the fix loop runs at most 2 iterations, then exits with code 4.

**Unverified assumptions.** The reviewer must list in `unverified` what its
verdict relies on but cannot check: claims about systems outside the repo
(`external`, e.g. a package name being free or a secret being configured),
interpretations of the request (`normative`), and behaviour no test exercises
(`untested`). The list has at most 20 items of at most 200 characters each.
When the changed files match a low-verifiability pattern, the prompt adds
path-specific questions. For `.github/**`, for example, it asks which accounts,
package names, environments or secrets the change assumes. carcara assigns the
ids (`U1`, `U2`, ...), and they stay stable across fix rounds; an item whose
text changes gets a new id. When
[probes](#project-config-carcaraconfigjson) are configured, an `external` item
may name one. carcara runs it after the review and runs it again when the
item's probe (name, arg or expect) changes. An item whose current probe result
confirms the expectation is marked resolved.

**Report fields.** Besides files, tests, review and cost, `report.md` and
`carcara status` show:

- `extent: N files, areas a, b, fix rounds K [carcara/extent-1]`: the raw
  extent facts. `areas` lists the first path components, capped at 10, with
  `(+)` when truncated. The bracketed rule version tells you how the facts were
  computed. No "observed size" is derived from them.
- `gate: <trigger> (paths: ...)`: shown when the run was gated.
- `unverified: N open (external x, normative y, untested z)`: followed by one
  line per open item, `- U1 [external] text (probe: <outcome> <result>)`.

`carcara status --json` includes the raw `gate`, `unverified`, `probe_results`
and `extent` values.

| Option | Description |
|---|---|
| `-p, --profile NAME` | profile name or `.env` path (default: the profile recorded by `carcara install`, else `balanced`) |
| `--size S\|M\|L` | skip triage and use this size |
| `--yes` / `--approve-plan` | auto-approve the plan gate / gate M plans too |
| `--max-budget-usd USD` | cap the total estimated cost (exit 5 when exceeded); stored with the run and kept on `--resume` unless given again; on a subscription this is a proxy for plan usage |
| `--use-api-key` | let the CLI use `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` (pay-per-token API billing) instead of your subscription login |
| `--dry-run` | show stages, models and tools without calling Claude |
| `--plan-only` | stop after the plan |
| `--triage-only` | run only triage and record its size, range and uncertainty kind (about one triage stage of cost, versus explore + plan for `--plan-only`); needs no clean tree and changes no code; Urutau records it as `plan_only` with the triage rationale; cannot be combined with `--size`, `--plan-only` or `--resume` |
| `--resume RUN_ID` / `--list` | continue a stored run / list stored runs |
| `--reject` | with `--resume`: reject the plan awaiting approval |
| `--feedback TEXT` | with `--resume`: re-plan with this feedback, or guide a needs_human retry; `-` reads stdin |
| `--accept-failures` | with `--resume`: finish a needs_human run, accepting its failures |
| `--allow-dirty` | allow uncommitted changes (clean tree required by default); the diff base is a snapshot of your uncommitted work |
| `--unrestricted-bash` | turn off the implementer/test-runner Bash deny-list for this invocation only (prints a warning; pass it again with `--resume`) |
| `--review` | also review S-sized changes (review always runs when the verifiability gate fires) |
| `--ultra` | fan out explore and review into parallel stages (see [Ultra mode](#ultra-mode---ultra)); kept on `--resume` |
| `--project-settings` | load the project's Claude Code settings and CLAUDE.md (note: their `env` / `apiKeyHelper` can re-enable API billing) |
| `--cwd DIR` | project directory (default `.`) |

The task may be `-` to read it from stdin. Long options must be spelled out
(abbreviations such as `--ye` are rejected). Only one run is active per
project at a time (exit 6 otherwise), and `carcara run` refuses to start inside
a carcara stage. When run from Claude Code (`CLAUDECODE` set) the approval gate
never waits on stdin: it stops with exit 3 instead. See
[Using carcara from Claude Code](#using-carcara-from-claude-code) for routing.

`carcara status [RUN_ID]` prints a run's report and resume command (`--json`
for machine-readable output, `--plan` for the stored plan); `carcara diff
[RUN_ID] [--stat]` shows the working tree's changes since the run's base,
excluding secrets and `.carcara/`. Both default to the active run, else the
latest one.

The budget is checked from the estimated stage costs the SDK reports. A stage
attempt that errors out but still returns SDK usage counts toward the total and
the `--max-budget-usd` cap, and appears in the report as `<stage> $x.xx
(failed)`. An attempt that dies before the SDK reports a result has unknown
cost: it is flagged as uncounted (`stage_error` with `"uncounted": true` in
`events.jsonl`, `uncounted_stages` in `carcara status --json`, and a note on
the report's total line), and the cap may be exceeded by that unknown amount.

**Stage stops and follow-ups.** Each `stage_error` event (and each entry in
`failed_attempts` in `state.json`) records why the attempt stopped: `subtype`,
`stop_reason`, `num_turns` and `duration_ms`. The test-runner agent (used only
when no [test commands](#project-config-carcaraconfigjson) are known) has a wall-clock
limit of 900 s (`RunOptions.stage_timeout_s`); other stages have none. A timed-out
attempt has `stop_reason: "timeout"` and its cost is uncounted. When a stage
ends without structured output, or times out, carcara sends one cheap follow-up
in the same session (at most 3 turns and 120 s, `RunOptions.followup_timeout_s`) asking for the results it already
has. That attempt is logged as a `stage_followup` event. If the follow-up returns a
result, the stage completes (`stage_completed` with `"followup": true`) and the work is not
repeated. If it fails, its `stage_error` carries `"followup": true`. The test
stage then stops as `failed` (resume with `carcara run --resume`). Other stages
that are missing their output get one full retry.

The test-runner's Bash is further restricted: calls with `run_in_background`
and `until`/`while` loops that `sleep` are denied, so it runs each command once
in the foreground (under `timeout`) instead of polling a background job.
`--unrestricted-bash` lifts these rules along with the rest of the deny-list.

**Run directory.** Each run is stored in `.carcara/runs/<id>/`: `state.json`,
`events.jsonl` (append-only log) and `report.md`. The base commit is recorded
in the state.

| Exit code | Meaning |
|---|---|
| 0 | done (or plan-only / triaged) |
| 1 | error |
| 3 | awaiting plan approval |
| 4 | needs human (e.g. tests still failing after the fix loop) |
| 5 | budget exceeded |
| 6 | another run is active (no run was started) |
| 130 | interrupted (state saved; resume with `--resume`) |

**Pruning runs.** Run directories are kept until you remove them with
`carcara runs prune`:

```bash
carcara runs prune --older-than 30d --dry-run   # show what would go
carcara runs prune --keep 20                    # keep the 20 newest runs
carcara runs prune --older-than 30d --keep 20   # both rules must match
```

`--older-than AGE` (`12h`, `30d`, `2w`) selects runs last updated more than
AGE ago (falling back to the creation time). `--keep N` keeps the N newest
runs; protected runs count toward N, so the last N runs always stay. With
both flags a run is deleted only if it is outside the `--keep` window *and*
older than `--older-than`. At least one of them is required. The run holding
the live `.carcara/active.json` lock, runs that are `running`,
`awaiting_approval`, `needs_human` or `budget_exceeded`, and runs whose state
cannot be read are never deleted. Try `--dry-run` first; `--json` prints the
decisions. The exit code is 1 if a deletion failed.

### Ultra mode (`--ultra`)

`carcara run --ultra` runs the read-only roles in parallel:

- **Explore (M/L).** A small main-model `scope` stage splits the task into
  0-4 independent areas. With 2 or more areas, one `explore:<area>` stage runs
  per area in parallel, and their summaries and findings are merged (findings
  deduped) before the plan. With 0 or 1 area, the single `explore` stage runs
  as usual. S runs have no explore stage.
- **Review.** Each review round runs 3 parallel dimension reviews
  (`review-dim:correctness`, `review-dim:security`, `review-dim:tests`). One
  `review` merge stage then checks their findings against the code, drops
  false positives and merges duplicates. The merge is the review that counts:
  the fix loop, the report's `review:` line, unverified assumptions and Urutau
  data come only from it. Fix rounds use the same split (`fix-1:review-dim:*`).
  A dimension or merged finding of major or higher needs the same kind of
  floor reason, in its `reason` field: `correctness`, `security`,
  `goal_defeating` (this includes a missing test for the behaviour the task
  asks for) or `low_verifiability`. Without one it is capped at minor and
  listed as an `untested` unverified item, so a missing test for an edge
  case does not start a fix round (#40). Before that cap, a `correctness` or
  `security` dimension review with a reasonless major or blocker is asked
  once, in the same session (`review-dim:<dim>:reason`), to add a `reason` or
  lower the severity; this is logged as a `review_reason_followup` event with
  `resolved`. A failed or still reasonless follow-up leaves the cap in place.
  The `tests` dimension gets no follow-up (#45). The merge keeps each
  finding's source `dimension`. The plain (non-ultra) review is unchanged.
- **Refute.** After the merge, up to 6 non-nit findings (most severe first)
  each get a parallel `review-refute:N` stage. The refuter tries to disprove
  the finding against the diff and re-rates its severity. Severities are only
  raised. A finding the refuter flags as goal-defeating or hard to verify
  (security policy, shell parsing, resume and the like) becomes at least
  major. A refuter's rating of major or higher counts only with a floor
  reason in `raise_reason` (goal-defeating, low-verifiability, a security
  defect or a correctness defect); without one the raise is capped at minor.
  A merged finding capped for lacking a `reason` is refuted at minor, so only
  such a justified raise makes it serious again. It is treated as contested,
  like a serious finding: a disproof drops it only with evidence or a second
  refuter's agreement, otherwise it is kept (`kept_unverified`) with its
  `untested` item (#45).
  A missing or weak test is at most minor unless the untested path is
  goal-defeating or hard to verify, so test gaps alone do not start fix
  rounds. Refuters still never lower a severity (open question U3 from #22).
  A serious finding (major or higher, as merged or as re-rated or
  flagged) is dropped only in two cases. Either the refuter quotes an added
  diff line near the finding as evidence, which is never enough for blockers
  or flagged findings. Or a second, independent `review-refute-2:N` agrees.
  Otherwise it is kept. The verdict is then recomputed from what survives.
  Findings and diffs are fenced as untrusted data in these prompts.

Fan-out is capped at 4 explore areas, 3 dimension reviews and 6 refuters per
round (plus up to 6 second refuters). Each review round costs roughly 4x a
normal review, plus one review-sized stage per refuter. With `--max-budget-usd`, the remaining
budget is split evenly across the parallel siblings. Implementers, test, docs
and probes stay sequential in one working tree; v1 only fans out read-only
roles. Plan steps may also list `depends_on` (ids of the steps they need);
the steps are run in dependency order, still one at a time.

The flag is stored with the run, so `--resume` keeps the ultra stages even
without `--ultra`. Finished parallel stages are replayed on resume and only
the missing ones rerun. The report adds a `Parallel stages: <keys> ($x.xx)`
line and, when refuters changed anything, a
`Refutation: N checked, N re-rated, N disproved` line. It adds
`(N escalated)` after the re-rated count when refuters raised severities, and
`N raises capped` when unjustified raises were capped (also logged as a
`review_raise_capped` event). Merged majors capped for lacking a reason add
`N majors capped (no reason)` to that line (or a `Review:` line of their
own), and are logged as a `review_severity_capped` event. A `Capped findings:`
list follows, one line per capped finding:
`- major -> minor [security] a.py: <issue> (capped: no reason)`, plus the
refutation outcome (dropped, disproof kept unverified, or restored) when
there is one, so a wrongly capped major can be spotted. `carcara status`
shows the same lines while a run is in progress, and `carcara status --json`
has them under `severity_capped` (per review key: `index`, `before`, `path`,
`issue`, `dimension`, `why`). `--dry-run --ultra` shows the extra rows.

### Tool policy

Each role gets only its own tools. Read-only roles (explorer, reviewer) may run
Bash only from a flag allowlist (git status/diff/log/show, ls, rg, grep, find,
cat, wc, head, tail; no shell metacharacters, no unknown options). Reads are
confined to the repo, and `.env`, `.env.*` and `secrets/` are refused (case-
insensitively, symlinks resolved). Write tools are confined to the repo and
refuse `.git`, `.claude` and `.carcara`. The policy is
enforced by a PreToolUse hook, which applies in every permission mode;
`bypassPermissions` is never used. The symlink-related protections are
tested only where the OS permits creating symlinks (on Windows, that needs
Developer Mode or an elevated shell); elsewhere those tests are skipped.

implementer and test-runner may run any Bash command except those caught by a
deny-list: git subcommands that create commits or move refs (`push`,
`commit`, `reset`, `merge`, `rebase`, `cherry-pick`, `revert`, `am`,
`commit-tree`, `update-ref`, `pull`, `symbolic-ref`, `stash`/`notes` except
`list`/`show`, `tag` except listing, `branch -f/-d/-m/-c`, `checkout -B`,
`switch -C`), `git -c alias.*` and `--config-env`, `git clean -f`, curl/wget piped or
substituted into a shell or interpreter, shell access to `.env`/`secrets/`
paths, shell writes (redirections, `tee`, `rm`/`mv`/`cp`/`mkdir`/...,
`sed -i`/`perl -i`/`ruby -i`) outside the repo or into `.git`, `.claude` or
`.carcara`, and interpreter one-liners (`sh -c`, `python -c`, `node -e`,
`perl -e`, `ruby -e`, `eval`, ...) whose code names one of those
directories, even to read it (use Read or `cat` instead). Reads outside the
repo stay allowed. Wrappers such as `sudo -u root`, `nice -n 5`, `env -u X`,
`env -S '...'`, `timeout -s KILL 5` and `xargs -n 1` are looked through, including
their option values. To turn the deny-list off, pass
`carcara run --unrestricted-bash`. It applies to that invocation only (a
resume needs it again), prints a warning on stderr and is recorded in the
run's events.

Probes (see [Project config](#project-config-carcaraconfigjson)) are not agent
tools. carcara itself runs them after the review, and they never go through
agent Bash. They are unauthenticated HTTP GETs to URL templates from the
project's allow-list, sent with a 5 s timeout and with no credentials or
cookies. Only the `{arg}` path segment comes from the reviewer, and it is
URL-quoted. Only the status code is read, and the run records just the item
id and a short result. The agent Bash policy above is unchanged.

Known risks:

- implementer and test-runner Bash is guarded only by a best-effort deny-list.
  Variables, `eval`, base64, `cd`, aliases, or a script written into the repo
  and then run all bypass it. It is string matching on the command line, so a
  wrapper option missing from its table can also hide a command. The opt-out
  is a CLI flag resolved before the run starts, so a stage cannot turn it on
  (files a stage creates have no effect). The real safety net is still the clean-tree
  requirement and the base commit recorded for each run. A per-project
  test-command allowlist is not implemented yet.
- Writes into `.git`, `.claude` or `.carcara` can still get through Bash:
  scripts written to disk and then run, heredoc/stdin programs
  (`python - <<EOF`), paths built at runtime (string concatenation,
  variables, base64), `awk -i inplace`, ex/vi/ed and `dd of=` are not caught
  (#19). sed is parsed with GNU grammar, so BSD/macOS `sed -I` (capital I)
  and `sed -i .bak` (backup suffix as a separate argument) are not caught; sed
  script writes via `w FILE` within the script or `s/a/b/w FILE` syntax are
  missed too.
- Grep and recursive searches may still surface secrets in a searched directory.
- `setting_sources` is empty by default, so project settings and CLAUDE.md are
  not loaded into stages; pass `--project-settings` to opt in.

## Urutau integration

`carcara run --issue N [--repo owner/name]` takes the task from GitHub issue N
(`gh issue view N -R owner/name --json title,body`; the repo defaults to the
git `origin` remote). Any task text you pass as well is appended as additional
instructions. An `--issue` run always triages (`--size` still forces a size)
and, when Urutau reporting is on, reports its lifecycle to
Urutau through the `record_run` MCP tool.

Reporting is on only when `--issue` is given **and** a token is available.
`--no-urutau` turns it off for one invocation. Without it, carcara behaves
exactly as it does without `--issue`. The report and `carcara status` (and
`--json`) show whether reporting was on and the last `record_run` outcome
(claim held, open unverified items by kind).

Configuration (environment variables win over the file):

| Setting | Env var | `~/.config/carcara/urutau.json` key |
|---|---|---|
| Urutau base URL (default `http://127.0.0.1:8787`) | `URUTAU_URL` | `url` |
| MCP token | `URUTAU_MCP_TOKEN` | `token` |

The file lives under `$XDG_CONFIG_HOME/carcara/` when that is set; keep it out
of any repo. To get a token, open Urutau's **Users** page → **Agent
integrations**. The token is sent only in the `Authorization: Bearer` header to
exactly `<base>/mcp`: redirects are not followed with it, and any request to
another path, host or scheme is refused. Each call opens its own short-lived
MCP session. The token is never logged, never written to `state.json`, the report or
the events, and never passed to agent stages (`URUTAU_MCP_TOKEN` and
`URUTAU_URL` are removed from their environment).

What is sent: the repo, issue, carcara run id and status; triage range
(`S`, `M`, `L`, `S-M`, `M-L`, `S-L`) and main uncertainty kind (`external`,
`normative`, `untested`, `none`; both omitted when `--size` forced the size);
fix rounds; cost; changed paths that pass Urutau's path rules (at most 200)
plus a count of the omitted ones; top-level areas; `observedBy:
carcara/extent-1`; and merge SHAs when known. At pauses and at the end it also
sends the unverified items, withdrawals of items that were reworded or are no
longer listed, confirmed probes and findings (`plan_only` runs send the plan's
goal, steps and acceptance criteria as findings, at most 4000 characters). Code, prompts, diffs and the token are never sent. The card's
estimate (size, confidence) is read with `get_board` at start and stored in
the run state for comparison.

Claim lifecycle:

- Start: `running` claims the issue's card with a 30-minute lease. If another
  run holds the claim, the run stops before doing any work. Setup errors (no
  board for the repo, repo not allowed, bad token, Urutau unreachable) also
  fail fast with a message saying what to do.
- While stages run, a heartbeat re-sends `running` every 10 minutes to renew
  the lease. A crashed run stops renewing, and its claim expires 30 minutes
  later.
- Pauses (`awaiting_approval`, `needs_human`, `budget_exceeded`) hold the claim
  with no lease, until a person releases it from the card. An interactive gate
  sends `awaiting_approval` before it asks (heartbeats pause while it waits)
  and `running` again once the run goes on.
- `--resume` re-sends `running`. If another run has claimed the card since,
  the resume stops before doing any work. Resuming a run whose end Urutau
  already recorded (for example a `failed` run) starts a new Urutau run with
  the id `<run id>-r2` (then `-r3`, ...), since an ended Urutau run never
  changes.
- The end (`done`, `failed`, `rejected`, `plan_only`) releases the claim.

Errors on heartbeats, pauses or the final call only add a warning event: the
run's own result and exit code are never lost because Urutau is down.

## Profiles

| Role | economy | balanced | quality |
|---|---|---|---|
| main session | sonnet | sonnet | opus |
| architect | sonnet | opus | opus |
| implementer | sonnet | sonnet | opus |
| reviewer | haiku | sonnet | opus |
| explorer | haiku | haiku | sonnet |
| test-runner | haiku | haiku | haiku |
| doc-writer | haiku | haiku | sonnet |

Profiles live in `src/carcara/data/profiles/*.env`. To customise, copy one and
pass its path (also accepted by `carcara run -p`):

```sh
cp src/carcara/data/profiles/balanced.env my.env    # edit MODEL_* values (opus, sonnet, haiku, inherit or a model id)
carcara --profile ./my.env --force .
```

`carcara install` records the chosen profile in `.carcara/profile` (a name, or
the absolute path of a custom `.env` file; `.carcara/` is gitignored, so a fresh
clone has no record). `carcara run` and `carcara run --dry-run` use it unless
`--profile` is given, and fall back to `balanced` when nothing is recorded. If
the recorded profile can't be loaded (e.g. the `.env` file moved), the run
warns on stderr and uses `balanced`. `--resume` always keeps the profile the
run started with. The run's first stderr line shows the profile:
`carcara: run <id> started (profile quality)`.

## Project config (`.carcara/config.json`)

This file is optional, separate from profiles, and read by `carcara run` from
the project directory. Unknown keys are an error. A run snapshots it at start
(`project_config` in `state.json`), and `--resume` uses the snapshot, so editing
the file mid-run has no effect on that run. Agents cannot write it.

```json
{
  "verifiability_paths": [".github/**", "**/migrations/**", "**/auth/**", "**/policy*", "**/policy/**"],
  "probes": {"pypi-name": "https://pypi.org/pypi/{arg}/json"},
  "test_commands": [".venv/bin/ruff check .", ".venv/bin/ruff format --check .", ".venv/bin/pytest -q"],
  "test_timeout_s": 900
}
```

| Key | Meaning |
|---|---|
| `verifiability_paths` | glob patterns (posix paths; `**/` matches any number of directories, `*` matches within one component) that trigger the [approval gate](#carcara-run). A list replaces the defaults shown above; `[]` turns the trigger off. |
| `probes` | named, read-only checks the reviewer may reference from `external` unverified items. Each value is an `http`/`https` URL with exactly one `{arg}` placeholder and no `user:pass@`. Without probes, nothing is fetched. |

A probe result counts as `exists` for a 2xx status and `absent` for 404/410.
It is `confirmed` or `contradicted` against the item's expectation, and any
other status, or an error, counts as `inconclusive`. Probes do not follow
redirects (a 3xx is `inconclusive` with result `redirect`) and ignore proxy
environment variables.

**Test commands.** The test stage runs `test_commands` itself, in order, as
shell commands in the run's directory, with stdin closed and each in its own
session. Every command runs even after an earlier one fails. A command that
exceeds `test_timeout_s` (per command; default 900 s, `RunOptions.test_timeout_s`
overrides it) is killed with its whole process group and reported as a failure
that says it timed out. When every command exits 0, the `test` result is built
without any model call and costs nothing. When any fails, one short no-tools
call on the test-runner model turns the failing commands and an output excerpt
into the `failures` list. That call is not memoised on its own: its cost and
usage are recorded on the `test` stage, and a resume that finds no `test` entry
reruns the commands and summarises the fresh results. If that call fails, carcara
lists each failing command with its output tail instead.

Without `test_commands`, carcara discovers them once per run and stores them in
`state.json` (`test_commands`; `--resume` reuses them). `[tool.ruff]` in
`pyproject.toml` gives `ruff check .` and `ruff format --check .`;
`[tool.pytest.ini_options]`, a `pytest.ini` or a `tests/` directory gives
`pytest`. Each tool runs from `.venv/bin/` when the project has it there. The
`test_commands_resolved` event records the list and its source (`config`,
`discovered` or `none`). If nothing is found, or `test_commands` is `[]`, the
test-runner agent runs the tests as before. Discovery can pick a tool the
project does not install. A command that is not found then fails the stage
(exit 127), so set `test_commands` explicitly in that case.

These commands come from the repository and are not checked against the agents'
[Bash deny-list](#tool-policy). Trust them as you would the project's own scripts.
They run with the same environment filtering as agent stages: `URUTAU_MCP_TOKEN`
and `URUTAU_URL` are removed, API-key variables are removed unless
`--use-api-key` is set, and `CARCARA_STAGE=test-runner` is set.

`pyproject.toml` is not gated by default. Only its version/publish sections
matter for releases, and patterns match whole paths. If you want every change
to it gated, add `"pyproject.toml"` to the list.

## Development

```sh
pip install -e ".[dev]"
ruff check .
pytest -q
tests/fixtures/regen_golden.sh     # regenerate golden installer output after template/profile changes
```

`regen_golden.sh` needs bash; on Windows, run it from Git Bash or WSL.

See `tests/fixtures/golden/README.md` for the golden and install-snapshot fixtures.

### Release

1. Bump `version` in `pyproject.toml` and commit to `main`.
2. Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z`.
3. The `release` workflow (`.github/workflows/release.yml`) checks that the
   tag matches the `pyproject.toml` version, builds and smoke-tests the sdist
   and wheel, and publishes them to PyPI.

One-time setup: on PyPI, add a (pending) trusted publisher for project
`carcara-sdlc` with owner `oshogun`, repository `carcara`, workflow `release.yml`
and environment `pypi`; then create a `pypi` environment in the GitHub repo
settings (optionally with required reviewers). Publishing uses OIDC trusted
publishing, so no API token is needed.
