iterion

Featurly — feature-dev run bilans

Autonomous 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/.

2026-07-07 — v2 minimal-framing PILOT: iterion validate --strict shipped in one pass (run 019f3bb4)

2026-06-25 — issue-comment → MR cloud-k8s e2e GREEN (Claude forfait + GLM; 8 more fixes) (run 019efbc6)

The 8 fixes (each surfaced by the next /featurly, continuing #1–8 below)

  1. workspace-delivery to the k8s sandbox (the substantial Phase-5-V2 piece the prior bilan deferred) — /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.
  2. RepoURL for issue-comment runs — the GitLab issue-note webhook carried no clone URL/ref. Now sets CloneURL = git_http_url (present in the payload) + DefaultBranch as the ref. 748050fd9.
  3. board-dispatcher dropped RepoURL (the real RepoURL gap) — a board-mode /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.
  4. tar workspace-copy perms — the archive’s ./ 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.
  5. GLM claw reviewer creds in-sandboxproviderCredentialEnvVars/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.
  6. k8s workspace mount path — the driver mounted the workspace at /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.
  7. pods-patch on resume — a runner-level resume re-applies the pod; 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.
  8. git author identitygit 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.

Result + lessons

2026-06-24 — k8s cloud-sandbox hardening campaign (8 fixes; feature-dev = first sandboxed bot on k8s)

The 8 fixes (each surfaced by the next /featurly)

  1. 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.
  2. host_stateITERION_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.
  3. claw binary in-sandbox — sandboxed claw shells to 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.
  4. per-run Secrets RBAC — the driver creates per-run Secrets (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).
  5. drop docker-only host bind mounts — feature-dev’s ~/.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.
  6. imagePullPolicy=Always for mutable sandbox tags (IfNotPresent for @sha256) — a stale node-cached :edge mustn’t shadow a fresh CI bake. 25b04eb71.
  7. drop ALL host binds — fix 5’s filter ran before the mount block, missing the runtime’s own optional binds (the bundle mount /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.
  8. claude_code CLI bake — post_create installs the CLI via 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).

Remaining blocker — CREDS (operator decision, not code)

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.

Lessons for next run

2026-06-24 — issue-comment → improvement-MR e2e on preprod (run 019ef703)

2026-06-23 — Verified Action recovery ladder, ADR-044 (run 019ef38d)

2026-06-17 — ADR-028 Steps 2-4 dispatcher I/O offload (runs 019ed4cd, 019ed4eb, 019ed51d)

2026-06-15 — ADR-028 + Step 1 lock-free dispatcher Snapshot (run 019ecafa)

2026-06-13 — sandbox-doctor static-binary check (runs 019ec149, 019ec180)

Update — fix applied + validated (run 019ec180). Taught act/fix to git -C <workspace_dir> add -A after editing (commit 44d34c9d), 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 commit 439d1116 on iterion/run/opal-flash-mothbeam-80d7pkg/cli/sandbox.go (+106, the doctor static/dynamic ELF check + WARNING), a tracked test, AND docs/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).

Value (the implementation is genuinely good)

Findings / misses

  1. SEVERE — feature-dev cannot converge on a feature that ADDS files. The reviewer anchor protocol correctly says “diff 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).
  2. Cost of non-convergence: $4.95 / 101k tokens / 507 steps burned on 10 passes that could never pass — the loop has no “is this failing for a structural reason I can’t fix?” escape, it just re-runs the fixer against an unfixable blocker.

Lessons for next run