feature-dev run bilansAutonomous end-to-end feature development. v2 (ADR-058 minimal-framing)
since 2026-07-07: one campaign agent ships the feature slice by slice
(commits in stride) against a deterministic build/test gate + bounded
continuation loop, opt-in MR tail. Pre-v2 bilans below describe the
retired plan → act → /simplify → alternating review-fix → commit
pipeline. See bots/feature-dev/.
iterion validate --strict shipped in one pass (run 019f3bb4)11aa9b65b) · iterion worktree branch @a497f1ae2 · sandbox-full:edge.iterion run from the conversion worktree (static binary), --store-dir <workspace>/.iterion (studio-visible), --merge-into none, --max-cost-usd 30 --max-duration 2h, mono claude (opus-4-8 high, Claude forfait — no cost metric reported). feature_prompt = add --strict to iterion validate (warnings fail the exit code for CI gating) + --json coverage + table-driven tests + docs.finished, gate.converged=true (real verify_run: passed, not skipped), 2 semantic commits in stride on storage branch iterion/run/thunder-sizzle-dawnglyph-ec4d @0cd39d446: feat(cli): add --strict to iterion validate to fail on warnings then docs(cli): document iterion validate --strict flag. Termination contract honest: feature_complete=true, commits_this_pass=2, summary matched the diff.TestValidate_Strict + TestValidate_StrictHumanOutput PASS (existing validate tests untouched-green); behaviour matrix exercised live — 0-warning file + --strict → exit 0; warning-bearing file without --strict → exit 0 (unchanged default); with --strict → exit 1 with result: INVALID (strict: N warning(s)).validate --json emits TWO JSON documents on failure (result object + error object) — pre-existing, verified against a pre-feature binary; candidate board finding, not a regression. (2) The campaign did not exercise the ask-user or findings-handoff paths (nothing to escalate on a clean feature) — those paths remain structurally tested only. (3) Run-worktree was based on the MAIN checkout HEAD (engine derives the repo from the store-dir side), not the launching worktree’s branch — harmless here, worth knowing when dogfooding from a worktree.reasoning_effort: high (the shape, not the tier, carried it); the verify_build node re-derived verify.sh correctly from devbox — no skill gap. GATE PASSED → rollout continues (docs-refresh, adr-cartograph, rgaa-audit, dep-update-guard, secured-renovacy P2, sec-audit-source remediation)./featurly on project 194 issue !2 → claude_code-forfait implementer wrote main.go → claw GLM reviewer approved → commit → push → MR !13 opened (iterion/fix-go-doc-comment → main, author project_194_bot) → back-link comment on issue !2 (“Created MR: …/merge_requests/13”). The 8-fix infra campaign below got the sandbox to start; these 8 more drove the run from “empty workspace” all the way to the MR + back-link.1712e4e56 → 31aa3636a (8 commits) · sandbox-full:edge · chart unchanged./featurly webhook → preprod runner → board-dispatched run → k8s sandbox. Creds: Claude forfait (CLAUDE_CODE_OAUTH_TOKEN) + GLM (ZAI_API_KEY) only — no OpenAI/codex (the user’s constraint, which sidesteps the ~/.codex forfait gap flagged in the prior bilan). claude_code nodes (plan/act/simplify/reviewer_claude/prepare_commit/finalize_mr) use the forfait; the claw reviewer_gpt uses anthropic/glm-5.2 because claw can’t use the Claude Code OAuth forfait (Consumer Terms). Provisioned to iterion-llm via kubectl patch (SealedSecret → temporary; scrubbed at end via runner rollout-restart)./featurly, continuing #1–8 below)/workspace is an emptyDir with no host bind-mount, so the runner’s clone never reached the bot → it ran in an EMPTY workspace. kubernetes/driver.go populateWorkspace: after the pod is Running, tar-stream resolveCloneRoot(WorkspacePath) (git rev-parse --git-common-dir → the clone root, kept with .git so the bot can commit+push from inside) into the pod via tar -cf - … | kubectl exec -i -- tar -xf -. 1712e4e56.CloneURL = git_http_url (present in the payload) + DefaultBranch as the ref. 748050fd9./command with the dispatcher active does ensureBoardCard(StateReady) + returns; the CloudBoardCoordinator (processBoardCard) then launches with Vars: iss.BotArgs only — no RepoURL → the runner never cloned. Fix: stamp clone-url/ref into the card’s BotArgs under reserved keys (__iterion_repo_url/_ref), lifted into LaunchSpec.RepoURL/RepoRef by liftBoardRepo. adf49681d../ root member made the in-pod tar chmod/utime the root-owned, fsGroup-setgid /workspace emptyDir (a non-root user can’t) → exit 2, failing the whole run though every file extracted fine. --no-overwrite-dir (48cf5f1c1) was INSUFFICIENT; v2 = os.ReadDir + tar entries BY NAME, so the archive has no ./ member at all. 1c3925437.providerCredentialEnvVars/forwardableProviderEnv never forwarded ZAI_API_KEY/ANTHROPIC_AUTH_TOKEN/ANTHROPIC_BASE_URL to the sandbox __claw-runner → the GLM reviewer had no z.ai creds in-container. Add the 3 vars; registry.go synthesises the z.ai bearer + ZAIDefaultBaseURL from ZAI_API_KEY. (Config: ITERION_VIBE_MODEL_GPT=anthropic/glm-5.2 — the anthropic/ prefix is required; claw rejects a bare glm-5.2 with invalid spec.) aa914dd42./workspace, but the bot’s deterministic tool nodes (commit_changes/finalize_mr) + PROJECT_DIR use the worktree ABSOLUTE path → git -C /home/iterion/worktrees/<id> = exit 128: No such file or directory, killing the run right after the (passing) review loop. Override p.workspace = info.WorkspacePath (mirror docker’s bind-at-host-abs-path so mount/workingDir/populate/cwd all align). a24511ff6.kubectl apply PATCHes the (largely immutable) pod, and the SA intentionally lacks pods/patch → Forbidden → DLQ. Force-delete (--grace-period=0 --force) any stale pod before apply so it always CREATEs fresh. a24511ff6.git commit in the sandbox: Author identity unknown / unable to auto-detect email — k8s has no ~/.gitconfig (the bind-mount is dropped, and the cloud runner pod has none of its own). Seed the clone’s LOCAL git config (user.name/email) in the runner right after git clone (travels into the sandbox with .git; default iterion/iterion@users.noreply.github.com, overridable via ITERION_GIT_AUTHOR_*). 31aa3636a.Package main doc-comment form during finalize_mr, committed on iterion/fix-go-doc-comment, pushed (auth via the token-injected remote URL), and glab mr created MR !13 + back-linked issue !2. forge_token resolves on the board path via the org bot-secret binding (Tier1/Tier2) — no SecretOverrides threading needed, and it mounts at /run/iterion/secrets/forge_token.README.md/go.mod/main.go — a prompt referencing a non-existent file (fetch.go) makes the bot create-or-confuse; prefer an existing-file modify (main.go).act’s git add -A staged the compiled revi-playground binary + .claude/plan.md; reviewers saw them in the working diff (didn’t block, but a .gitignore/curated git add would be cleaner). prepare_commit’s curated [main.go] kept the commit clean.glab auth set-token --host failed (flag renamed); the adaptive agent recovered with glab auth login --hostname. The forge-mr-create skill could pin the modern syntax.origin/main: 1712e4e56 748050fd9 adf49681d 1c3925437 aa914dd42 a24511ff6 31aa3636a./featurly on project 194 issue !2 advanced the run one stage, and it now reaches post_create executing inside the sandbox pod (pod created, baked image pulled, pod Ready). The bot still can’t complete because preprod’s iterion-llm secret has no LLM creds (placeholder) — a green run (bot implements + opens the MR) needs a creds decision, below. This is the FIRST sandboxed bot ever run on the kubernetes sandbox driver, so the path was entirely unexercised; 8 durable engine/chart/image fixes resulted.89ed642dc → c4364319f · chart v0.17.2./featurly comment) → preprod runner → k8s sandbox (sandbox-full:edge, user 1000:1000). claude_code implementer + claw gpt-5.5 reviewers + forge_token./featurly)ITERION_POD_IP (downward API status.podIP) — chart 0.16.1; the k8s network proxy needs a routable advertise address. Gated on runner.sandbox.enabled.ITERION_SANDBOX_HOST_STATE=none in the configmap (chart) + an engine bug: the cloud runner (pkg/runner/loop.go) read cfg.Sandbox.HostState from the env then dropped it — never wired it to the engine like pkg/cli/run.go does for iterion run. 89ed642dc threads it through runner.Config → engine opts.iterion __claw-runner in-container; docker bind-mounts the host binary but k8s has no host fs. Bake static iterion into the sandbox images (FROM ${ITERION_IMAGE} AS iterion-bin + COPY; image.yml needs: build) + gate the host bind-mount on a new Capabilities.SupportsHostBindMounts (docker true / k8s+noop false). 1ca693d42.forge_token as:file + proxy TLS CA); the runner SA lacked secrets. chart v0.17.2 (fb03b4e5a): get/create/update/patch/delete, no list/watch (least privilege).~/.claude OAuth mount (type=bind + the docker-only consistency= key) hard-failed k8s manifest build. The runtime drops type=bind on a no-host-fs driver (reuse SupportsHostBindMounts; dropHostBindMounts/mountIsHostBind, unit-tested). 25b04eb71.IfNotPresent for @sha256) — a stale node-cached :edge mustn’t shadow a fresh CI bake. 25b04eb71./opt/iterion/bots/<bot> → /run/iterion/bundle). Moved it after the block → catches bot mounts + bundle/attachments/run-files. Skills still reach the sandbox via the workspace mirror (<workspace>/.claude/skills). 6c794e05d.sudo npm install -g, but the k8s pod is runAsNonRoot/allowPrivilegeEscalation=false, so sudo can’t escalate → post_create exits 1. Bake the pinned llm-clis (claude-code 2.1.175) into the sandbox slim image + symlink claude onto PATH; post_create’s claude --version then passes, skipping the sudo branch. c4364319f (final image build in flight at time of writing).The preprod runner has no LLM creds (ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / OPENAI_API_KEY / CLAUDE_CODE_OAUTH_TOKEN all empty; iterion-llm is a placeholder — Revi’s live e2e ran on ovh-prod, which has real keys). There is also a deeper creds-to-sandbox-user gap: claude_code gets ANTHROPIC_API_KEY forwarded by its delegate into the sandbox exec (works if the runner has a key), but claw (gpt) reads ~/.codex (the ChatGPT forfait), which host_state mounts at the host path while the sandbox runs as devbox (/home/devbox) — there is no .codex bridge like the .claude one. A green “bot-runs-in-sandbox” e2e therefore needs (a) real LLM creds in preprod iterion-llm, and (b) a decision on forwarding provider creds into the sandbox env for both providers (vs per-provider mounts). Both are the operator’s call.
SupportsHostBindMounts (true for docker) or is additive (image bakes), so docker keeps host bind-mounts (OAuth via ~/.claude) unchanged.origin/main: 89ed642dc 1ca693d42 fb03b4e5a 25b04eb71 6c794e05d c4364319f (+ chart v0.17.2).feature_dev run launched); the bot run then failed on a pre-existing preprod infra gap, NOT the feature.finalize_mr tail) · iterion preprod :edge @0f1d8d670 (this work) · webhook-launched on the cloud runner (sandbox).0f1d8d670); provisioned on preprod a gitlab webhook (0b00720c, wildcard) + a distinct project-194 bot PAT + forge_token bindings (feature-dev/whole-improve-loop/branch-improve-loop) on devthejo/revi-playground (194) + GitLab hook #13; posted /featurly add a doc comment to fetch.go on issue !2./featurly → feature-dev, launched run 019ef703. The run FAILED at sandbox start: network proxy: kubernetes: ITERION_POD_IP env var is empty; the runner pod manifest must inject it via downward API (status.podIP). No MR (never reached finalize_mr).finalize_mr mechanics test against 194 (real MR opened + back-linked + cleaned up)./featurly route + bot launch all fire.wildcard_bots: a webhook created via POST /api/teams/{id}/webhooks with explicit bot_ids has an EMPTY CommandMap, so /featurly filtered as “no command route” until I PATCHed wildcard_bots=true (then the live registry discovery resolves it). Documented, but a footgun — consider having the manual create endpoint compute the CommandMap from bot_ids (parity with the forge orchestrator’s buildCommandMap).ITERION_POD_IP via the downward API (status.podIP) → the k8s sandbox network proxy cannot start → EVERY sandboxed bot fails immediately on preprod (ovh-dev / chart iterion 0.14.0). Fix: add env: [{name: ITERION_POD_IP, valueFrom: {fieldRef: {fieldPath: status.podIP}}}] to the runner pod template. Blocks the deployed bot-run→MR e2e until fixed (a kubectl patch was correctly denied as a shared-infra change — needs operator consent).ITERION_POD_IP + the wildcard_bots webhook, the deployed e2e is one /featurly comment from completing — provisioning was left in place (webhook 0b00720c, hook #13, bindings, bot PAT; issue !2). Loop-guard is satisfied (distinct bot PAT identity ≠ commenter).done; deliverable builds + all new tests green (verified independently in the worktree, anti-façade).claude_code/opus · worktree: auto · --merge-into none.feature_prompt asking Featurly to implement the entire ADR-044 “Verified Action” synthesis (goal+recipe+postcondition+policy quad; idempotent-skip → recipe → self-repair → agent-recovery escalation; postcondition-as-truth; gates stay deterministic) one-shot. Run was cap-interrupted at 08:58 (Anthropic session limit) at reviewer_claude, resumed cleanly from checkpoint on a switched account → finished.iterion/run/019ef38d-745b… @ 79f9111ed, 1592 insertions / 22 files: DSL (parser/AST/jsonenc/IR + validate_verified_action.go/unparse/EBNF), runtime (executor_verified_action.go 342L ladder + executor_tool.go wiring + engine.go + new event), unit + e2e tests, docs (ADR-044, DSL quickref, CLAUDE.md), and a demo application on secured-renovacy’s commit node. go build ./... + go test ./pkg/backend/model ./pkg/dsl/{ir,parser,unparse} green.executor_verified_action.go + overlap resolution).secured-renovacy the same day (secured-renovacy.md).fe132645 · dispatched via the dispatcher (own iterion dispatch daemon on the operator’s repaired config, --no-server, sandbox iterion-sandbox-full:edge, worktree: auto). Each step: isolated ticket with an anti-façade done-criterion, reviewed + race-verified + repatriated before the next.77a2cb80, FF to main. launchDiscovery→cmdCandidates, single-flight discoveryInFlight, postCmd shutdown-safe choke point.a72d40f7, FF. finishPlan value-copy; transition-FIRST/Release-LAST to close the re-dispatch window; optimistic-retry-as-guard for the give-up HTTP window (cmdDropRetry).fix_gpt (sandboxed gpt-5.5 via claw) repeatedly hit “context canceled” at the dispatcher’s 10-min stall timeout, looping retry→re-dispatch→stall. I reviewed the uncommitted worktree directly (max rigor), confirmed correctness, and manually repatriated (9b3bd3bd → cherry-pick 70b3d4ed, auto-merged clean over the operator’s parallel commands.go bug-sweep).RefreshStates remains, deliberately deferred).setupPending=true), and guarded both reapers (refreshRunningStates + reconcileStalled) against the setup window — correctly identifying that the off-actor UpdateState makes the tracker read RunningState before the entry records TransitionedFromState, which would otherwise self-cancel the run.fix_gpt/reviewer-fix on sandboxed gpt-5.5 (claw) hangs >10 min → trips the dispatcher’s 10-min stall timeout → cancel → retry loop, blocking review-loop convergence on a perfectly good change. Runtime issue (sandboxed claw streaming / context on a large review-loop context), not the bot. Relates to the known sandboxed-claw streaming + gpt-5.5-forfait-context work. Worth a ticket. Secondary: the run-status monitor false-terminals on a transient cancel→auto-resume — key on issue-state, not run-status.-race + manual review) and repatriate rather than re-dispatching into the same stall. For the riskiest step, the reduced variant (Claim-atomic, offload post-claim) avoids the reserved-before-claim state entirely and was the right call.8477a067 (≈ HEAD)iterion dispatch CLI, --no-server, fresh static binary so delegated subprocesses + sandbox mount use current code; workspace store .iterion). claude_code (Opus 4.8) plan/act//simplify/reviewer_claude; claw GPT reviewer_gpt; sandbox: iterion-sandbox-full:edge, worktree: auto. Ticket = ADR-028 body (decomposed I/O-offload roadmap) with an anti-façade Step-1 scope.finished, ~16 min, ~56k tokens. 1 commit 89dd2f57 on branch iterion/run/aurora-hunt-prismpunk-01af; repatriated to main by FF (clean — main was ancestor); issue efb9022d → done.docs/adr/028-dispatcher-actor-io-offload.md (records the tracker-as-claim-authority insight + per-issue state-machine direction + incremental sequence + rejected alternatives) AND implemented Step 1: Snapshot() is now lock-free (atomic.Pointer[Snapshot], published by the actor in fireSnapshot, seeded at construction), so dashboard reads never wait on the actor’s in-flight tracker I/O. Removed the now-dead cmdSnapshot.buildSnapshot() with the correct reasoning that it “would read c.state off the actor goroutine — the very race this read path removes” — i.e. it understood the invariant, didn’t just add a field. (2) Scoped strictly to the read path; no out-of-scope dispatch/claim/finishRun changes. (3) The test (TestSnapshotLockFreeWhileActorBlocked) gates fakeTracker.ListCandidates on a channel, waits until the actor is provably parked inside it, then asserts Snapshot() returns < 500ms with real state — genuinely proves the property, not its shape. Independently re-verified: build/vet OK, race-clean 3×..iterion/dispatcher/dispatcher.json routed every bot to stale examples/<bot> paths (bots had moved to bots/<bot>), so iterion dispatch refused to start (stat examples/branch_improve_loop: no such file) and the studio’s embedded dispatcher had silently degraded to an inert stub (slots.global_max=0, tracker="") — enabled but never dispatching. Config validation failed loudly for the CLI (good); the studio swallowed it into a stub (worth surfacing in the dashboard). Worked around with a minimal repaired config (feature-dev → bots/feature-dev); operator’s original backed up to /tmp/operator-dispatcher.json.bak.examples/*→bots/*), and consider a startup banner when the embedded dispatcher degrades to a stub.Update — fix applied + validated (run 019ec180). Taught
act/fixtogit -C <workspace_dir> add -Aafter editing (commit44d34c9d), so new files are tracked and visible to the reviewers’git diff HEAD. Re-running the SAME feature_prompt: Featurly converged and committed (finished, $2.85 / 247 steps vs the looping$4.95 / 507 / cancelled), shipping commit439d1116oniterion/run/opal-flash-mothbeam-80d7—pkg/cli/sandbox.go(+106, the doctor static/dynamic ELF check + WARNING), a tracked test, ANDdocs/adr/019-sandbox-doctor-static-binary-check.md. The new test being in the commit is the direct proof the untracked-files bug is fixed. Feature pending integration to main (after the parallel Depsy run, to avoid a watchexec restart).
POST /api/runs, feature_prompt = add a static-binary check to
iterion sandbox doctor (warn when the host iterion is dynamically linked — the
exact trap that broke Seki). --merge-into none, default workspace_dir
(worktree-isolated ✅, .iterion/worktrees/019ec149..., safe under watchexec).
Backends: claude_code opus (plan/act/simplify/fix_claude/prepare_commit) + claw
gpt-5.5 (reviewer_gpt/fix_gpt). 101.7k tokens, $4.95, 507 steps, review_loop
10/15 — cancelled (non-convergent).act produced a correct, well-documented feature: pkg/cli/sandbox_binary.go
with iterionBinaryIsStatic(path) detecting static-vs-dynamic via the ELF
PT_INTERP program header, a focused _test.go, and the sandbox doctor
integration in sandbox.go. The doc comment even cites addClawBinaryMount and
the precise exec: … no such file or directory failure mode. Salvageable from the
preserved (cancelled-run) worktree.git diff HEAD, NOT HEAD^…HEAD”
(so a reviewer doesn’t conclude “feature not implemented” off the base commit).
But git diff HEAD omits untracked files — and act creates new files
without git adding them. So the helper + test (pkg/cli/sandbox_binary.go,
…_test.go) were ?? untracked, invisible to the reviewers’ git diff HEAD
--name-only. The GPT reviewer correctly rejected every pass:
“the helper and focused unit test are still untracked … the committable tracked
diff references iterionBinaryIsStatic without including its implementation or
the required test.” The fix_* agents can’t resolve it (the files already
exist; the real gap is staging), so it loops to review_loop(15) and dies. This
almost certainly hits any review loop that anchors on git diff HEAD for a
change that adds files (feature-dev, possibly Billy/branch-improve-loop and Doki).git -C <wt> add -N . (intent-to-add) before the anchor diff, so git diff
HEAD shows new files as additions (full content) without fully staging them; the
existing prepare_commit’s git add -- <files> still does the real staging at
commit. Equivalent belt-and-suspenders: have act/fix_* git add new files
when they create them. Apply the same to every loop bot that diffs git diff HEAD.git diff
HEAD, not HEAD^…HEAD”) is now extended with the untracked-files caveat
(CLAUDE.md, asymptote section).git status: if act
left ?? untracked files, the review loop will never converge until they’re
staged — that’s the bug above, not a bad implementation.