Skip to content

[Spec 1338] retire-gemini-cli-as-a-builder - #1342

Open
mohidmakhdoomi wants to merge 67 commits into
mainfrom
builder/spir-1338
Open

[Spec 1338] retire-gemini-cli-as-a-builder#1342
mohidmakhdoomi wants to merge 67 commits into
mainfrom
builder/spir-1338

Conversation

@mohidmakhdoomi

@mohidmakhdoomi mohidmakhdoomi commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Retires the standalone Gemini CLI as a supported built-in builder/architect harness. Google ended consumer Gemini CLI access (free, Pro, Ultra) on 2026-06-18, so it can no longer be presented as supported. A gemini harness now fails closed at every spawn (including --shell) / architect launch / reconnect / clean-exit boundary with a specific retirement message (no orphaned worktree, no stray shell PTY or DB row, no Tower crash); codev doctor flags a persisted gemini config for both roles; and the docs no longer present it as a built-in. Retained-access users (Standard/Enterprise or API-key) keep a sanctioned escape hatch: an explicit custom gemini harness selected via shell.builderHarness / shell.architectHarness.

Closes #1338

Changes

  • Resolver (Phase 1): a RETIRED_HARNESSES sentinel in the shared resolveHarness fails closed on both the explicit path (specific retirement, not a generic "unknown" error) and the auto-detect path (never the silent CLAUDE_HARNESS fallback). Built-in GEMINI_HARNESS removed; gemini retained in the detector so a gemini command is recognized-and-retired, not misclassified.
  • Spawn/launch (Phase 2): fail-closed guards at all four boundaries — a spawn preflight that runs for every mode including --shell (before any worktree / porch / PTY / DB state is created), architect launch, reconnect (buildArchitectReconnectRestartOptions → fail closed), and clean-exit relaunch (a new FreshLaunch { stop: true } contract). No orphaned state, no Tower crash.
  • doctor (Phase 3): flags a retired gemini builder/architect config with the retirement message, for both roles; preserves the custom-harness escape hatch (an explicit <role>Harness backed by a matching custom def is not flagged). The custom-harness recommendation interpolates the configured role.name rather than a hard-coded "gemini", so the advice stays correct as RETIRED_HARNESSES grows.
  • Docs (Phase 4 + Review): README retirement framing with a working custom-harness snippet (GEMINI_SYSTEM_MD env injection); the runtime message, doctor recommendation, and README all name the explicit selector; governance docs (arch.md, lessons-learned.md) refreshed. The upstream release CHANGELOG.md is intentionally left untouched — contributors don't edit it.
  • Diagnostics (Review): siblingRegistrationIsLive logs the retirement reason when it prunes a retired-architect registration, so the reconcile loop's generic "no resumable session" prune line can't misattribute the cause.
  • Unaffected: Claude / Codex / OpenCode / custom builders; the gemini consult lane (now agy).

Testing

  • Full unit suite: 4148 passing / 48 pre-existing skips / 0 failing (e2e excluded).
  • spawn-retirement.test.ts drives the real spawn() against a real temp workspace and asserts 0 orphaned .builders/ + 0 porch state after a rejected gemini spawn — for both a worktree spawn and a --shell spawn (shell mode is no longer exempt from the preflight).
  • Regression coverage across harness / config / doctor / tower-utils / tower-instances / session-manager; built-ins (claude/codex/opencode) regression-tested for both roles. Added: doctor asserts the interpolated custom-harness clause; tower-utils asserts the retired-prune log fires (and does not fire for a live sibling).

Spec

codev/specs/1338-retire-gemini-cli-as-a-builder.md

Review

codev/reviews/1338-retire-gemini-cli-as-a-builder.md

🤖 Generated with Claude Code

mohidmakhdoomi and others added 30 commits August 3, 2026 15:12
Retire the standalone Gemini CLI (`gemini`) builder harness. Google
discontinued the Gemini CLI for Pro/Ultra/free tiers on 2026-06-18.

Spec recommends a retirement sentinel in the harness resolver so that
selecting gemini as a builder — via explicit builderHarness or via
command auto-detection — fails closed with a clear retirement message,
closing the #929-class silent-fallback-to-Claude footgun. Consult lane
(agy / consult -m gemini) is out of scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Iteration 1 review: Gemini APPROVE, Codex + Claude REQUEST_CHANGES.
All substantive points accepted and incorporated:

- Role-agnostic retirement: resolveHarness is shared (no role param) by
  architect+builder resolution; retire gemini for both roles explicitly.
