Unattended delivery, run records and cost
How to let Claude take a feature from spec approval to a running dev without you watching. This works on GitHub Free with private repos (ADR-028: Unattended delivery without branch protection).
The flow
Section titled “The flow”you: /component:spec <feature> → answer its questions → /component:spec approve <NNN> (the only decision)unattended: /component:ship <NNN> plan architect writes plan.md (skipped when a valid plan exists) build failing acceptance tests → coders until green → QA → reviewer (verification.md) → image pr cplat component ship pr: pushes feature/<spec>, opens the PR land cplat component ship land: waits for every check; red → logs → coder → push (≤ 3 rounds); green → squash merge deploy cplat component ship deploy: the service's CI publishes sha-<40>, pin-dev merges the pin PR after CI, Argo syncs staging a green dev smoke dispatches pin-staging (self-merging PR), Argo syncs, staging smoke (spec 093)you: check staging → /instance:promote <component> --to prod (evidence summary, reviewed PR)The run stops on its own only when it is done or escalated. While it is unfinished, the shared plugin’s Stop hook refuses to let the session end (spec 066). It escalates (cplat component ship escalate '<reason>') when a rule needs you: a merge conflict after its re-plan (a task is re-planned once, a build at most 3 times), a changed acceptance test, a security finding with no fix released, or a bump that breaks a gate, a decision the spec does not answer, red CI after three fix rounds, a closed pin PR. The hook holds only the owner: the session that started or resumed the run (its owner) is held; any other session is not, and a run without an owner holds nobody and says so at each stop. Progress is a new commit on the feature branch or a phase change (for a /system:ship run also a repo’s state change). After two stops of the owner without progress the run escalates as no progress: 2 stops in phase <phase> without a new commit or phase change, with the last message as an excerpt. The cause comes from a fixed rule: a matching escalation rule on the last message, else code when the evidence is red (untraced criteria, red tests, red required checks), else decision. See ADR-070: The Stop hook holds only the run’s owning session, and only while it makes progress. /component:ship <NNN> resumes where it stopped (and makes the resuming session the owner). cplat component ship status shows the phase at any time.
Dependency patches
Section titled “Dependency patches”When the security gate is red because of dependencies with a released fix, the run bumps them itself (cplat deps patch, spec 095), re-runs the gates and commits the bump, or reverts it exactly. The PR body lists each bump under “Dependency patches” (a major bump is marked) and the run record has a patches section. At most 2 rounds; a finding without a fix, or a bump that breaks a gate, escalates.
Run it without a terminal session
Section titled “Run it without a terminal session”cplat headless "/component:ship 012" # in the repo; resumes in fresh sessions until done or escalatedcplat headless "/component:ship 012" --quiet 23:30-07:00 # never start inside that window (wait for it to end)cplat headless "/component:ship 012" --max-runs 6 --max-minutes 240 --model opusEach session is claude -p <prompt> --permission-mode acceptEdits: file edits inside the repo are accepted, the repo’s committed allowlist (.claude/settings.json) governs every other tool, and everything else is refused instead of prompting. (Until 4.5.0 this was dontAsk, which also refused every file edit, because no allowlist grants Edit or Write.) git push origin main and gh pr create are ask rules there, so they are refused; cplat component ship does the narrow pushes and the PR itself. Logs are in ~/.cache/sdlc-foundry/headless/<repo>/; summary.log has one line per run, and a desktop notification says how it ended (exit 0 shipped, 1 needs you, 3 inside the quiet window with --no-wait).
The coordinator session runs on Sonnet unless you choose otherwise (spec 100, ADR-036 — The coordinator runs on Sonnet; project and user settings choose otherwise); cplat headless always passes --model. Precedence: the --model flag, then coordinator_model in the repo’s .platform-settings.yml, then in ~/.config/sdlc-foundry/settings.yml, then sonnet. The value is sonnet, opus or haiku, or a map with a default and one entry per workflow (build: opus); any other name is refused before anything starts. Agents with their own model (architect, reviewer, product-manager) keep it. --plugin-dir PATH (repeatable) loads a plugin from a folder, and cplat platform runs benchmark <workflow> --plugins <worktree>/plugins measures a change with the worktree’s plugins before it is released; the record names the plugins (dir, commit, dirty) and the coordinator model.
Open the repo interactively once before the first headless run: Claude Code ignores a project’s allowlist in a workspace it has not trusted yet (ADOPTING.md → Unattended runs).
On a schedule (macOS launchd)
Section titled “On a schedule (macOS launchd)”<plist version="1.0"><dict> <key>Label</key><string>com.me.shop-api-ship</string> <key>WorkingDirectory</key><string>/Users/me/Dev/shop-api</string> <key>ProgramArguments</key><array> <string>/bin/zsh</string><string>-lc</string> <string>cplat headless "/component:ship 012" --quiet 23:30-07:00 --no-wait</string> </array> <key>StartCalendarInterval</key><dict><key>Hour</key><integer>8</integer><key>Minute</key><integer>0</integer></dict></dict></plist>Outside a Claude session cplat is not on the PATH: use the plugin’s launcher (~/.claude/plugins/marketplaces/sdlc-foundry/plugins/shared/bin/cplat), or uv run <sdlc-foundry checkout>/scripts/cplat/cplat.py.
What has to be in place
Section titled “What has to be in place”| Piece | Why | Check |
|---|---|---|
The repo is a platform repo (.copier-answers.yml) |
shape routing, recipes, allowlist | /platform:doctor; existing repos: cplat adopt --type <shape> |
| CI on pull requests reports checks | land never merges without a check |
a PR shows checks; self-hosted runners: vars.CI_RUNNER (guide) |
.platform-app.yml and the GITOPS_TOKEN secret |
the service’s CI asks the gitops-app for the dev pin (ADR-027) | the pin-dev job on main does not print a notice |
The SERVICE_REPOS_READ_TOKEN secret in each gitops-app (spec 098) |
cplat component ship gitops pr marks its PRs with Service-ref: <owner>/<repo>@<branch>; the gitops CI reads that branch’s CRDs with this token (fine-grained, Contents: read on the service repos) and validates the Components against them. Without it a marked PR fails its check |
cplat platform doctor in the gitops-app (a warning when missing) |
The component is in the system’s system.yaml |
pin-dev pins only composed components; for services the generator cannot express, use an external entry (ADR-029: External services in services.yaml) |
/system:status in the gitops-app |
| Actions may open PRs in the gitops-app | pin-dev opens the pin PR | gh api repos/<org>/<gitops>/actions/permissions/workflow |
| Optional: a kube context | cplat component ship deploy --context <ctx> also waits for Argo Synced/Healthy |
kubectl --context <ctx> -n argocd get applications |
Feedback loops
Section titled “Feedback loops”Eight loops bring what happens in runs and in running systems back as data or as deduplicated issues. They file and propose; you route issues in /issue:triage and approve specs.
| Loop | Signal | Lands in | Setup |
|---|---|---|---|
| Run records (087, 099) | phases, CI rounds, escalations, tokens and cost of every run | the private run store | cplat platform runs init <owner/name> |
| Agent quality (089) | fix rounds, red CI, review findings, re-plans per agent, shape and model | the run store | none |
| Weekly retro (090) | the last complete week against the one before | a Weekly retro issue with evidence-checked spec proposals |
store workflow, runner and two secrets |
| Platform failures (088) | escalations with cause platform, cplat crashes |
one issue per signature in ika100/sdlc-foundry |
cplat issue triage setup-labels |
| Smoke in dev (091) | (smoke) criteria against the deployed service |
the run record; a smoke-failure issue in the service repo |
expose: and hosts.dev in system.yaml |
| Firing alerts (092) | alert rules of the observability setting and ServiceDown |
an issue with an env: label in the owning repo |
ALERTS_TOKEN in the gitops-app, self-hosted runner |
Report an issue (/issue:report) |
anything a person or agent hits, secrets removed | an issue, filed after your OK | gh logged in |
Triage (/issue:triage, spec 035) |
new issues from all of the above | a spec, quick-task, fix-bug or reply | none |
Run records
Section titled “Run records”Every run is recorded as one JSON record in a private GitHub repo (spec 087, ADR-032 — Every run is recorded in a private run store).
- Setup:
cplat platform runs init <owner/name>creates the repo private (or checks an existing one) and printsexport CPLAT_RUNS_REPO=<owner/name>. Put that line where non-interactive shells read it too, because the shells of Claude Code’s Bash tool do not load an interactive profile:~/.zshenvfor zsh (not~/.zshrc); for bash, a file named inBASH_ENV. Without it, the runs of an agent’scplatcalls are only buffered locally.cplat platform doctorrun from inside Claude Code shows whether the run store is configured there. - A record holds kind, repo, spec, phases with durations, CI rounds, reruns, the escalation reason and cause, and the outcome. It never holds secrets (redacted), file contents or logs.
- Offline: without a store, or when it is unreachable, records wait in a local buffer (1000) and one warning line says so. The run itself is never affected.
cplat platform runs flushwrites the buffer;cplat platform doctorshows what is waiting. - Read it:
cplat platform runs report [--json] [--since DAYS]. - Why a
pr waitended: its own record carriespr_wait.result(green,merged,red,budget-refused,timeout,no-checks,closed,merge-failedorerror) with the failing or pending check names. The report and the weekly retro show the count per result and the 5 checks that fail most, and count a red result asred-ciinstead offailed: red CI means the gate worked, not that the wait broke. Records from before this have no result and count asunrecorded.
Token usage and cost
Section titled “Token usage and cost”A record also holds a usage section (spec 099, ADR-035 — Token usage is measured from the local session transcripts): calls and tokens (input, output, cache read, cache write) and an estimated cost, in total and by phase, role and model. It is read from the local Claude Code session transcripts, so it needs Claude Code 2.1.132 or later and the machine that ran the session. Nothing but numbers and names of phases, roles and models is stored.
- Read it:
cplat platform runs usage <run_id>(or--session <id>,--json);cplat platform runs reportadds usage per kind. - As reported: output tokens are shown as the transcripts report them, so the output cost is a lower bound. Costs are estimates from the price table in the repo (its date is printed); a model without a price shows
unknownand marks the cost incomplete. - Delivered vs. gate-failed: a run is delivered when its outcome is
doneand neither gate (cplat platform runs gate acceptance|security, set by/component:build) failed. Adonerun with a failed gate counts as gate-failed, next to the failed, escalated and abandoned runs. - Benchmark:
cplat platform runs benchmark <quick-task|build|ship>runs one fixed task headless againstika100/todo-apiat a pinned revision (buildis two phases:/component:plan 900, then/component:build 900) and appends abenchmarkrecord;cplat platform runs report --benchmarksshows median tokens and cost per workflow and platform version.shipmerges and deploys to the sample’s dev, so the command resets the sample repo to the pinned revision afterwards (force-push of its main branch);--reset-firstrepairs a sample left in a bad state by an earlier failed reset. Benchmark runs never count in the normal report. Each workflow reuses one clone,~/.local/state/sdlc-foundry/runs/benchmark/<workflow>, reset before every run; Claude Code ignores a repo’s permission rules in an untrusted folder, so trust it once (cdthere, runclaude, accept the dialog) — the benchmark refuses to start until you have. A run counts asdoneonly when it produced its result: a new open PR forquick-taskandbuild, a new merged PR forship; otherwise it isfailedandresult_checksays why. The reset leaves PRs, branches and tags that existed before the run alone.
Agent metrics
Section titled “Agent metrics”A build record also says how each agent did (spec 089), per agent, shape and model: cplat platform runs report --agents [--json] [--since DAYS] [--repo R] [--agent NAME] [--shape SHAPE].
- What is counted, against whom:
- coder: hand-backs (
fix roundsby cause lint/type, test, ci, verification), red PR CI rounds (cplat component ship landrecords them, always against the coder), review findings (first pass and after a fix) and refused acceptance-test diffs; - architect: files outside the plan, merge conflicts (and the share that was resolved) and re-plans;
- quality: refused test diffs; tester: untraced criteria and tests that failed for the wrong reason.
- coder: hand-backs (
- Not measured: a metric whose point the run never reached (an escalated Phase 0, a ship that never built, a record from before spec 089) is
n/a, never 0, and is left out of the averages. few runsmarks a row with fewer than 5 runs: do not read a trend into it.- Releases:
--by-versionsplits every row by platform version, versions side by side, so you can compare before and after a release. - Incomplete: a recording call that failed or was malformed never stops the build; it marks the run
incomplete(counted in the row) and the report still shows what was recorded.
Weekly retro
Section titled “Weekly retro”Every Monday the platform looks back at the last complete week and proposes what to build next (spec 090, ADR-039 — The weekly retro: a deterministic report in code, agent proposals checked against it).
- Three steps, one directory:
cplat platform retro --out DIRbuilds the report (report.json,report.md,prompt.md),cplat platform retro propose --dir DIRlets headless Claude writeproposals.json,cplat platform retro post --dir DIRchecks and posts. A failed proposal step still posts the report withProposals: not generated (<reason>). - The report covers ISO week Monday to Sunday UTC, every figure next to the week before: runs per kind and outcome, phase durations, CI rounds and reruns, escalations by cause with the top reasons, reruns by check, platform issues counted again in the week, and the per-agent metrics of spec 089.
- Proposals are checked: each cites evidence refs of the report with their exact values. A proposal without a matching ref, with a bad format, or over the limit (5 specs, 3 prompt changes) is refused and counted. A finding an open spec or issue already covers is shown as
amends spec NNNorsee #N. - Public and private: the issue
Weekly retro <week>inika100/sdlc-foundryhas counts and placeholders only. A leak check stops the post if a repo, spec or store name remains. The full report goes toretros/<week>.mdand.jsonin the run store. An earlier week’s open retro issue is commented on and closed. - Setup:
cplat platform runs init <owner/name>adds.github/workflows/cplat-retro.ymlto the store (it needsgh auth refresh -h github.com -s workflowonce). Give the store repo a self-hosted runner (variableCI_RUNNER) and the secretsCLAUDE_CODE_OAUTH_TOKEN(claude setup-token) andRETRO_GH_TOKEN(fine-grained: Issues read/write on the platform repo, Contents read/write on the store).cplat platform doctorwarns about what is missing. - By hand:
cplat platform retroprints the report and writes nothing;cplat platform retro --week 2026-W42picks another complete week.
Auto-filed platform issues
Section titled “Auto-filed platform issues”When a run stops because the platform itself misbehaves, the failure is filed as an issue in ika100/sdlc-foundry (spec 088, ADR-033 — Failures become auto-filed issues, deduplicated by a signature line).
- What is filed: an escalation with
--cause platform, and a crash (stack trace) of anycplatcommand inside a recorded run. Never filed: causescode,decision,environmentandunknown, and crashes outside a recorded run (those print the/issue:reporthint). - Escalations without a cause are classified: when no allowed cause is given, fixed rules match the reason text (case-insensitive) and record the cause with
cause_source: rule, printingclassified as <cause> by rule <name>. A session waiting for approval of a git action and a dirty working tree areenvironment; a spec/test conflict and a build left open for review aredecision. A stated cause always wins (cause_source: given), no rule ever yieldsplatform, and an unmatched reason staysunknown(cause_source: none). The stop gate giving up and headlessno progressrecordenvironment.cplat platform runs reportand the weekly retro showUnclassified escalations: <n> of <m> (<p>%)and the reasons still unknown, so the rules can be extended; older records are classified when read, never rewritten. - One issue per failure: the body has a
Signature:line. A repeat adds a +1 comment (occurrence number, date, platform version, shape, run id) to the oldest open issue. After the issue was closed, a repeat files a new issue that saysSeen again after #<n> was closed, unless the closed one is labelledwontfix: then nothing is filed and the run record notessuppressed by #<n>. - Redaction: tokens, passwords, e-mail addresses and home paths are removed, and your repo, spec and branch are replaced by
<repo>,<spec>and<branch>. The issue names the run id but never links the private run store. - Cap: at most 10 new issues per 24 h per machine. +1 comments are not capped.
- Offline: if filing fails (
ghmissing or not logged in, no network) or the cap is reached, the escalation still completes with the same exit status, printsnot filed: <reason>, and the filing is retried at the nextcplat platform runs flush. - Off switch:
CPLAT_AUTO_ISSUES=offsends nothing and prints the draft instead.CPLAT_FEEDBACK_REPO=<owner/name>files in another repo. Runcplat issue triage setup-labelsonce so theauto-filedlabel exists.
Smoke in dev
Section titled “Smoke in dev”The deploy step needs the system’s kube context to check Argo, run smoke and read the image digest. Pass --context, or set it once: export CPLAT_KUBE_CONTEXT=<context> or kube_context: <context> in .platform-settings.yml or ~/.config/sdlc-foundry/settings.yml. Without one, smoke is reported as not run.
After dev is healthy, cplat component ship deploy checks the spec in dev (spec 091, ADR-040 — Smoke tests run against a deployed environment through one recipe and one output contract).
- The marker: end a criterion line with
(smoke)when its behaviour is observable through the service’s public URL. The tester writes a smoke-tagged test for it (cplat spec tracereportssmoke: no smoke-tagged testuntil there is one). - The recipe: every deployable template has
devbox run smoke. It runs only the smoke-tagged tests againstSMOKE_BASE_URL(exit 2 without it) and printsPASS|FAIL|SKIP <path>::<name>lines, thensmoke: <p> passed, <f> failed, <s> skipped(or0 smoke tests). Web repos use standard Playwright tests ine2e/tagged@smoke. - Retry and limit: a red run is repeated once after 30 s; a pass on retry is reported as
passed on retry. A run is killed after 600 s and counts as red. - Red: deploy escalates with cause
code(smoke red in dev: <n> failing (<spec>)), prints the failing tests and the last 60 output lines (secrets redacted), and files onesmoke-failureissue in the service repo (one issue per failing set, a repeat adds a +1 comment). Dev is not rolled back./component:ship <spec>resumes with smoke only. - Not run: no
--context, a service not exposed in dev, an unreachable URL, a repo without the recipe or without a gitops-app link. Deploy finishesdone, never reports “passed”, and lists the unchecked(smoke)criteria. - The record: the run record has a
smokesection (outcome, counts, duration, retry, per-criterion result, failing tests, filed issue). - By hand:
cplat instance verify [dev] [--base-url URL] [--spec SPEC] [--json]in a service repo (exit 0 passed, 1 red, 3 not run); it files no issue. - The dev URL comes from the linked gitops-app: the service must have
expose:and ahosts.devtemplate insystem.yaml. An optionalurls.devinsystem.yamloverrides it ({host}is the host fromhosts). For*.localhosthosts the port isLOCAL_HTTP_PORT, default 8088.
Deploy gate
Section titled “Deploy gate”A deploy counts only after every step below. Each step is enforced by code (a test pins it) or by an agent (a rule in a prompt, no test). Spec 105 added the digest, the failure classes and the environment retries.
| step | enforced by | check | test |
|---|---|---|---|
| pin confirmed | code | the pin PR for the merge commit is merged and its pin-dev run is not red | test_deploy_fails_at_once_on_a_red_pin_dev_run |
| Argo healthy | code | Argo CD runs the pinned build and reports Synced/Healthy | test_argo_old_image_is_waiting_not_healthy |
| smoke | code | the spec’s smoke tests pass in dev and the record names the image digest the cluster runs | test_deploy_runs_every_smoke_test_against_the_published_dev_url_ac_091_6, test_the_smoke_record_holds_the_digest_dev_runs_ac_105_1 |
| retry | code | an application failure is retried once, an environment failure up to three times in total, 30 s apart | test_an_application_failure_keeps_the_single_retry_ac_105_5, test_an_environment_failure_is_tried_three_times_30_seconds_apart_ac_105_5 |
| escalation | code | a red smoke escalates: cause code with an issue, cause environment without one |
test_an_application_failure_still_escalates_with_cause_code_ac_105_5, test_a_persistent_environment_failure_escalates_without_an_issue_ac_105_5 |
| digest match | code | staging is pinned only while the registry digest of the tag equals the smoked digest | test_a_changed_digest_pins_nothing_and_says_so_ac_105_2 |
| promotion | code | a smoke that was not run is reported NOT VERIFIED and promotes nothing |
test_a_not_run_smoke_reports_not_verified_and_promotes_nothing_ac_105_6, test_a_dev_smoke_that_was_not_run_promotes_nothing_ac_093_3 |
A replay of cplat component ship deploy skips the dev smoke only when the record holds a pass for the digest dev runs now (test_a_replay_with_the_same_digest_does_not_smoke_dev_again_ac_105_7). cplat instance promote … --to prod prints the checked digest and flags digest differs from what staging runs.
Prod runs what staging tested
Section titled “Prod runs what staging tested”cplat instance promote <component> --to prod compares the registry digest of the sha-<40> image staging runs with the release X.Y.Z image (spec 106) before it writes anything, for all services first. Equal digests: the promotion goes on and the evidence and the PR body say image digest: <digest> (same in staging and prod). A mismatch refuses with “prod would not run what staging tested”, both tags and both digests, and the fix: re-publish the release from the tested image (the release job re-tags the sha-<40> image instead of rebuilding it), or promote the release whose image is the staging one. A missing tag, a failed lookup (gh auth refresh -s read:packages) and a staging tag that is not a sha-<40> build refuse too.
--dry-run prints staging: <tag> <digest>, prod: <tag> <digest> and match or mismatch per service (data.digests with --json); a mismatch exits 1. --accept-digest-mismatch "<reason>" (a sentence of 3 or more words ending in ., ! or ?) accepts a mismatch only, and is recorded in the output and the PR body. --no-verify skips the check and prints image identity not verified (--no-verify).
Unit sessions and the run allowlist
Section titled “Unit sessions and the run allowlist”cplat headless runs a ship or build as a series of short sessions, one unit each, instead of one long session (ADR-075: An unattended ship or build runs as one session per unit, each saved at a checkpoint). The units are plan, red-tests, task:<id> (one per plan task), verification and land. A session gets --unit <unit>, follows only that unit (plugins/component/references/unit-mode.md) and stops at its checkpoint, a commit recorded in the run’s units file. The next session starts the next unit, so one hard task no longer uses up the context, the stops and the budget of everything after it.
In a unit session the Stop hook also runs when CPLAT_UNIT is set, not only when cplat-ship.json exists. It holds the session until the unit has its checkpoint, once per HEAD: a stop without a new commit since the last hold is let through and the supervisor counts it as a stall. Two sessions in a row on the same unit without a new commit escalate the run as no progress: unit <unit> after 2 sessions without a commit. A session that moves HEAD resets the count.
Before the first session, cplat headless and cplat system ship repo start run a preflight and print preflight failed: <category>: <detail> on a failure. The run is then escalated with cause environment and no session, branch, commit or PR is started. In a system ship only that repo is escalated.
| Category | What to do |
|---|---|
no-repo-allowlist |
.claude/settings.json is missing: run /platform:update or cplat adopt |
blocked-by-settings |
a deny or ask rule matches a git or gh command the run needs: remove or narrow the rule, or run the command attended |
git-identity |
set git config --global user.name and user.email (needed for commit -s) |
gh-auth |
gh auth login --hostname <host> |
no-push-access |
ask for write access to the repository |
The sessions get a run allowlist (ADR-076: An unattended run gets its git and gh permissions per session from a checked run allowlist, never from a settings file): the feature branch (switch, create), commit -s, push of that branch without force and gh pr create. It applies to this run’s sessions only and no settings file is changed; cplat headless prints it at the start. Force pushes, pushes to the default branch and merges outside cplat component ship land stay refused.
Budgets and repeated failures
Section titled “Budgets and repeated failures”Every unattended run tree shares one budget (spec 102, ADR-043 — One budget per run tree, kept in a local ledger and checked at safe points): wall-clock minutes, agent invocations (sessions and subagents), CI reruns and estimated cost. The outermost run owns it (cplat headless, /system:ship, or a ship or build started by hand); every ship, build and CI wait below it counts against the same budget.
| Limit | Per ship | Counted by |
|---|---|---|
minutes |
180 | wall clock from the root’s start, carried across resumes |
invocations |
60 | each headless session, and each subagent (the shared plugin’s PreToolUse hook runs cplat budget gate --invocation) |
reruns |
6 | each CI rerun of cplat component ship land and cplat pr wait |
cost |
$15 | estimated from the local transcripts (spec 099); unknown when a price is missing, then only the other three apply |
A root that covers several ships multiplies the defaults: cplat headless "/component:ship 102 103" gets twice the limits, a system ship one set per repo in its plan. Change a limit with --budget minutes=240 (repeatable, off turns one off) on cplat headless, cplat component ship start, cplat system ship start or cplat platform runs begin, or set budget: in .platform-settings.yml or ~/.config/sdlc-foundry/settings.yml. cplat budget show prints the limits and what is used.
Limits are checked at safe points (before a session, a subagent or a CI rerun, at the end of a phase, before a system ship starts a repo). A run that hits one starts nothing new, escalates with cause decision and reason budget exhausted: <limit>, and stays resumable: run it again with a higher limit, and the usage continues from where it stopped. Running work is never killed, and no gate is skipped or counted as passed (not run).
A stage that fails twice in a row with the same error (timestamps, paths, ids, line numbers and durations removed) and no fewer failing checks stops with no progress: <stage>. Every escalation leaves a recovery snapshot in <git dir>/cplat-recovery.json (stage, a redacted excerpt, changed files, criteria, earlier attempts; never uploaded), and cplat budget resume shows which stages are still verified for the current HEAD, so a resumed run starts at the failing one. Run state (cplat-ship.json, the recovery snapshot) lives in the worktree’s own git dir, so a run in one worktree never blocks or attaches to another checkout’s session (ADR-051 — Run state is per worktree).
Scaled budgets and the verification grace (spec 131)
Section titled “Scaled budgets and the verification grace (spec 131)”Once the plan exists, the default limits follow its size (ADR-077: Run budgets scale with the plan, and verification gets one grace). For T tasks and C active criteria: cost max(15, 10 + 1.5·T + 0.5·C) USD, minutes max(180, 60 + 15·T + 5·C), invocations max(60, 20 + 5·T + C), reruns 6. Spec 128 (8 tasks, 14 criteria) gets $29, 250 minutes and 74 invocations. A budget: setting replaces the floor and an option replaces the limit. A run covering several specs gets the sum of each spec’s defaults (a part per spec), raised at the plan checkpoint and never lowered. This replaces the multiplication in the table above for runs with a plan.
When a limit is used up while every task is done and only verification is left, verification starts within a grace of 20 % of each limit (at least $3, 30 minutes and 10 invocations), once per run. If that is used up too, the run escalates with budget exhausted: <limit> (grace used). The grace never skips a check. An incomplete transcript read counts as an unknown cost, not a partial sum, and subagent costs are read from cached transcripts.
Change-aware validation in fix rounds
Section titled “Change-aware validation in fix rounds”Inside a build or ship, fix rounds run only what the change can affect (spec 103, ADR-044 — Change-aware validation in the inner loop only, decided by rules in code): cplat validate run plans every check of devbox run ci as run, reused or skipped and names the rule that decided it. A change to dependencies, the Dockerfile, CI, secrets or auth, a spec or an acceptance test (and per shape: the API contract or migrations of a service, a gitops-app’s manifests, a web repo’s routes or build config) runs the full suite and the security scan. A change that touches only documentation runs lint and the secret scan. A mapped change runs the tests of its criteria and its component’s unit tests. Anything the rules cannot place runs everything. A passed check is reused only when its inputs are byte-for-byte unchanged, and only on this machine.
The final gate is never selective: before a PR is marked ready or landed, devbox run ci runs in full, and the PR’s CI is unchanged. A check that the loop skipped or reused and the final gate then finds red is counted as a miss for its rule; cplat platform runs report shows the counts and misses per rule. The same holds for the red checks of the PR’s CI (cplat component ship land records them). --full (also as an argument of /component:build and /component:ship), validation: full in .platform-settings.yml or CPLAT_VALIDATION=full turn selection off. Every spec file under docs/specs/ counts as a spec change, never as documentation; checks that never started because an earlier one was red are reported as not started. Java, Go and gitops-app repos have no test selector yet and always run everything.
Failure summaries
Section titled “Failure summaries”Spec 108, ADR-071: A failing check reaches an agent as a summary made by fixed rules, the full log stays on disk. A failing check reaches an agent as a compact summary made by fixed rules, not as a raw log tail: cplat quiet, cplat validate run, the pre-push devbox run ci and the red CI checks of cplat component ship land and cplat pr wait print, per failed check, its format, the failing tests or files with line and message, repeated entries counted once, with secrets redacted. A format the parsers do not know is printed as format: unknown with the last 40 lines. The full log stays on disk next to a <log>.summary.json sidecar, and the agents are told to read the summary first and to open the full log only when the summary is not enough. Spec 102’s no-progress signature and recovery snapshot use the summary.
To get the earlier output back, run cplat quiet --raw <script> or set failure_summary: off in .platform-settings.yml. cplat platform runs benchmark fix-rounds compares a build with summaries on and off.
Live usage in Grafana (optional)
Section titled “Live usage in Grafana (optional)”cplat headless and cplat platform runs benchmark can turn on Claude Code’s own OpenTelemetry export for the sessions they start (spec 101), so tokens and estimated cost show up live, per run, model and day. The run store stays the system of record: phases, roles and the figures in cplat platform runs report always come from the transcripts.
- Explicit endpoint:
export CPLAT_OTLP_ENDPOINT=http://localhost:4318, ortelemetry: {endpoint: …}in.platform-settings.ymlor~/.config/sdlc-foundry/settings.yml. It must be local (localhost,127.x,::1,*.localhost, a cluster*.svcname). A remote backend needstelemetry: {allow_remote: true}(orCPLAT_OTLP_ALLOW_REMOTE=1); without it the endpoint is refused with one line. - The observability setting: when the system’s gitops-app has
observability: {ui: lgtm}and a kube context is set (telemetry: {context: k3d-<app>-local}orCPLAT_KUBE_CONTEXT), cplat port-forwards to the local Grafana stack for the session. The context is never guessed. Grafana gets a Claude Code usage dashboard: tokens and cost per run, per model, per day, and the coordinator’s share. - Nothing configured means telemetry stays off, with no output and no delay. An unreachable backend shows one warning and never changes the run’s result.
- cplat forces every content switch off (
OTEL_LOG_USER_PROMPTS,OTEL_LOG_TOOL_DETAILS,OTEL_LOG_TOOL_CONTENT,OTEL_LOG_ASSISTANT_RESPONSES,OTEL_LOG_RAW_API_BODIES), even if your shell sets them. Only metrics are exported (no events, logs or traces): counts, model, cost, the attributescplat.run_id,cplat.kind,cplat.repoandcplat.platform_version, and the attributes Claude Code always attaches without a switch (user.id,user.email,organization.id,terminal.type). Every attribute that has a switch (session id, account UUID, version, entrypoint, repository) is turned off, and your ownOTEL_*variables (endpoints, headers, exporters) do not reach the session. Because the always-attached identity leaves the machine, the endpoint must be local unlessallow_remoteis set. - After a run with telemetry, the record gets
usage.otel: the OTel totals queried by run id and their difference from the transcript totals per token type. Use it to check the transcript counting.
Watching runs live (optional)
Section titled “Watching runs live (optional)”To see running sessions, their status and a way to step in while a run works, you can use dot-agent-deck next to the platform. It is optional and only shows activity: outcomes, costs and gates still come from the run store, CI and the ship gates.
Safety on the free plan
Section titled “Safety on the free plan”main cannot be protected on GitHub Free for private repos, so a human with write access can still push to it. Unattended runs cannot: the allowlist refuses git push origin main. Every automated merge (feature PRs through cplat component ship land, pin PRs through pin-dev) happens only after its checks passed. A merge happens without a human review of the PR. The approved spec, the acceptance tests written before the code, and the reviewer’s verification.md (required for done, spec 068) are the review. Use /component:ship <NNN> --no-merge to read the PR yourself first.