Context
ADR 2026-06-03-1-execution-state-domain-model.md
ratified the bounded-module decomposition and named Execution / Runtime as
the owner of workspace resolution, branch state, and CWD-invariant context. It
also recorded that the residue surfaces (~40 command surfaces re-deriving
execution context) must route through resolve_action_context
(core/execution_context.py).
What that ADR did not lock in was the physical home of the execution-state
domain. Today resolve_action_context and its ActionContext value object live
in src/specify_cli/core/execution_context.py, buried inside the CLI package.
That placement makes the domain invisible at the package level: there is no
top-level module name that screams "this is where execution state lives", and
nothing prevents new surfaces from re-deriving their own context rather than
importing the canonical one.
The design analysis in
docs/plans/engineering-notes/runtime_and_state_overhaul/06-proposed-domains-and-splits.md
§4/§5 resolves this. §4 records the operator decision (Stijn, 2026-06-03) to
create a net-new top-level mission_runtime/ umbrella package, and §5 fixes the
owner shape (Strangler façade → operation service; value-object resolver as
fallback).
Per C-006, this ADR is written before any mass code lands. The umbrella is
stood up empty-but-registered in this work package (WP02); the relocation of the
hardened ExecutionContext logic into it is WP03.
Decision
1. Module name: top-level mission_runtime/
The canonical execution-state surface is a net-new top-level package
src/mission_runtime/. Rationale (doc 06 §4):
- Screaming Architecture — the package structure should name the domain. A
top-level
mission_runtime/makes the execution-state domain discoverable at the highest level of the source tree, rather than hiding it underspecify_cli/core/. - Strangler Fig — the new home grows alongside the old. The umbrella is the
new canonical surface; existing call-sites are strangled onto it
incrementally, and the old
core/execution_context.pybecomes a thin re-export shim removed once unreferenced (FR-003).
mission_runtime is preferred over harden-in-place (keeping the logic in
core/) for domain clarity. The trade-off — a new top-level package must be
registered in the layer meta-guard — is accepted and discharged below.
2. Layer placement: charter-level sibling of runtime
mission_runtime is registered in _DEFINED_LAYERS in both
tests/architectural/test_layer_rules.py and the landscape fixture in
tests/architectural/conftest.py. Omitting either causes
test_no_unregistered_src_packages to fail (doc 06 §4).
It sits at the charter level, consistent with runtime and glossary. The
spine remains kernel ← doctrine ← charter ← specify_cli; runtime,
glossary, and now mission_runtime are siblings above charter.
mission_runtime may import specify_cli.* (it delegates to today's resolver
during migration) but must not import CLI presentation layers
(specify_cli.cli / specify_cli.next), mirroring the runtime boundary.
3. Lean public API expressed over context objects
The package root exposes a minimal __all__ (the __all__ Declaration
Convention, C-007):
__all__ = ["ExecutionContext", "resolve_action_context", "ActionContextError"]
ExecutionContext— the immutable, complete resolved context (read/write/dest dirs, branch, WP identity, prompt). The API is expressed over this context object, never over path fragments. Consumers receive a resolved context; they do not reconstructmain_repo_root / "kitty-specs" / mission_slugthemselves (FR-009).resolve_action_context(repo_root, mission, wp_id=None, *, mode=None) -> ExecutionContext— the single resolution entry point. CWD-invariant, topology-aware, mode-correcttarget_branch, and raisesActionContextErroron unresolvable context with no silent fallback.ActionContextError— the only error type consumers catch.
Everything else is internal. The implementation lives in submodules
(mission_runtime/context.py, mission_runtime/resolution.py) that consumers
must not import directly — enforced by
tests/architectural/test_mission_runtime_surface.py (FR-005). This keeps the
surface lean and prevents the internal-leakage that let the old core/ resolver
sprawl.
4. Stage-C shape only
This umbrella adopts the Stage-C shape from doc 06 §5: a Strangler façade
(resolve_action_context) returning an immutable ExecutionContext value
object, delegating to today's resolver during migration (option C → A). The
Stage-B operation service / CommitTarget (commit-seam atomicity) is out of
scope (C-008) — doc 06 §6 step 7 confirms safe_commit already enforces the
commit invariant, so CommitTarget is an ergonomic improvement deferred to
later work, not a safety gate this surface must provide.
5. Strangler migration order
- WP02 (this WP) — stand up
mission_runtime/empty-but-registered: package skeleton + lean__all__(stub symbols), layer-guard registration in both files, sole-resolver surface test, and this ADR. - WP03 — relocate the hardened
ExecutionContext/resolve_action_contextlogic fromcore/execution_context.pyinto the umbrella; wire the__all__symbols; leavecore/execution_context.pyas a thin re-export shim (NFR-001: behaviour preserved). - Downstream WPs — strangle the residue surfaces onto
mission_runtime.resolve_action_context; delete duplicated path-builders; remove thecore/execution_context.pyshim once unreferenced (FR-003).
Consequences
What changes
- A new top-level package
src/mission_runtime/exists and is enforced by the layer meta-guard. Future code that adds execution-state logic has a named home. - The lean
__all__becomes the contract; the surface test makes internal imports a build failure, so the leakage that bloated the old resolver cannot recur.
What stays the same
- Nothing relocates in this WP.
core/execution_context.pyand all current call-sites are untouched (relocation is WP03). The umbrella symbols are stubs until WP03 wires them. - Legacy missions continue to resolve through the existing resolver until they are strangled (NFR-001).
What is now explicit
- The execution-state domain has a screaming, top-level home:
mission_runtime. - The public surface is lean and expressed over context objects (originally four
symbols;
ExecutionModewas retired 2026-08-21 — see Amendments), and internal submodules are import-forbidden from outside the package. - The migration is Strangler-ordered: umbrella first (WP02), relocation second (WP03), call-site cutover and shim removal after.
Amendments
2026-08-21 —
ExecutionModeretired (missionrc3-execution-mode-consolidation-01M0GGX1, M7).mission_runtime.context.ExecutionMode(aworktree/code_changeenum) was never consumed:MissionExecutionContext.execution_modecarries a rawstr, and no code compared against the enum's members. It was removed fromcontext.py, from the package__all__, and from this ADR's public-API listing. The worktree-vs-direct execution-mode axis is owned by the externalspec_kitty_events.status.ExecutionMode(worktree/direct_repo); the retired local enum was a dead, mis-named duplicate of that axis whosecode_changetoken also collided with the ownership enum's unrelatedcode_change("WP produces code"). Retiring it removes both the dead symbol and one half of that class-name/token footgun.2026-09-28 —
OwnedCheckoutandOwnedRefusalCodeadded to the root surface (missionowned-checkout-lifecycle-authority-01M3M2ZB).mission_runtime.OwnedCheckoutis the validated ownership fact: the owned checkout, the repository root checkout, the mission's stored topology and its target branch, proven once per command. The placement seam, WP workspace resolution, the status transition pipeline, thenextruntime and the prompt builder consume it, so it lives in this package and adds no outbound edge into the CLI application layer. It is minted only byspecify_cli.core.owned_mission(architectural gate G3) and is exported on the root because MR-1/MR-2 forbid submodule imports.mission_runtime.OwnedRefusalCodeis a second new root export: aStrEnum(value == name) holding theOWNED_*refusal-code registry fromdata-model.md, exported alongsideOwnedCheckoutso code and tests import the member instead of repeating the literal (Sonar S1192).OwnedCheckoutPathRefused, the exception that subclasses the already-publicActionContextError, is deliberately not exported.tests/architectural/test_mission_runtime_surface.pypins bothOwnedCheckoutandOwnedRefusalCodein_PUBLIC_SURFACE.
References
- Mission spec:
kitty-specs/execution-state-canonical-surface-01KTG6P9/spec.md - Contract:
kitty-specs/execution-state-canonical-surface-01KTG6P9/contracts/mission_runtime_api.md - Design basis:
docs/plans/engineering-notes/runtime_and_state_overhaul/06-proposed-domains-and-splits.md§4/§5 (and §6 sequencing); doc 17 (consolidated domain model) - Prior ADR:
2026-06-03-1-execution-state-domain-model.md— bounded-module decomposition - Prior ADR:
2026-06-03-2-executioncontext-owner-and-committarget.md— ExecutionContext owner / CommitTarget rationale - Tactic:
src/doctrine/tactics/built-in/refactoring/refactoring-strangler-fig.tactic.yaml - Issue #1619: root-cause analysis and Strangler Fig sequence