- Two footgun modes made precise (silent Claude fallback at :392 vs
  undefined/TypeError at :387); sentinel must guard before both.
- Reframed as a Codev product retirement (consumer tiers ended 2026-06-18;
  Standard/Enterprise + API-key remain -> custom-harness escape hatch).
- Doctor branch redefined + builder-side flagging; assert structured fields.
- afx reset outcome recorded as decided/accepted.
- Tests expanded to getBuilderHarness/--builder-cmd/array-form/architect;
  coverage-by-replacement; doc criterion scoped (exempt historical+consult).

Rebuttal: codev/projects/1338-*/1338-specify-iter1-rebuttals.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Approach 1 (retirement sentinel in shared resolver), role-agnostic per
architect approval. Three phases:
1. Retire gemini in resolveHarness (core + resolver/config tests) —
   RETIRED_HARNESSES registry, guard both footgun exits (:387 undefined,
   :392 Claude default), keep detector, remove GEMINI_HARNESS + registry.
2. Surface cleanly: doctor redefinition + builder-side flagging + spawn/
   architect-launch clean-failure handling.
3. README presentation. Governance docs (arch/lessons) in Review phase.

agy / consult -m gemini out of scope throughout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Iter-1 review: Gemini APPROVE, Codex REQUEST_CHANGES, Claude COMMENT.
All points accepted; restructured 3->4 phases:

- Precedence FIX: built-in -> custom -> retired -> unknown (draft wrongly
  put custom before built-in, which would shadow supported built-ins).
- Spawn preflight before worktree/porch creation (spawn.ts resolves harness
  at :471, after createWorktree/initPorch) -> no orphaned state on reject.
- Enumerate 4 getArchitectHarness sites; :291 siblingRegistrationIsLive is
  a predicate -> guard to return false, not a launch.
- Doctor is persisted-config detection, not override-aware.
- Name 2nd BUILTIN_HARNESSES consumer harnessProviderFor (reset/context:468).
- Add CHANGELOG [Unreleased] breaking-change entry (Phase 4).
- Custom gemini escape hatch is explicit-only; auto-detect stays retired.

Phases: (1) resolver core+tests (2) fail-closed spawn/launch (3) doctor
(4) README+CHANGELOG. Governance docs in Review.

Rebuttal: codev/projects/1338-*/1338-plan-iter1-rebuttals.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hared resolver

Retire the built-in `gemini` harness (Google ended consumer-tier Gemini CLI
availability 2026-06-18). The retirement is role-agnostic: it lives once in the
shared resolveHarness and applies to both builder and architect resolution.

- Add RETIRED_HARNESSES registry (name -> explanation) plus prototype-safe
  isRetiredHarness / getRetirement predicates and a throwRetired helper.
- Remove GEMINI_HARNESS and its BUILTIN_HARNESSES entry; keep the `gemini` case
  in detectHarnessFromCommand so the auto-detect path lands on the retirement.
- resolveHarness fails closed on both exits: explicit name (builtin -> custom ->
  retired -> generic "Unknown harness"), and auto-detected command (retired check
  BEFORE the BUILTIN_HARNESSES lookup). Never a silent Claude fallback (Issue
  #929 class), never undefined. Explicit custom `gemini` still resolves (escape
  hatch for retained enterprise/API-key access).
