Skip to content

ADR 0073: discern co-manages Codex's auto-generated environment.toml, and reuses the cwd-based teardown verb for its cleanup

Amendments.

  • Vocabulary: current spellings are discern, the gate, or the bar for the retired product-category wording, and Shared file (formerly Co-managed seed / co-managed file); the decisions below are unchanged.

Status: accepted; extends ADR 0031 (one typed provider registry) with an optional worktree-app seam, and builds on ADR 0040 (the worktree verbs parse their own input) and ADR 0045 (integration artifacts are re-established on every refresh, not seeded once)

Context

§

Phase B wires Codex's worktree lifecycle. Codex exposes two committable surfaces a development system can drive, and they behave unlike Claude Code's WorktreeCreate / WorktreeRemove contract:

  • A per-session SessionStart hook in .codex/hooks.json — the portable worktree ensure analogue, fired in the bare CLI. Handled like any other provider hook (a HooksIntegration + a seed template; ADR 0071).
  • .codex/environments/environment.toml with [setup]/[cleanup] script keys — a real create/teardown pair, but with two properties that make it unlike every integration file discern had wired before:
    1. It is auto-generated by the Codex app (a "DO-NOT-EDIT" file the app rewrites), carrying the app's own keys (version, name, [[actions]]).
    2. Its scripts run only for worktrees the Codex app creates (under $CODEX_HOME/worktrees), as bare commands in the worktree cwd with no stdin payload — not for discern's own sibling worktrees, and not with the {worktree_path} JSON that Claude's WorktreeRemove hands worktree remove.

Two questions fell out: how should discern write into a file the app owns and regenerates, and what runs as [cleanup] when there is no stdin to carry the worktree path?

Decision

§

discern co-manages environment.toml through a new optional provider seam, and points [cleanup] at the pre-existing cwd-based worktree teardown verb — no new verb is introduced.

  1. A WorktreeAppIntegration on the provider record (src/lib/providers.ts), optional like hooks?/skillsDir?:
   interface WorktreeAppIntegration {
     readonly configFile: string;
     register(root: string): Promise<string[]>; // files written, idempotently
   }

Only Codex declares one. registerCodexEnvironment merges [setup].script = "discern worktree ensure" and [cleanup].script = "discern worktree teardown" via the comment-preserving TomlEditor, rewriting a script key only when it is absent or still set to discern's own default. A user-customized script value is preserved, while the app's [[actions]] and comments survive untouched. Codex's schema additionally REQUIRES top-level version (number) and name (string) — a file missing them is rejected with expected string, received undefined at name — so discern seeds version = 1 and name = "Discern" set-if-absent: a file discern writes from scratch validates (giving immediate Codex-environments access), while a file the app already created keeps its own version/name. Writing those root-level keys is why TomlEditor gained hasRootKey / setRootLiteral (the discern.toml subset has no pre-section keys; a foreign Shared file does). It is safe when the file is absent (created).

  1. It re-emits on every refresh, with ownership-aware script writes. wireProviderWorktreeApp runs inside compileGuidelines right after the MCP wiring — the same timing as wireProviderMcp (ADR 0045). Because the app may regenerate the file at any time, a seed (write-once, then the user's) would be silently clobbered and never reapplied; re-emitting self-heals the two script keys whenever the app rewrites the file and drops them. The self-heal does not imply permanent ownership over the user's commands: once a script value differs from discern's default, refresh treats it as user-owned and leaves it alone. A shared idempotent-TOML core writes back only when the bytes change, so a refresh that finds both scripts present or customized is a clean no-op.
  1. [cleanup] reuses the existing cwd-based worktree teardown verb. That verb already resolves the worktree from the process cwd (it walks up to discern.toml and runs worktreeTeardown against that root) and needs no stdin — exactly the bare-command-in-cwd shape the Codex app invokes [cleanup] with. It has existed since the engine's first dispatcher; Phase B adds no teardown verb, only the environment.toml line that names it. (worktree remove, the Claude WorktreeRemove entry point, stays distinct: it reads a {worktree_path} stdin payload, which [cleanup] does not provide.)

The explicit nos:

  • discern does not build its own sibling worktrees around environment.toml. That file serves only the Codex-app-managed-worktree workflow; discern's own worktrees stay driven by its CLI/MCP verbs + the SessionStart hook. discern writes the file so the app-managed flow works, and stops there.
  • No new teardown verb, and no second teardown core. [cleanup] and Claude's worktree remove converge on the one worktreeTeardown(lifecycleContext(...)) core; only the worktree-resolution differs (cwd vs. payload).
  • The seam is not forced on every provider. Unlike mcp/trust (required), worktreeApp? is optional: an agent whose app owns no such file declares none and is skipped, never guessed — Cursor/Copilot/Antigravity (Plans C/D) have no environment.toml, so a required field would be speculative generality.

Consequences

§
  • A developer on Codex-app-managed worktrees gets discern setup/teardown via environment.toml, kept in step with the app's regenerations, while the app's own config and user-customized lifecycle scripts are preserved.
  • The engine stays agent-agnostic. Every .codex path lives in src/lib/providers.ts and is reached through the registry; compileGuidelines calls wireProviderWorktreeApp(root, agents) and constructs no Codex path itself, so the tests/agent_agnostic_test.ts guard holds.
  • The contract is regression-proofed. A test runs the literal [cleanup].script discern writes as a bare command in the worktree cwd and asserts it tears the worktree down — so renaming the verb (or the written script) without updating both red-lights the gate rather than silently breaking Codex teardown.
  • One more registry-driven knob. Adding a future agent with an app-managed lifecycle file is a worktreeApp declaration + a register function, no plumbing edit — the ADR 0031 payoff, extended to this seam.

Alternatives considered

§
  • Seed environment.toml once (write-once, like .codex/hooks.json). Rejected: the app regenerates the file, so a one-shot seed is clobbered and never reapplied — the two script keys would silently vanish. Re-emitting on refresh is the only shape that survives the app owning the file.
  • Unconditionally overwrite the two script keys on every refresh. Rejected after the launch review: it self-heals app regeneration, but it also destroys a user's deliberate environment setup/cleanup commands. Ownership-aware writes keep the self-heal for absent or discern-default values without treating user customization as drift.
  • A new worktree cleanup (or worktree remove-style) verb for [cleanup]. Rejected: worktree teardown already does cwd-based, no-stdin teardown through the shared core; a second verb would duplicate it and split the teardown logic the gate must keep coherent. (The Phase B brief assumed a new verb was needed; the live code already had one — trust the code over the brief.)
  • Overwrite the whole environment.toml (treat it as discern-owned). Rejected: it is the app's file with the app's keys; a wholesale rewrite would drop [[actions]] and fight the app on every regeneration. Surgical, comment-preserving edits of only the two script keys co-exist with the app.
  • A required worktreeApp field on every provider. Rejected: only Codex has such a file; a required field would force none-like declarations on agents that will never have one — the optional-capability shape (hooks?, skillsDir?) is the fit.
choose openEsc close