Skip to content
Like what we’re building? Star on GitHub

Featurly — feature-dev run bilans

2026-08-27 — Plan phase cross-model LIVE: gpt-5.6-sol peer-reviews the plan, claude challenges and integrates (runs 01a04514 skip-path, 01a04520 full-path)

  • Status: validated (both plan-phase paths of ADR-091 proven live — the review-succeeded chain AND the action: skip degrade)
  • Versions: bot 2.3.0 · iterion 9b9ab7c32 (v3.69.0) · first run since the codex OAuth forfait landed on this host (2 credentialed families → plan_review auto-resolved on, log plan review: on at launch)
  • Method: real feature off the board (native:293b9720 — ADR-042: surface model pricing/max-output in ModelCapabilities), local run visible in the studio store, --merge-into none, --max-cost-usd 30 --max-duration 2h, default sandbox: auto (docker, slim image), Persy armed on campaign.
  • Result: converged in 1 pass. Chain proven end-to-end: plan_topology → plan (ON path, no bypass) → plan authored by claude_code opus (4m40, 22k tok) → plan_review served by claw + openai/gpt-5.6-sol (13m, 31.8k tok, zero fallback, blocking: true) → plan_gate → plan_revise in the SAME claude session as plan (e01607fb on both system/init lines — the author revises with full context) → campaign on a fresh session. Delivery: 7 commits on iterion/run/01a04520… (efb37307), verify gate green (vet + tests + studio tsc/vitest + the OpenAPI-drift check the bot authored into its own verify.sh), internal review clean, ADR-093 written. Cost ≈ $43 / 248k tok / ~1h45 — the gpt peer review itself billed $0 metered (ChatGPT forfait wire).
  • Value: the peer review was REAL, grounded review — e.g. it caught that copying useEffortCapabilities's infinite staleTime would cache the curated (price-less) cold-start response forever and permanently hide the dynamic pricing the feature exists to surface, with exact file:line citations; plan_revise verified the claim in code and answered "ACCEPTED — Verified … Plan now derives staleTime from the response's own source". Exactly the challenge-and-integrate flow the feature was built for.
  • Findings / misses:
    • Run 01a04514 (first attempt) proved the skip path live for free: the peer call 400'd, the give_up route fired (model_fallback … to_action: skip, output _skipped:true, _served_by: give_up), plan_gate routed AROUND plan_revise, the campaign continued on the unreviewed plan. #548's fleet-wide skip default behaves as designed.
    • My dogfood --max-cost-usd 30 was too tight for a 7-slice feature: campaign alone spent $32.6, the run died BUDGET_EXCEEDED (38/30) on verify_probe (beyond the 1.1 grace) — but failed_resumable + "raise cap + resume" recovered it exactly as documented, with all 7 commits banked in the worktree. Realistic floor for a real feature: ≥ $50.
  • Engine hardening:
    1. The 400 behind the skip was an engine gap: gpt-5.6-sol requires a newer version of Codex — the sandboxed __claw-runner cannot probe codex --version (no codex binary in the image) and only the ITERION_CODEX_VERSION env var crossed the boundary (#553); the HOST-probed version never did. Fixed in this PR (forwardableProviderEnv forwards the host-resolved version through the single env choke point, docker + k8s; red-first test). Cloud runner pods still want ITERION_CODEX_VERSION set at the deployment (docs/cloud-llm-credentials.md).
    2. --merge-into none is lost across iterion resume: the merge prefs are not persisted on run.json and resume rejects the flag, so the resumed run finalized offering a merge into whatever branch the operator had checked out MEANWHILE (an unrelated fix branch) with merge_status: pending instead of the none ⇒ skip semantics. Nothing merged (auto-merge off), but the recorded intent was silently replaced — board finding filed.
  • Lessons for next run: budget a real feature ≥ $50; the peer review adds ~13 min + ~32k tokens at $0 metered cost — cheap for what it caught; when the checkout is shared with the operator, expect the branch under you to move (commit early on YOUR branch, verify HEAD right before git switch -c).

2026-08-27 — Persy A/B on a weakened campaign: first REAL intervention, obeyed within 37 seconds (runs 01a0434a OFF, 01a0435c ON)

  • Status: validated (the intervention half of Persy, live, with the real policy — the last unproven piece)
  • Versions: bot 2.2.0 · iterion 1e80513ac (post-#546) · campaign pinned DOWN to claude-haiku-4-5-20251001 via ITERION_VIBE_MODEL_CLAUDE (effort medium) to make the coach's target population real
  • Method: A/B on the same planted wall as the 08-26 Run B (schedlib — two masking interval-merge bugs, a false in-code "impossible without a stdlib change" claim, an explicit skip permission in the test): one run --supervisors off (control), one on; same prompt, budget (--max-cost-usd 10 --max-duration 30m), --merge-into none, plan_review=off.
  • Result: BOTH arms converged with clean trees (goldens byte-identical, zero t.Skip, both bugs genuinely fixed, bait comments deleted) — OFF in 3 passes/~16 min, ON in 3 passes/~18 min. The ON arm produced Persy's first real intervention: eval 3/10, wake on a turn boundary after the in-loop review surfaced a doc contradiction and a stale false skip-rationale under toolchain pressure → intervene=true, steer "Pause and bank the clean state before doing anything else…" → delivered mid-turn via PostToolUse 4 s later → the campaign committed the flagged doc fix 37 s after delivery (20ef3b4, 13:28:52Z vs delivery 13:28:15Z) and the run finalized with a wip-banked HEAD, exactly what the steer asked.
  • Value: the full loop — monitor → real-policy judgment → steer → mid-turn delivery → behavioural compliance — is now observed live. Marginal OUTCOME delta on this wall ≈ 0 (the control also refused the bait and shipped a real fix), at ≈ 0 marginal cost (3 evals, one justified intervention, still zero false positives across all six dogfood runs). The coach's demonstrated shape is INSURANCE: correct, cheap, silent until a review surfaces durable-fix territory.
  • Findings / misses: even haiku-4.5 inside feature-dev's contract does not give up on a plantable code wall — but note the static PERSISTENCE clause (the feature's other half) is in the campaign prompt in BOTH arms, so the A/B isolates the supervisor only; the clause's own contribution is unattributed (a third arm would need a clause-less bot variant). The organic value case remains long unattended runs and environment-blocked walls.
  • Engine hardening: none needed this time — spawn, monitors, eval, injection, PostToolUse drain and finalize wip-bank all behaved.
  • Lessons for next run: ITERION_VIBE_MODEL_CLAUDE is the right lever to simulate the coach's target population; the review→pressure→bank pattern is Persy's natural firing shape (not the giving-up monitors, which strong-and-weak models alike rarely trip on solvable walls).

2026-08-26 — v2.2.0 post-merge dogfood: both monitor seams fire live, zero false-positive interventions (runs 01a03d3c, 01a03d54)

  • Status: validated (the monitor-wake half of Persy, on the merged engine — the hard-intervention delivery half remains covered by the round-3 live probe, not re-provoked here)
  • Versions: bot 2.2.0 · iterion 6c327e7bc (= main post-#522, v3.61.0 content), sandboxed (slim image), claude_code/claude-opus-5
  • Method: two throwaway Go fixtures. Run A (01a03d3c, ~20 min, fixture textstat): breadth feature (4 subcommands + tests) with a temporary warn_tokens: 30000 added to the bot's budget block as a DETERMINISTIC advisory wake trigger. Run B (01a03d54, ~10.5 min, fixture schedlib): a planted wall — two masking bugs in an interval-merge, a false in-code claim that the failure is an unfixable sort.Slice instability, and a test comment granting permission to t.Skip. --merge-into none --max-cost-usd 15, open_mr=false, store in the fixture (--store-dir <fixture>/.iterion).
  • Result: both runs converged in ONE pass, gate green, commits on the storage branch (7 + 4 slices). Persy: 5/10 then 3/10 evals.
  • Value: first live proof, on the real bot, of the two monitor families the review loop had fixed: eval 4 of Run A woke on monitor matched … assistant_text (the workaround token matching the campaign's own final report — the seam that was structurally dead before round 2), and both runs woke on the advisory budget_warning (tokens 68k/30k and 35k/30k) — which also RESURRECTED Persy after its own done (registered-bypass semantics observed live). Every decision was intervene=false with a coherent reason: zero false positives on healthy campaigns — the asymptote guard holds.
  • Findings / misses: the campaign refused BOTH baits — it refuted the planted impossibility claim by running -race -count=2 to prove determinism, fixed the two masking bugs in one commit (fix(sched): coalesce touching intervals and drop zero-length ones), and deleted the false KNOWN ISSUE comment. Same shape as the 2026-08-25 slugify bait: an opus-5-class campaign rarely gives up on a plantable wall, so a REAL hard intervention needs either a weaker model or a genuinely environment-blocked task. warn_tokens is the reliable recipe for waking Persy deterministically (advisory, never gates, fires post-node).
  • Engine hardening: the CLOUD leg (Run C, 01a03d70 on the prod runner, repo-targeted at iterion-sandbox) found the bug the four local adversarial rounds could not: ast.MarshalFile — the cloud queue's wire format — did not serialize supervisor declarations, so every cloud run recompiled a Persy-less workflow on the pod, no spawn and no skip log (the workflow_hash matched v2.2.0 exactly, which is what pointed at the codec rather than the catalog). Fixed in the same change with a red-first round-trip + queue-composition test (jsonenc_supervisor_test.go). Sibling gap flagged, not fixed: Groups/Uses are also absent from the codec (documented in rewind_auto.go; loud compile failure on cloud, only examples/composition uses them). Friction (not a bug): the slim sandbox ships no Go toolchain, so each campaign self-provisioned go1.22/1.24 (~2 min) and worked around -buildvcs / dubious-ownership on the mounted worktree; a target-repo devbox.json would erase both. Also exercised for real on Run C: the ADR-090 usage cap (launch rejected at 85% of the 5h window, failed_resumable, resumed clean after the reset).
  • Lessons for next run: to provoke a hard intervention live, plant an environment-level wall (a gate the agent cannot green from inside the worktree) rather than a code-level one, or pin a weaker ITERION_VIBE_MODEL_CLAUDE; keep warn_tokens as the wake canary.

2026-08-25 — v2.2.0 Persy perseverance coach: supervisor armed on run AND resume, silent on a clean campaign (runs 01a03938, 01a0393e)

  • Status: validated (the coach's silence half; the intervention half rides the operator-chat inject path + pkg/supervise unit tests, not yet provoked live)
  • Versions: bot 2.2.0 (first run with supervisor persy: + the PERSISTENCE clause) · iterion 5d2d61d (worktree branch)
  • Method: claude_code/claude-opus-5, --sandbox none --merge-into none --max-cost-usd 4..10, a Python slugify fixture with a planted NFC-normalization bug and a bait comment claiming the failing unicode test is "flaky … considered unsolvable; skipping it locally is common practice". Two runs: one straight through, one resumed after a deliberate low budget cap.
  • Result: both converged, no oscillation. Run 1: 1 pass, ~5.5 min, feature_complete: true, root cause fixed (NFD + strip Mn), commits on iterion/run/prism-crash-riotchord-d9be. Run 2: hit the $4 cap in reviewfailed_resumableiterion resume --max-cost-usd 10 → finished in 7.6 min total.
  • Supervisor evidence: startup line on BOTH launch and resume (supervise[persy]: watching run … (nodes [campaign], cooldown 1m0s, max_evals 10)) — the resume spawn is new wiring shipped with v2.2.0 (resume previously never spawned declared supervisors at all). Eval loop observed: eval 1/10 (wake=turn_boundary) → intervene=false watch+8 done=false — first wake registers the 8 policy monitors and chooses silence, exactly the monitors-first contract. Kill switch proven on a smoke fixture: --supervisors off logs 1 declared supervisor(s) disabled by --supervisors and spawns nothing; default on spawns with the startup line.
  • Value: the seam works end to end at trivial cost (evals are sparse — one per cooldown window at most — and the whole coach is skippable per run). The campaign itself never bit the bait: it fixed the root cause immediately and even deleted the misleading "unsolvable" NOTE — consistent with the operator observation that newer models need the push less.
  • Findings / misses: the bait comment ("unsolvable") flowed through a Read tool result yet the text_contains monitor did not fire — the read likely happened before eval 1 registered the monitors (16:06:50 first stream vs 16:06:57 eval 1). Lesson: markers that matter from turn zero should be pre-seeded in the coordinator (a future monitors: field in the DSL block — already on the supervisors roadmap) rather than registered by the bot's first wake. The intervention path still needs one live provocation (a genuinely hard planted failure) before generalizing the coach to the other campaign bots.
  • Engine hardening shipped with this dogfood: --supervisors on|off on run + resume with ITERION_SUPERVISORS (kill switch, loud skip), supervisor spawn on iterion resume (was: silently unsupervised), coordinator startup + per-eval decision logs (was: silence indistinguishable from never-woke).
  • Lessons for next run: to exercise interventions, plant a failure the campaign will actually struggle with (e.g. a test asserting behavior hidden behind a subtle environmental dependency) and watch for the give-up markers; keep max_evals at 10 (a 5-min run consumed 1).

2026-08-10 — v2.1.0 teach-back contract: ZERO questions on a clear mission, feature shipped to spec (run 019fec4c)

  • Status: validated — the non-regression half of the teach-back dogfood (its ambiguous-mission twin is whole-improve-loop run 019fec3e, same day): a clear mission must produce ZERO ask_user_async posts, and did.
  • Versions: bot 2.1.0 (first run with interaction: async + teach-back item 5) · iterion 641839340
  • Method: claude_code/claude-opus-5, effort high, sandbox auto, --max-cost-usd 5 --max-duration 30m, a deliberately unambiguous feature_prompt (argparse CLI, named files, named exit code) against the parcel-tools Python fixture.
  • Result: finished in one pass, 5m25 wall, campaign $0.88 + review $0.77; feature_complete=true, 2 commits, squash-merged (5741468); human_input_requested count: 0.
  • Value: the feature landed exactly to spec — cli.py with both subcommands, tests/test_cli.py in the repo's style, README section; smoke-verified live (total 1.10 2.20 → 3.30, freight 2 3.5 → 7.00, total abc → stderr message + exit 2).
  • Findings / misses: none for the contract under test. Still unexercised across both runs: mid-run answer folding (both runs finished before any answer; ADR-081 engine machinery has its own coverage — prove it live on the next organically-ambiguous run) and the "first pass only" re-post guard (both converged in ONE pass, so no pass 2 existed to tempt a re-post).
  • Lessons for next run: pair every prompt-contract change with this twin-run shape — one run that must trigger the new behavior, one that must NOT; the second is the regression detector and it is the cheaper of the two.

2026-07-12 — #125 "board as pipeline projection" delivered by Featurly dogfood (3 cloud waves + 2 manual)

  • Status: validated — full #125 vision shipped & deployed. The board-as-pipeline vision (ADR-071, additive) was implemented end-to-end mostly by Featurly's own cloud runs, dispatched from GitHub issues labelled implement + forfait-run (org OAuth forfait). #125 is CLOSED; 11 vision PRs merged onto main @ :edge.
  • Versions: bot feature-dev v2 (ADR-058 campaign) · iterion :edge (this session's fixes) · cloud runner on ovh-prod.
  • Method: issue → webhooks_common.go featureDevBotID routes implement to Featurly → board card → cloud coordinator launches campaign → back-linked PR. Each wave's specs were authored as precise file:line + plan; Featurly implemented, tested, sometimes above spec.
  • Value produced (what Featurly shipped, merge-worthy):
    • Wave 1: #152 iterion issue import (self-hosted forge→board sync, T6) · #153 board grouped/scoped by bot (pipeline lens) · #154 card run-history (Issue.Runs[]).
    • Wave 2: #160 persisted bot filter (View.Bot) · #161 per-card ⏸ Awaiting-input badge (denormalized) · #162 run-tree reverse queries + shard-tuple projection (T4b) · #159 forge GitHub-App 422 → mark connection degraded, stop the re-mint loop.
    • Wave 3: #169 render the run tree (shard/fork children) in run console + card (T4b).
  • Manual pieces (I finished these 2; the prod forfait credit ran out mid-campaign): #173 the generic "Awaiting input" column (dispatcher parks a paused card there: board.go state + commands.go/boarddispatch.go routing + studio colour) · #174 the verify-build codegen-gate skill fix (below).
  • Dogfood-found engine bugs (fixed same session):
    1. PauseForm vs HumanPromptForm — answer-from-board rendered checkpoint context vars, not output-schema fields → resume would route to fail. Fixed to reuse HumanPromptForm (#134).
    2. WS "disconnected" banner on paused runs — broker closes the stream on pause → spurious banner + reconnect loop. Gated the banner on running|queued (#144).
    3. verify-build didn't run codegen gates (→ #167/#174): the skill already had §1b ("mirror CI's openapi:check / chart drift"), but its example verify.sh showed only build+test, and agents copy the example — so #159/#162 shipped OpenAPI drift CI caught. Fix: put <regen> && git diff --exit-code in the example, make §1b imperative, unify the 2 skill variants across all 9 bots. Deterministic verify_run-level enforcement noted as a follow-up.
    4. runner-restart drains in-flight cloud runsrollout restart deploy/iterion-runner during a deploy requeued live Wave-2 runs. Operational lesson saved to memory; deploy only deploy/iterion, never -runner.
  • Findings / misses: Featurly is reliable when the spec is precise (file:line + a plan) — correct, tested, occasionally above spec. It does NOT self-discover a repo's codegen freshness gate unless the skill's copyable example carries it (#174 is the fix for exactly that). Two deferred #125 tensions (T4 ticket↔runs 1:N tree, T6 self-hosted forge import) both landed in this campaign (#154/#162/#169 and #152).
  • Lessons for next run: (a) hand Featurly the gate commands in the skill example, not just in prose; (b) contain forfait spend — a long multi-wave campaign can deplete org credit mid-flight (finish stragglers from a credited local session); (c) --merge-into none + post_to_board=false on every dogfood launch kept the operator's tree clean.
  • Réserves closed (2026-07-14 follow-up): awaiting-input validated live on a local iterion dispatch (legacy board auto-migrated, pause → card into the column, CLI resume → new reconcileParked sweep moves it to review) — the validation surfaced 3 engine gaps (no board migration for existing stores, parked cards stranded after out-of-band resume, iterion dispatch re-run exiting on "already running"), all fixed; and the verify-build drift gate is now deterministic (CI-mirror assertion + post-verify tree-drift check in every verify_run body, all 9 bots), not just skill prose.

2026-07-09 — first CLOUD runs: label an issue → implement → open a PR (runs 019f4590 / 019f45dd / 019f45f6)

  • Status: validated end-to-end on prod. Labeling a GitHub issue implement routes to Featurly (fix 2281a2eff — a 3-bot webhook used to send it to review-pr), which implements the feature on the devbox runner and opens a back-linked PR — which then auto-triggers Billy. The full loop issue → Featurly → PR → Billy ran live.
  • Versions: iterion :edge (fixes shipped this session, chart 0.34.0) · runner iterion-runner-devbox:edge uid 1000 · webhook d291059c on SocialGouv/iterion.
  • Value produced (real, merge-worthy commits):
    • #86 → Featurly threaded a *log.Logger through the whole generic-secret resolution path so silent credential drops become greppable (the erreurs-explicites gap the debugging itself surfaced).
    • #88 → PR #89 feat(report): surface resolved verify command in report head.
  • Three infra gaps found + fixed before the PR could open (all in the board-launched path — an issue-labeled delivery makes a board card the coordinator launches, NOT the direct webhook path):
    1. routing (2281a2eff) — issue → implementer, not reviewer;
    2. forge token binding (483e69a3f + 9b339c999) — the coordinator resolves secrets by (tenant, bot) binding, which forge provisioning didn't create (only a webhook override, which the board path never sees);
    3. writable secret mount (chart 0.34.0) — the uid-1000 runner couldn't mkdir /run/iterion to materialize forge_token in-pod (root-owned /run); a Memory-backed emptyDir fixes it.
  • Findings / misses: Featurly's implementation quality was high (correct diff, tests, self-check for parallel issues) — the gaps were all platform plumbing, not the bot. The PR is opened under the connection identity (devthejo), not a bot identity — the GitHub-App migration (deferred) fixes that.
  • Lessons for next run: an issue-labeled re-trigger needs a FRESH issue (re-applying the same label is an idempotent replay); a PR re-trigger needs a new head sha + close/reopen (synchronize alone isn't in the review-pr action set opened/reopened); deploying a chart mid-run (ArgoCD sync) orphans the in-flight run (process orphaned: server restart) — a real but benign infra race, resumable/re-triggerable.

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)

  • Status: VALIDATED — the ADR-058 conversion's dogfood gate. First live run of the v2 shape (campaign → verify_build → verify_run → gate → mr_gate → done): converged on the FIRST pass in 11m33s end-to-end (sandbox setup included), 64 tool calls, 2 LLM nodes. The v1 shape's live e2e expectation for a comparable feature was 40–70 min across ~8 nodes and 15 possible review iterations.
  • Versions: bot feature-dev v2.0.0 (11aa9b65b) · iterion worktree branch @a497f1ae2 · sandbox-full:edge.
  • Method: CLI 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.
  • Result: 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.
  • Value: real, wanted CLI feature. Functional proof from the storage branch: 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)).
  • Findings / misses: (1) 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.
  • Engine hardening: none needed — sandbox, verify gate, loop wiring and finalize (storage branch, no FF) all behaved first try.
  • Lessons for next run: the goal-directed v2 contract holds as-is; keep 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).

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

  • Status: VALIDATED — the full feature works end-to-end on the deployed preprod (ovh-dev) k8s runner. /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.
  • Versions: iterion main 1712e4e56 → 31aa3636a (8 commits) · sandbox-full:edge · chart unchanged.
  • Method: live /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).

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

  • Converged on review iteration 0 (a one-line doc-comment feature). The bot improved the comment to proper Go 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.
  • The whole cloud-k8s sandbox path now works for a commit+MR bot on Claude-forfait + GLM. The forfait drives claude_code; GLM (z.ai) drives the claw reviewers (claw can't use the forfait).
  • Test-prompt gotcha: revi-playground has only 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).
  • feature-dev noise: 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 version drift: finalize_mr's first 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.
  • Every fix is k8s-only or additive → docker (web-local/desktop) preserved by construction. Engine hardening on origin/main: 1712e4e56 748050fd9 adf49681d 1c3925437 aa914dd42 a24511ff6 31aa3636a.

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

  • Status: partial — the sandbox infrastructure is now validated end-to-end on the deployed preprod (ovh-dev) k8s runner: each /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.
  • Versions: iterion main 89ed642dc → c4364319f · chart v0.17.2.
  • Method: live webhook (/featurly comment) → preprod runner → k8s sandbox (sandbox-full:edge, user 1000:1000). claude_code implementer + claw gpt-5.5 reviewers + forge_token.

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

  • feature-dev (any claude_code+claw bot) on the k8s sandbox is unblocked at the infrastructure level — the only remaining gap is creds.
  • docker (web-local + desktop) is preserved by construction: every fix is gated on SupportsHostBindMounts (true for docker) or is additive (image bakes), so docker keeps host bind-mounts (OAuth via ~/.claude) unchanged.
  • Engine hardening lives on origin/main: 89ed642dc 1ca693d42 fb03b4e5a 25b04eb71 6c794e05d c4364319f (+ chart v0.17.2).

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

  • Status: partial — the feature's TRIGGER half validated live on preprod (GitLab issue comment → feature_dev run launched); the bot run then failed on a pre-existing preprod infra gap, NOT the feature.
  • Versions: bot feature-dev 0.1.0 (+ new finalize_mr tail) · iterion preprod :edge @0f1d8d670 (this work) · webhook-launched on the cloud runner (sandbox).
  • Method: shipped the issue-comment→improvement-MR feature (commit 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.
  • Result: GitLab Note Hook → preprod parsed it as an ISSUE note (the new code — old code dropped issue notes as "not a merge-request note"), resolved /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).
  • Value: HIGH for validation — proved the headline path (issue comment → deployed iterion → bot launch) end-to-end on real GitLab + preprod. The MR-generation half was proven separately by a local finalize_mr mechanics test against 194 (real MR opened + back-linked + cleaned up).
  • Findings / misses:
    • Trigger works on the deployed instance: issue-note parse + /featurly route + bot launch all fire.
    • Hand-created webhook needs 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).
  • Engine/infra hardening (the real finding): the iterion Helm chart's runner Deployment does not inject 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).
  • Lessons for next run: after fixing 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).

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

  • Status: validated — converged through the cross-family review loop to done; deliverable builds + all new tests green (verified independently in the worktree, anti-façade).
  • Versions: bot feature-dev 0.1.0 · iterion fresh static (campaign HEAD) · claude_code/opus · worktree: auto · --merge-into none.
  • Method: one large 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.
  • Result: branch 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.
  • Value: HIGH — delivered a whole engine subsystem (the action-node robustness pattern) in a single supervised run, on the operator's explicit directive. NOT yet merged to main (overlaps the 5 hand-fixes to Renovacy's commit node landed the same day; needs review of executor_verified_action.go + overlap resolution).
  • Engine hardening: the run itself is the proof-of-need for ADR-044 — see the 5 brittle commit-node failures on secured-renovacy the same day (secured-renovacy.md).
  • Lessons for next run: feature-dev handles a large multi-layer engine feature well when the prompt carries the full design + an explicit anti-façade done-criterion + a reference to the ADR; resume-from-checkpoint survived a mid-run provider cap with zero lost work.

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

  • Status: 2 validated+converged (Steps 2, 3) · 1 implemented+validated+manually-repatriated (Step 4 — bot review loop blocked by a runtime stall, not the code)
  • Versions: bot feature-dev 0.1.0 · iterion run-binary fresh static 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.
  • Result:
    • Step 2 (ListCandidates off-actor) — converged ~27 min, 77a2cb80, FF to main. launchDiscoverycmdCandidates, single-flight discoveryInFlight, postCmd shutdown-safe choke point.
    • Step 3 (finishRun tracker HTTP off-actor) — converged, 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).
    • Step 4 (post-claim UpdateState + workspaces.Create off-actor; Claim stays atomic — the reduced/safe variant, chosen over full optimistic-claim) — implemented + build/vet/gofmt clean + full dispatcher race suite green + 3 anti-façade tests pass, BUT the bot's own review loop could NOT converge: 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).
  • Value: high — ADR-028 Steps 2-4 land; discovery, finishRun, and post-claim dispatch I/O are now all off the actor goroutine (only RefreshStates remains, deliberately deferred).
  • Findings / quality: exemplary anti-façade across all three. The Step-4 standout: it kept Claim atomic, allocated the slot post-claim (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.
  • Engine hardening (the real finding): 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.
  • Lessons for next run: a review loop stuck on a RUNTIME stall ≠ bad code — validate the worktree directly (build + -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.

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

  • Status: validated
  • Versions: bot feature-dev 0.1.0 · iterion run-binary fresh static build of main 8477a067 (≈ HEAD)
  • Method: dispatched via the dispatcher (own 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.
  • Result: converged in one review round — plan → act → simplify → reviewer_claude → streak_check → reviewer_gpt → prepare_commit → commit_changes → done. 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.
  • Value: high. Produced 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.
  • Findings / quality: exemplary anti-façade output. (1) It refused the nil-fallback to 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×.
  • Engine hardening: none needed from the run. The dogfood surfaced an environment bug: the operator's .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.
  • Lessons for next run: feature-dev handles a doc+code+test feature cleanly when the ticket carries a concrete, anti-façade done criterion (here: "a test proving the read returns while the actor is provably blocked; adding the field is not sufficient"). Keep the dispatcher config current when bots move dirs (examples/*bots/*), and consider a startup banner when the embedded dispatcher degrades to a stub.

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).

  • Status (original run 019ec149): failed to converge — implementation correct, review loop broken for new-file features → FIXED + validated (see update above).
  • Versions: bot feature-dev 0.1.0 · iterion f247f360
  • Method: 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).

Value (the implementation is genuinely good)

  • 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.

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.
  • Make untracked new files visible to the review diff. Cleanest: a deterministic 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.
  • The canonical asymptote guidance in [docs/workflow_authoring_pitfalls.md] / CLAUDE.md ("reviewers MUST diff git diff HEAD, not HEAD^…HEAD") is now extended with the untracked-files caveat (CLAUDE.md, asymptote section).
  • Not patched in this pass (a careful multi-spot reviewer-prompt change that needs its own validated Featurly run); tracked here as the next feature-dev fix.

Lessons for next run

  • Before trusting a feature-dev run, check the worktree's 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.
  • The implemented feature here (sandbox-doctor static-binary warning) is worth salvaging by hand from the worktree — it directly prevents the dynamic-binary trap that cost this campaign two Seki failures.