- Tests: retirement + escape-hatch + built-in-not-shadowed + predicate coverage
  (harness, harness-integration); gemini resume guard re-expressed as opencode;
  config paths (--builder-cmd/--architect-cmd/explicit harness/array-form) all
  fail closed. Two stale gemini-as-builder comments swept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface the gemini harness retirement (Issue #1338) at the spawn and
architect-launch boundaries so a retired selection fails closed with no
orphaned state and no Tower crash.

- spawn.ts: preflight in the spawn() dispatcher (mode !== 'shell')
  aborts a retired builder harness BEFORE any handler runs. createWorktree
  itself resolves the builder harness, so guarding above dispatch is what
  guarantees a rejected gemini spawn leaves zero worktree/porch/db state.
- config.ts: assertBuilderHarnessNotRetired — delegates to getBuilderHarness
  (single source of truth) and rethrows only the retirement; every other
  resolution error still surfaces at its existing call site.
- harness.ts: RetiredHarnessError — a distinct error type so callers can
  scope a catch to the retirement and rethrow everything else.
- tower-utils.ts: siblingRegistrationIsLive catches the retirement and
  returns false (a retired registration is not live -> reconcile prunes it),
  so the throw never aborts the whole sibling-reconcile pass. buildArchitectArgs
  documents the launch-boundary fail-closed throw.
- tower-instances.ts: addArchitect converts the retirement throw into a clean
  { success: false, error } (the add-architect route awaits it without its own
  try/catch); launchInstance already funnels through its own catch.

Tests: assertBuilderHarnessNotRetired (cmd/explicit/array forms; no-op for
supported; defers on unknown), siblingRegistrationIsLive -> false for gemini,
buildArchitectArgs throws for gemini, launchInstance + addArchitect return a
clean retirement result. Full unit suite green (4128 passed / 48 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es no state

Promote the phase_2 no-orphaned-state proof into a committed regression test.
Drives the real spawn() dispatcher against a real temp git workspace whose
.codev/config.json selects the retired gemini builder harness (real config
loader, no mocks): the spawn rejects with the retirement and leaves zero
.builders/ and zero codev/projects state. Tower/GitHub are never reached
because the preflight throws before the dispatcher hands off to a handler.

Guards the invariant that the preflight runs ABOVE every state-creating
handler — createWorktree itself resolves the builder harness, so a guard
placed below dispatch would orphan a half-built worktree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onnect (Codex iter1)

Codex's phase_2 review found two reachable gaps on the restart/reconnect
paths — reachable when a config is edited to gemini mid-session, or Tower
restarts reading a gemini architect config (initial-launch analysis alone
called them unreachable and missed both):

1. Fail-open on reconnect: resolveArchitectRestart propagated
   RetiredHarnessError, but both tower-terminals consumers
   (_reconcileTerminalSessionsInner, getTerminalsForWorkspace) caught ALL
   harness errors and fell back to relaunching cmdParts[0] — the retired
   gemini binary itself — with no role injection. Extracted the duplicated
   block into buildArchitectReconnectRestartOptions (tower-utils), which
   fails closed on retirement (returns undefined: reconnect to a live
   process if any, never auto-restart into the retired binary) while keeping
   the plain-command fallback for other harness errors. includeFreshLaunch
   preserves each site's prior behavior (startup reconcile wires the #1264
   clean-exit rerun; on-the-fly reconnect does not).

2. Uncaught throw on clean-exit relaunch: buildArchitectFreshLaunch.next()
   resolved the harness unguarded; session-manager invokes it with no
   try/catch, so a clean-exit relaunch of an architect whose config flipped
   to gemini became an uncaught Tower exception. Guard it: RetiredHarnessError
   -> log + rerun the original launch args with no harness injection (baseArgs
   come from the original supported-harness launch, so nothing re-injects the
   retired harness); rethrow any other error.

+7 regression tests (tower-utils.test.ts): both paths fail closed / no-throw
for gemini and stay unchanged for codex/claude; non-retirement harness errors
still degrade to the plain command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuttal for phase_2 review iteration 1: both Codex REQUEST_CHANGES points
(fail-open reconnect relaunch; uncaught clean-exit throw) accepted and fixed
in 1152783; Claude's non-blocking deviations recorded for the final review;
Gemini/Claude approvals stand.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…f a retired harness (Codex iter2)

Codex's phase_2 iter2 review found the iter1 C2 fix incomplete: returning
{args, env} from buildArchitectFreshLaunch.next() on retirement stops the
throw but NOT the relaunch. SessionManager's clean-exit handler retains the
launch `command` and applies only next()'s args/env — so if the retained
command is itself the retired binary (a custom `gemini` harness later removed,
or a config edit before a clean exit), the retired binary is respawned.
Fail-open.

Fix: give the FreshLaunch contract a `{ stop: true }` signal. On
RetiredHarnessError, next() now returns { stop: true } instead of args/env;
SessionManager's clean-exit handler honors it by ending the session (no
respawn) and surfacing the reason in the pane via session-gave-up ->
PtySession.notice — the same visible-teardown UX as the fast-clean-exit valve.

Tests:
- session-manager.test.ts: end-to-end regression through the real clean-exit
  handler with the retained command: "gemini" — asserts spawn is NOT called,
  the session is removed, and a `retired` reason is surfaced (the coverage
  Codex asked for; the iter1 test only checked returned args).
- tower-utils.test.ts: buildArchitectFreshLaunch.next() for a retired gemini
  architect now returns { stop: true }; codex/claude unchanged.
- tower-utils.test.ts: isolate HOME + TOWER_ARCHITECT_CMD in the three
  retirement describes (Claude iter2 non-blocking nit) so a dev with either
  set locally can't mask the workspace-config fixtures.

