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, andShared file(formerlyCo-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
SessionStarthook in.codex/hooks.json— the portableworktree ensureanalogue, fired in the bare CLI. Handled like any other provider hook (aHooksIntegration+ a seed template; ADR 0071). .codex/environments/environment.tomlwith[setup]/[cleanup]scriptkeys — a real create/teardown pair, but with two properties that make it unlike every integration file discern had wired before:- 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]]). - 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'sWorktreeRemovehandsworktree remove.
- It is auto-generated by the Codex app (a "DO-NOT-EDIT" file the app rewrites), carrying the app's own keys (
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.
- A
WorktreeAppIntegrationon the provider record (src/lib/providers.ts), optional likehooks?/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).
- It re-emits on every refresh, with ownership-aware script writes.
wireProviderWorktreeAppruns insidecompileGuidelinesright after the MCP wiring — the same timing aswireProviderMcp(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 twoscriptkeys 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.
[cleanup]reuses the existing cwd-basedworktree teardownverb. That verb already resolves the worktree from the process cwd (it walks up todiscern.tomland runsworktreeTeardownagainst 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 theenvironment.tomlline that names it. (worktree remove, the ClaudeWorktreeRemoveentry 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 + theSessionStarthook. 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'sworktree removeconverge on the oneworktreeTeardown(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 noenvironment.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
.codexpath lives insrc/lib/providers.tsand is reached through the registry;compileGuidelinescallswireProviderWorktreeApp(root, agents)and constructs no Codex path itself, so thetests/agent_agnostic_test.tsguard holds. - The contract is regression-proofed. A test runs the literal
[cleanup].scriptdiscern 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
worktreeAppdeclaration + aregisterfunction, no plumbing edit — the ADR 0031 payoff, extended to this seam.
Alternatives considered
§- Seed
environment.tomlonce (write-once, like.codex/hooks.json). Rejected: the app regenerates the file, so a one-shot seed is clobbered and never reapplied — the twoscriptkeys would silently vanish. Re-emitting on refresh is the only shape that survives the app owning the file. - Unconditionally overwrite the two
scriptkeys 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(orworktree remove-style) verb for[cleanup]. Rejected:worktree teardownalready 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 twoscriptkeys co-exist with the app. - A required
worktreeAppfield on every provider. Rejected: only Codex has such a file; a required field would forcenone-like declarations on agents that will never have one — the optional-capability shape (hooks?,skillsDir?) is the fit.