FreshLaunch is architect-only (single implementer, single consumer at
session-manager.ts), so the contract addition is low-blast-radius.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuttal for phase_2 review iteration 2: Codex's C3 (clean-exit relaunch of
the retained retired command) accepted and fixed in 9ec14c4 via a fail-closed
FreshLaunch stop signal; Claude's non-blocking test-isolation nit addressed;
Gemini/Claude approvals stand.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…th roles + builder side

codev doctor's gemini branch claimed "gemini is supported for builders, not
architects" — the inverted premise after the #1338 retirement. Redefine it to
present the retirement for BOTH roles, and add builder-side detection.

- Factor a shared local resolvedShellHarness(role) helper (raw shell.<role> /
  shell.<role>Harness, array-or-string, via detectHarnessFromCommand) used by
  both the architect and builder branches so they can't drift. Persisted-config
  only (not CLI/env override-aware), matching the spec's scope.
- getRetirement() is the single source of truth for "retired": its truthiness
  gates the branch and its message is surfaced. The architect branch now shows
  the retirement (both roles) instead of "builder-only"; a new builder branch
  flags a retired builder harness proactively (Phase 2 already fails it closed
  at spawn — this is user education before a spawn is rejected).
- Update the structured issue:/recommendation: fields (the stable assertion
  target): "<harness> configured as {architect,builder} shell (harness retired)".

Tests (doctor.test.ts, new '#1338 shell-harness retirement flagging' describe):
gemini builder and gemini architect configs assert the structured fields and
that no output claims "supported for builders"/"builder-only"; explicit
builderHarness: gemini is detected; a supported config (claude builder + codex
architect) is not flagged. Doctor never calls resolveHarness, so it detects and
reports without throwing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mohidmakhdoomi and others added 11 commits August 3, 2026 20:46
…INI_SYSTEM_MD (iter2 review)

The README custom-`gemini` escape-hatch injected the role via `--system`, but the
Gemini CLI reads its system prompt from the GEMINI_SYSTEM_MD env var (retired
GEMINI_HARNESS at e222b9e^: args:[] + env{GEMINI_SYSTEM_MD}, empty script fragment).
A copy-paste user got a launch line the CLI rejects. Reproduce the retired provider
verbatim (roleArgs:[] + roleEnv/roleScriptEnv) and realign the same-named
"retained-access escape hatch" test to assert the GEMINI_SYSTEM_MD injection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… to name the explicit selector

The three retirement-guidance touchpoints must agree so a retained-access user can
actually follow the escape hatch. The README was fixed in phase_4; this aligns the
runtime message and doctor recommendation (accepted + deferred to Review by all three
reviewers across phase_4 iterations):

- harness.ts RETIRED_HARNESSES.gemini: name the explicit shell.builderHarness /
  shell.architectHarness selector (a bare auto-detected `gemini` stays retired).
- doctor.ts builder + architect recommendations: the "custom harness" advice now
  names the role-appropriate explicit selector.
- CHANGELOG: point at the README's GEMINI_SYSTEM_MD snippet (Claude iter3 note).

Tests: lock the message/recommendation shape (harness.test.ts, doctor.test.ts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ni harness retirement

- arch.md: drop gemini from the built-in HarnessProvider list; replace the stale
  "Gemini is builder-only" framing with current state (retired both roles, fails
  closed, custom-only via explicit selector); fix a now-stale override example.
- lessons-learned.md: +2 durable lessons (fail-closed at every resolution path when
  retiring a registry entry; documented config snippets must reproduce the real
  mechanism). Historical #929 lesson left intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mohidmakhdoomi

Copy link
Copy Markdown
Collaborator Author

Architect Integration Review — PR #1342

Contributor-side integration review (we are not upstream maintainers; final merge is a maintainer's call). Ran a 3-way CMAP plus an independent line-by-line pass of the production diff (resolver, config/spawn preflight, all four architect boundaries, session-manager contract, the tower-terminals consolidation, and doctor).

3-way CMAP verdicts

Reviewer Verdict Confidence
Gemini APPROVE HIGH
Codex REQUEST_CHANGES HIGH
Claude APPROVE HIGH

The design itself is sound and consistently applied: both resolver footguns are closed (no silent CLAUDE_HARNESS fallback, no undefined on the auto-detect path), the typed RetiredHarnessError keeps every catch narrowly scoped, the four architect boundaries are each handled correctly (launch propagates cleanly, the liveness predicate returns false instead of aborting the reconcile pass, the clean-exit relaunch fails closed via FreshLaunch { stop: true }, and reconnect fails closed to undefined), and the tower-terminals de-dup is a genuine maintainability win. Typecheck + full unit suite green (4145 pass / 0 fail) independently reproduced.

Blocking (must address before merge)

1. afx spawn --shell bypasses the retirement (Codex — verified).
spawn() exempts shell mode from the preflight (spawn.ts:933, mode !== 'shell'), but spawnShell() runs commands.builder directly via startShellSession()bash -c <builder cmd> (spawn.ts:643, spawn-worktree.ts:990) and upsertBuilder(...). So a shell.builder: "gemini …" config still launches the retired Gemini via afx spawn --shell and persists a shell row — contradicting AC1 ("no longer treated as a supported builder option") and this PR's own "fails closed at every spawn boundary" summary. The exemption's comment ("shell spawns a plain terminal with no builder harness") is also factually wrong — shell mode does run the builder command.
Fix: extend the retirement preflight to shell mode (it delegates to getBuilderHarness, so the custom-gemini escape hatch is preserved), correct the comment, and add a test asserting no PTY / no DB row is created for a rejected gemini --shell spawn.

2. Revert the CHANGELOG.md edit (contributor-scope).
As contributors rather than upstream maintainers, we shouldn't be editing the release changelog — the [Unreleased]/version history is the maintainers' to manage. Dropping the ### Removed (Spec 1338…) block; the retirement is already fully described in the PR summary, the spec, and the review doc. Happy to re-add on a maintainer's request.

Non-blocking (cheap to fold in while revising)

  • Doctor recommendation: hard-codes "gemini" (doctor.ts:851/:875) inside a branch otherwise generic over RETIRED_HARNESSES; the console text and issue: already interpolate ${role.name}. Two-token fix keeps the next retirement correct.
  • siblingRegistrationIsLive retirement path logs nothing, so the subsequent prune line misattributes the cause as "no live terminal and no resumable session." A one-line log() closes the diagnosability gap.

Follow-ups (not this PR)

Doctor doesn't flag an unknown explicit harness name; .claude/skills/codev/SKILL.md (both trees) still lists "Gemini" as a checked CLI (stale since the #778 agy migration); afx reset on a pre-existing gemini builder now says "cannot determine harness" (accepted in the spec's Assumptions).


Merge is held pending the two blocking items above, human validation, and an upstream maintainer's approval. Not self-merging.

Architect integration review

mohidmakhdoomi and others added 9 commits August 3, 2026 22:29
…ease changelog)

The upstream release CHANGELOG.md is maintained by the maintainers, not
contributors, so this PR should not add to it. The Spec 1338 retirement is
documented via the README + the runtime/`doctor` retirement messaging instead.
Reverts the drafted [Unreleased] "### Removed (Spec 1338)" block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onal spawn preflight)

The retirement preflight was gated `if (mode !== 'shell')`, but `spawnShell`
still runs `commands.builder` via `startShellSession` (a PTY) and persists a
shell row via `upsertBuilder`. A retired gemini `shell.builder` would therefore
launch the retired CLI and leave a shell row behind. Make the preflight
unconditional; `getBuilderHarness` still honors the custom-harness escape hatch,
so behavior is unchanged for every supported harness and every mode.

Regression test drives the real `spawn({ shell: true })` against a real temp
workspace and asserts it rejects with the retirement and leaves no PTY / row /
on-disk state — the guard against re-introducing the shell exemption.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…om-harness recommendation

The custom-harness clause in the retirement recommendations hard-coded "gemini",
while the console and structured-issue lines two lines up already interpolate
`role.name`. Interpolate `architect.name` / `builder.name` so the advice stays
correct if a second harness is ever retired (RETIRED_HARNESSES is extensible).
Byte-identical output for the only current retired harness (gemini), so existing
assertions still pass; new assertions lock the interpolated clause.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n pruning a sibling registration

`siblingRegistrationIsLive` returned `false` silently for a retired architect
harness, so the reconcile loop's prune log ("no live terminal and no resumable
session") misattributed why the row went away. Thread an optional `log` through
the opts (idiomatic — tower-utils uses injected loggers, not a module logger)
and emit the retirement-specific reason in the catch; the reconcile caller passes
its logger. Tests assert the line fires on retirement and does not fire for a
live (supported) sibling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s + scrub CHANGELOG claims

Review doc: a new Deviations bullet records the four integration-review changes
(CHANGELOG revert, unconditional --shell preflight, doctor role.name
interpolation, sibling-prune retirement log), and the stale CHANGELOG claims in
the Summary / Spec Compliance / Consultation Feedback are scrubbed so nothing
implies this PR edits the release changelog. Spec/plan Status draft -> approved.
Thread updated with the full integration-review narrative.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rtifacts (audit trail)

Codex PR review (iter1) asked for the five untracked `*-context.md` consultation
context files to be committed, consistent with prior SPIR projects — 11 such files
are already tracked across the repo (e.g. 0104-custom-session-manager), alongside
the `*-rebuttals.md` files this project already commits. Adds the audit artifacts
for the phase 2–4 re-iterations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… interpolation-test comment

Two non-blocking Claude PR-review notes:
- `assertBuilderHarnessNotRetired` swallowed non-retirement resolution errors
  silently. Worktree modes re-surface them at the real `getBuilderHarness` call,
  but shell mode never resolves a harness downstream — so the now-unconditional
  preflight is the one place such an error is seen. Add a `console.debug` so a
  misconfigured `builderHarness` stays diagnosable; it fires only on an actual
  error, so the normal supported-harness path is unaffected.
- The doctor `${role.name}` interpolation assertion reads identically under the
  old hard-coded literal (only one `RETIRED_HARNESSES` entry), so its comment
  overstated what it proves. Reworded (builder + architect) to say it locks the
  rendered clause; the interpolation is verified by inspection and the already-
  asserted console/issue lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ups + iter1 rebuttal

Addresses Codex PR review (iter1): refreshes stale Key Metrics (commits 47 -> ~60,
tests 4145 -> 4148), the Flaky-Tests count, and the Consultation-Summary counts
(33/11/7 -> 36/12/8). Adds two Follow-up Items from Claude's non-blocking review
(resolveHarness prototype-chain lookups; stale Gemini-CLI mentions in INSTALL.md
and SKILL.md, pre-existing since #778). Adds the iteration-1 review rebuttal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mohidmakhdoomi

Copy link
Copy Markdown
Collaborator Author

Architect Integration Review — Update: all items resolved ✅

Follow-up to my integration review above. Every blocking and recommended item is addressed, and I re-verified each against the diff (not just the builder's summary).

Blocking — resolved

# Item Resolution Verified
1 afx spawn --shell bypassed the retirement Preflight made unconditional (spawn.ts; the mode !== 'shell' exemption + its wrong "no builder harness" comment removed) + a regression test asserting a rejected gemini --shell creates no PTY and no DB row (507c4074) git diff shows preflight at spawn.ts:934 with no mode guard; new test in spawn-retirement.test.ts
2 Revert CHANGELOG.md (contributor scope) ### Removed (Spec 1338…) block dropped (5da41839); stale CHANGELOG claims scrubbed from the PR body + review doc git diff main...HEAD -- CHANGELOG.md is empty

Recommended — folded in

  • doctor recommendation: no longer hard-codes "gemini" — interpolates ${role.name}, so the branch stays correct for any future RETIRED_HARNESSES entry (b97d96a6).
  • siblingRegistrationIsLive retirement path now logs its reason — the prune line no longer misattributes the cause (a2b8b90a).

Re-review on the fixed code (fresh 3-way)

Reviewer Verdict
Gemini APPROVE
Claude APPROVE / HIGH — independently reproduced tsc --noEmit clean + full suite 4148/0, and confirmed all four fixes in code
Codex REQUEST_CHANGES — audit-record only ("No production correctness or security blockers found"): commit the 5 consultation *-context.md artifacts, and refresh stale review-doc metrics. Both resolved (f29c1cc5, c36f2df3).

State

  • Full unit suite 4148 pass / 48 skip / 0 fail; tsc clean; working tree clean.
  • Both resolver footguns closed; all four architect boundaries handled; spawn preflight (now incl. --shell) prevents orphaned state; doctor flags both roles with the custom-harness escape hatch preserved; the agy / consult -m gemini lane is untouched.

From the contributing architect's side this is ready for maintainer review and merge. We are not upstream maintainers, so we are not self-merging — the GitHub merge is a cluesmith/codev maintainer's call.

Architect integration review (update)

@waleedkadous

Copy link
Copy Markdown
Contributor

Architect Integration Review

Excellent work — this is a model retirement: fail-closed at every boundary with zero orphaned state, a principled escape hatch (explicit custom name wins; auto-detect never consults customs), and the FreshLaunch { stop: true } contract is exactly the right answer to "next() can't change the command". I read the resolver, spawn preflight, addArchitect, and session-manager seams line-by-line and they all hold. Ran a 2-way CMAP (codex + claude; the agy lane is down for structured reviews): claude APPROVE, codex REQUEST_CHANGES with one finding I verified against the source.

I'd like to see one change before merge:

  • Reconnect leaves the PTY's restart flag inconsistent with SessionManager. buildArchitectReconnectRestartOptions() correctly returns undefined for a retired harness, but both reconnect paths in tower-terminals.ts (ptySession.restartOnExit = true at ~808 and ~1019, pre-existing code) force the flag for every architect unconditionally. When a retired-harness architect's process exits, the pane reports it's restarting and holds clients through the restart wait, though no restart can occur. Suggested: set the PTY flag only when restartOptions actually exists, and add a test at the consumer path (not just the helper's undefined return).

Optional, take or file as follow-ups (from the claude lane — all non-blocking):

  • siblingRegistrationIsLive returning false makes the reconcile loop delete the sibling row; returning true routes to addArchitect, which now fails closed anyway — same safety, but the user's config row survives for when they fix their harness. One-liner if you agree.
  • console.debug in agent-farm/utils/config.tslogger, since Tower imports that module.
  • Pre-existing: BUILTIN_HARNESSES[name] could take the same hasOwnProperty guard you gave RETIRED_HARNESSES.

The migration story (doctor → runtime message → README, all naming the same explicit selector) is coherent across all three surfaces, and thank you for the disciplined contributor-side CMAP — it made this review fast.

mohidmakhdoomi and others added 5 commits August 5, 2026 12:16
…n restartOptions

Upstream maintainer review (PR #1342, BLOCKING): both reconnect paths in
tower-terminals.ts forced `ptySession.restartOnExit = true` for EVERY architect.
A retired-harness architect resolves `restartOptions` to `undefined`
(buildArchitectReconnectRestartOptions fails closed), so `reconnectSession` was
told NOT to configure an auto-restart — SessionManager mirrors this with
`restartOnExit: hasRestart` (session-manager.ts:577). Forcing the PTY flag true
anyway made PtySession hold WebSocket clients in a "restarting…" wait
(startRestartWait) for a process that can never come back.

Gate both sites on `dbSession.type === 'architect' && restartOptions` — the
startup reconcile path (restartOptions added to the probeResults destructure)
and the on-the-fly getTerminalsForWorkspace path (restartOptions already in
scope). The PTY flag now tracks whether a restart is actually configured.

Test (consumer path, per the maintainer's explicit ask): new
tower-terminals-restart-gating.test.ts mocks buildArchitectReconnectRestartOptions
and drives BOTH real reconnect paths x {undefined -> false, defined -> true}.
Mocking the helper isolates the consumer's gating branch; the helper's real
undefined-return is separately covered in tower-utils.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… row instead of pruning it

Upstream maintainer review (PR #1342, optional). siblingRegistrationIsLive
returned `false` for a retired architect harness, which makes the reconcile loop
DELETE the user's sibling config row (setArchitectByName null). Return `true`
instead: it routes to addArchitect, which fails closed on the same retirement
(tower-instances.ts:1094 — "no state created before this point") and LEAVES the
row intact, so the registration survives for when the user repairs the harness
(fixes it, or wires a custom "gemini"). Same launch safety, non-destructive to
user config.

Update the docstring + the diagnostic INFO (now explains the row is KEPT, not
pruned). Adjust the two tower-utils.test.ts retirement tests (returns-false ->
true; log "pruning" -> "keeping the row") and correct a now-stale
"retired-harness prune" comment in tower-instances.test.ts (its reconcile tests
mock the predicate, so the false->prune / true->respawn contract is unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ght log through logger.debug

Upstream maintainer review (PR #1342, optional). assertBuilderHarnessNotRetired's
catch logged a non-retirement resolution error via `console.debug`. Tower imports
this module, and `console.debug` always writes to stdout — polluting Tower's log
stream. Route it through `logger.debug` (agent-farm/utils/logger), which is
DEBUG-gated, so the deferred-error line stays silent in Tower unless DEBUG is set
while remaining diagnosable when needed.

Strengthen the config.test.ts "defers on unknown harness" test to spy
logger.debug and assert the deferred error is logged there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…N_HARNESSES lookup

Upstream maintainer review (PR #1342, optional). `BUILTIN_HARNESSES[name]` was
indexed with a user-controlled key at two sites — resolveHarness (config
shell.builderHarness / shell.architectHarness) and harnessProviderFor (a running
builder's launch-script harness name). For an inherited Object key like
"constructor" the bare index returns a truthy value (Object's constructor), which
`if (builtin) return builtin` would hand back as a bogus provider that TypeErrors
at the first buildRoleInjection call.

Add getBuiltinHarness(name) — an own-property accessor mirroring the guard
isRetiredHarness already gives RETIRED_HARNESSES — and use it at both sites. The
constrained auto-detect lookup (resolveHarness:466, `detected` in
{claude,codex,opencode}) stays a direct index; only user-controlled keys need the
guard.

Tests: resolveHarness rejects inherited Object keys with "Unknown harness"
(never a bogus provider); a getBuiltinHarness describe covers real names, unknown
names, and the proto-key footgun.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(1 blocking + 3 optional)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mohidmakhdoomi

mohidmakhdoomi commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

@waleedkadous thanks for the careful review. Pushed the blocking fix + all three optional items as four atomic commits (7bd4267f..050335b8). Each summarized below, verified against the pushed diff.

BLOCKING — reconnect restartOnExit inconsistent with SessionManager (d6a0554d)

Both reconnect paths in tower-terminals.ts forced ptySession.restartOnExit = true for every architect. For a retired-harness architect, buildArchitectReconnectRestartOptions() returns undefined, so reconnectSession was told not to configure an auto-restart — and SessionManager already reflects that with restartOnExit: hasRestart (session-manager.ts:577). Forcing the PTY flag true anyway made PtySession hold clients in a "restarting…" wait for a process that can never come back.

Fix: gate both sites on dbSession.type === 'architect' && restartOptions:

  • startup reconcile (_reconcileTerminalSessionsInner): added restartOptions to the probeResults destructure so it's in scope at the assignment.
  • on-the-fly (getTerminalsForWorkspace): restartOptions was already in local scope.

The PTY flag now tracks whether a restart is actually configured, matching hasRestart.

Consumer-path test (per your ask — not just the helper's undefined-return unit test): new tower-terminals-restart-gating.test.ts drives both real reconnect paths (reconcileTerminalSessions + getTerminalsForWorkspace) end-to-end with a live shellper client, asserting the resulting PtySession.restartOnExit is false when restartOptions is undefined and true when it's defined. buildArchitectReconnectRestartOptions is mocked so its return is the controlled input — this exercises the consumer's gating branch directly. (An earlier real-config version tripped a Vitest module-identity artifact: under that file's vi.mock graph the helper's err instanceof RetiredHarnessError check and config.ts's throw bound different harness.js instances, so the check missed and fell to the generic fallback. Not a production issue — the single-graph chain is already covered by the retirement tests in tower-utils.test.ts.)

Optional 1 — siblingRegistrationIsLive returns true on retirement (37277cfb)

Returning false made the reconcile loop DELETE the user's sibling config row (setArchitectByName(..., null)). Now returns true, which routes to addArchitect — which fails closed on the same retirement (tower-instances.ts:1094, before any state is created) and leaves the row intact so it survives a harness fix. Docstring + the diagnostic INFO updated (now "keeping the row", not "pruning"). Adjusted the two tower-utils.test.ts retirement tests (false → true; log-message assertion). The tower-instances.ts reconcile tests mock this predicate, so the false→prune / true→respawn contract they assert is unchanged — I only corrected a now-stale "retired-harness prune" comment there.

Optional 2 — console.debuglogger (24bcb252)

assertBuilderHarnessNotRetired's deferred-error branch now uses logger.debug (agent-farm utils/logger) instead of console.debug. Since Tower imports this module, the bare console.debug always hit Tower's stdout; logger.debug is DEBUG-gated, so it stays quiet in normal Tower operation and prints under DEBUG. Strengthened the config.test.ts "defers on unknown harness" test to spy logger.debug.

Optional 3 — own-property guard on BUILTIN_HARNESSES[name] (bea86496)

Added getBuiltinHarness(name) — an own-property accessor mirroring the guard isRetiredHarness gives RETIRED_HARNESSES — and applied it at both user-controlled lookup sites: resolveHarness (config shell.*Harness) and, for consistency, harnessProviderFor in reset/context.ts (a running builder's launch-script harness name) which had the identical footgun. Inherited keys like constructor/toString now resolve to undefined instead of a truthy Object member masquerading as a provider. The constrained auto-detect lookup (resolveHarness, detected ∈ {claude,codex,opencode}) stays a direct index. Tests: resolveHarness rejects inherited Object keys with "Unknown harness"; a getBuiltinHarness block covers real names, unknown names, and the proto-key cases.

Verification

pnpm build clean (tsc + dashboard + skeleton). Full unit suite: 4156 passed / 48 skipped / 0 failed (+8 new tests). Not self-merging — leaving the GitHub merge to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Retire Gemini CLI as a builder harness

2 participants