Context and Problem Statement
spec-kitty dispatch opens bounded, doctrine-governed agent actions that run
immediately — no spec, no plan, no work
packages, no lane worktree — yet they still load governance context, route to an
agent profile, and produce a durable record. The implementation lives in
src/specify_cli/invocation/ and already emits OpStartedEvent /
OpCompletedEvent (see src/specify_cli/invocation/__init__.py), but the
concept these commands express has never been named or ratified at the
architecture level. #1688 (the proposal, now closed/superseded into this ADR)
diagnosed the gap precisely: the records were treated as scratch byproducts of
"governance machinery" rather than as instances of a real unit of work.
Two epics now press on the same gap from different directions:
- #1804 (Ops execution layer) wants a durable, governed, queue-backed
execution artifact for standalone dispatch. It has no ADR — it is the highest-value
architecture gap in
work/EPIC_ARCHITECTURE_CORRELATION.md. - #1802 (pre/post-mission lifecycle) wants bounded, governed flows that sit outside a Mission's spec→merge loop: an intake flow before a Mission exists and a correction flow after a Mission has merged.
Both want the same thing: a bounded, doctrine-governed action with a durable record and no full Mission lifecycle. Left unguided, #1804 and #1802 would each mint their own primitive — two parallel abstractions for one shape. The mission's binding constraint C-005 (single source of truth — no parallel narrative/abstraction surfaces) forbids that. This ADR is the seam that prevents the divergence: it names the shared primitive once so both epics build on it rather than beside it.
The decision matters now because OpStartedEvent/OpCompletedEvent are already
in the codebase. Ratifying the concept before #1804/#1802 land code is the
cheapest moment to converge; after, we would be unifying two shipped primitives.
Decision Drivers
- C-005 — one abstraction, not two. #1804 (Ops) and #1802 (lifecycle) must share a single execution primitive.
- Screaming the tier. The tier between a full Mission and an ungoverned ad-hoc shell command must be a named, discoverable concept, not an emergent property of three sibling commands.
- Durability + governance without the full planning loop. The primitive must carry real doctrine context and a permanent trace, while staying lighter than a Mission (no spec/plan/tasks/worktree/merge).
- Coherence with the current execution shapes. Any execution-context,
commit, or status mechanics the Op touches must reference the canonical
surfaces (
mission_runtime,core/commit_guard, the append-only status event log), never the retired ones.
Considered Options
- Option A — Two primitives (status quo drift): let #1804 build an "Ops" artifact and #1802 build a separate "lifecycle flow" artifact.
- Option B — One shared primitive: the Op (chosen). Ratify the Op as the single tier between Mission and ad-hoc; pre/post-mission lifecycle flows are Ops, not a parallel concept.
- Option C — Fold everything into Mission: model intake/correction/dispatch as degenerate Missions.
Decision Outcome
Chosen option: "Option B — One shared primitive: the Op."
What an Op is
An Op is a bounded, doctrine-governed agent action, dispatched immediately,
that produces a durable governed record without a full Mission lifecycle. It is
to dispatch what a Mission is to the
spec→plan→tasks→implement→review→merge loop: lighter, immediate, no planning
loop — but real work, real doctrine, real record.
The Op lifecycle is a short dispatch loop, not a Mission state machine:
- Route — resolve the operator request to an agent profile (explicit
--profile, else the router). Ambiguous handles fail closed (no silent fallback), consistent with the identity-selector rule. - Load governance context — assemble the action-scoped doctrine payload for the resolved profile (the same charter-context surface a Mission step uses).
- Open a durable Op record — append an
OpStartedEventto the append-only audit trail (src/specify_cli/invocation/). - Agent acts on the panel — the dispatched agent does the work under the loaded governance context; the CLI does not perform the work itself.
- Close the Op — append an
OpCompletedEventwith the real outcome.
An Op carries a ModeOfWork (task_execution, mission_step, or query, per
src/specify_cli/invocation/modes.py). The Op record is the durable trace; durability is a
property of the artifact, not a bolt-on. (#1688 framed the original
two-invocation durability bug as a symptom of this missing concept; naming the Op
resolves it structurally.)
The three execution tiers
| Tier | Lifecycle | Governance | Durable record | Workspace |
|---|---|---|---|---|
| Mission | spec → plan → tasks → implement → review → merge | full doctrine + step contracts | mission artifacts + status event log | lane worktrees + mission branch |
| Op | route → context → record → act → close | action-scoped doctrine payload | OpStartedEvent / OpCompletedEvent trail |
none (acts in the current checkout) |
| ad-hoc | none | none | none | none |
The Op is the governed middle tier. The distinction from ad-hoc is exactly governance + durability: an ad-hoc shell command is ungoverned and untraced; an Op is governed and traced. The distinction from a Mission is the absence of the planning loop, work packages, and dedicated worktrees.
Pre/post-mission lifecycle flows are Ops (the C-005 unification)
The binding clause of this ADR: pre/post-mission lifecycle actions (#1802) are Ops, not a second primitive.
- Intake Op — a pre-mission flow (e.g. triaging a request into a future Mission) is an Op: bounded, governed, durable-recorded, no Mission yet.
- Correction Op — a post-merge flow (e.g. a governed follow-up fix after a Mission has merged) is an Op: bounded, governed, durable-recorded, no new Mission.
Epics #1804 (Ops) and #1802 (lifecycle) therefore consume one abstraction. Epic #1804 builds the Op artifact and its queue-backed durability; #1802's intake/correction flows are modes of dispatching an Op, not a parallel artifact type. Neither epic may mint a competing primitive — doing so reintroduces the C-005 violation this ADR exists to prevent.
Coherence with the canonical execution shapes (binding)
Where an Op touches execution mechanics, it consumes the current canonical surfaces — never the retired ones:
- Execution context. If an Op needs resolved context, it calls
resolve_action_contextfrommission_runtime(src/mission_runtime/resolution.py), which returns the immutableExecutionContextvalue object (src/mission_runtime/context.py). The retiredspecify_cli/core/execution_context.pypath and the value-object's old home are historical — see ADR2026-06-07-1-execution-state-canonical-surface.mdand the 2026-06-10 addendum to2026-06-03-2-executioncontext-owner-and-committarget.md. - Commit safety. Ops run in the operator's current checkout, so most Ops do
not commit at all. Any Op that does create a commit routes its protection
decision through the single policy seam
core.commit_guard.evaluate(src/specify_cli/core/commit_guard.py) — the one protected-branch decision (C-GUARD-1). The destination is carried as the canonicalCommitTarget(ref: str, kind: CommitTargetKind)value object (kind ∈ {PRIMARY, COORDINATION, FLATTENED}), and authorization is the asserted-at-the-surfaceGuardCapabilityparameter — never derived from message text, file content, env, or op records. An Op carries no special protected-branch privilege: its default capability isGuardCapability.STANDARD. This is the delivered(ref, kind)/GuardCapabilityshape (Strangler step 7, delivered across missions 01KTPKST + 01KTRC04), not the retired(worktree_root, destination_ref)sketch. - Status. An Op never drives the 9-lane WP status machine; the append-only Op event trail is its sole record. WP lane state remains the exclusive authority of the status event log and is untouched by Ops.
Consequences
Positive
- #1804 and #1802 build on one ratified primitive — C-005 holds; no parallel abstraction.
- The execution layer gains a named, discoverable middle tier; the "missing concept" #1688 identified is closed.
- Op durability is structural (the artifact owns its trail), so the original two-invocation durability failures cannot recur as one-off patches.
- The Op's mechanics are pinned to the canonical execution surfaces, so the
layer cannot drift back onto the retired
execution_context.py/(worktree_root, destination_ref)shapes.
Negative
OpStartedEvent/OpCompletedEventand theinvocation/package predate this ratification; #1804 must reconcile the shipped event/record shapes with the ratified concept (rename/relocate work is implementation scope, tracked under #1804, not this ADR).
Neutral
- This ADR ratifies the concept and boundaries; the durable queue-backed storage mechanics (#1804) and the intake/correction dispatch surfaces (#1802) are designed in their own implementation specs, governed by this seam.
Confirmation
The decision is confirmed when #1804 and #1802 both reference this ADR as their
architecture anchor and neither introduces a competing execution primitive — i.e.
work/EPIC_ARCHITECTURE_CORRELATION.md shows the #1804 Ops gap closed (mission
success criterion SC-2) and #1802 listed against this ADR. Confidence: high —
the Op shape is already partially expressed in shipped code (OpStartedEvent /
OpCompletedEvent, ModeOfWork), so this ratifies an emergent reality rather
than proposing a speculative one.
Pros and Cons of the Options
Option A — Two primitives (status-quo drift)
Let #1804 and #1802 each define their own artifact.
Pros:
- No coordination needed between the two epics up front.
Cons:
- Direct C-005 violation: two abstractions for one shape.
- Guarantees future consolidation debt (unifying two shipped primitives).
- The execution-layer concept stays unnamed and undiscoverable.
Option B — One shared primitive: the Op (chosen)
Ratify the Op as the single Mission ⟷ ad-hoc tier; lifecycle flows are Ops.
Pros:
- Satisfies C-005 — one primitive, two consumers.
- Names a real, already-emergent concept; closes the #1688 gap structurally.
- Pins Op mechanics to canonical execution surfaces.
Cons:
- Requires #1804 to reconcile pre-existing
invocation/shapes with the ratified concept.
Option C — Fold everything into Mission
Model intake/correction/dispatch as degenerate Missions.
Pros:
- Reuses the existing Mission machinery.
Cons:
- Forces the full planning loop (spec/plan/tasks/worktree/merge) onto bounded immediate actions — the exact weight the Op tier exists to avoid.
- Pollutes the Mission state machine and status authority with non-Mission work.
Adjudications (WP06 — escalated from WP02's approved review)
Two cross-cutting items in the architecture surface were escalated for the
architect to adjudicate. They are recorded here because they belong to the
architecture/3.x/adr/** authority this WP owns.
Adjudication 1 — Charter authority-path modernization: deferred
Finding. The charter's default authority pointer for architectural intent
(charter.context_renderers.authority_paths.DEFAULT_AUTHORITY_PATHS) still cites
architecture/2.x/adr/, while architecture/3.x/adr/ is the canonical track.
Decision: defer the flip, with a sanctioned recovery path. Rationale:
- Not broken today. WP02's living layout converted the post-cutover 2.x ADR
paths into back-compat symlinks into
3.x/, and the renderer is existence-gated. Soarchitecture/2.x/adr/still resolves; the pointer is a back-compat alias, not a dangling link. The modernization is a clarity improvement, not a correctness fix. - The true blast radius exceeds the sanctioned chain. The flip's enumerated
chain was:
authority_paths.py→ the two software-dev SOURCE prompt templates → regenerated agent copies → two contract tests (test_template_governance_payload_contract.py,test_wp_prompt_governance_contract.py) → the twelve-agent parity baselines. Investigation found three additional surfaces outside that chain that assert the2.x/adr/default and would break on a flip:tests/charter/test_context_authority_paths.py,tests/charter/test_sync_authority_paths.py, andtests/charter/test_schemas_additive_fields.py. It also requires re-coordinating.kittify/charter/charter.md— a file WP02 owns and which WP02 deliberately left annotated asarchitecture/2.x/adr/ # 2.x-era architectural decisions (historical)alongside a separate activearchitecture/adrs/entry. Flipping the default unilaterally from WP06 would reach across the WP02 ownership boundary and beyond the enumerated chain. - Per the WP06 mandate, when the flip requires surfaces beyond the enumerated chain, the sanctioned action is to record a reasoned deferral here rather than improvise a partial, test-breaking flip.
Recovery path (when undertaken). Do all links together, in one change:
flip DEFAULT_AUTHORITY_PATHS and its docstrings → update both software-dev
SOURCE prompt templates (implement/prompt.md, review/prompt.md) → regenerate
agent copies via the documented upgrade flow → update the two governance-contract
tests → update the three tests/charter/ assertions → re-coordinate
.kittify/charter/charter.md with the owning WP → regenerate the twelve-agent
parity baselines (PYTEST_UPDATE_SNAPSHOTS=1 pytest tests/specify_cli/regression/)
and commit the baselines with the template change → confirm
pytest tests/architectural/ green. This is a coherent follow-up Op/mission, not
a WP06 side effect.
Addendum — 2026-06-12 — EXECUTED (WP07, mission name-vs-authority-remediation-01KTYGTE):
The deferred flip was executed in full. All seven chain links landed atomically:
src/charter/context_renderers/authority_paths.pyDEFAULT_AUTHORITY_PATHSflippedarchitecture/2.x/adr/→architecture/3.x/adr/; module docstring and dict docstring updated.- Both source prompts updated:
src/doctrine/missions/mission-steps/software-dev/implement/prompt.mdandreview/prompt.md. - Two governance-contract tests updated:
tests/architectural/test_template_governance_payload_contract.py(fixture directory + two path assertions) andtests/specify_cli/next/test_wp_prompt_governance_contract.py(inline charter string,adr_path_presentcheck, self-sufficiency regex extended to[23]\.x). - Three
tests/charter/assertions updated:test_context_authority_paths.py,test_sync_authority_paths.py,test_schemas_additive_fields.py. .kittify/charter/charter.mdline 317 annotation updated (architecture/3.x/adr/ # canonical architectural decisions (3.x era)).- Twelve-agent parity baselines regenerated via
PYTEST_UPDATE_SNAPSHOTS=1 pytest tests/specify_cli/regression/— 26 baseline files updated (13 agents × implement + review); no other baseline churn observed. pytest tests/architectural/ -qfully green post-flip.
No 2.x-pointing authority default remains in any active code path.
Adjudication 2 — Duplicate doctrine-layer-merge-semantics ADR: 3.x is canonical (already resolved by WP02)
Finding. 2026-05-16-1-doctrine-layer-merge-semantics.md existed in both
2.x/adr/ and 3.x/adr/; docs/explanation/org-doctrine-layer.md linked the
2.x copy.
Decision: architecture/3.x/adr/ is canonical for this ADR; the 2.x path is a
back-compat pointer. This is consistent with the era rule WP02 recorded in
architecture/3.x/adr/README.md ("This folder is canonical for 3.x decisions.
Back-compat symlinks at the old architecture/2.x/adr/<filename> paths point
here"). WP02 already converted the 2.x copy into a symlink into 3.x/, so
the duplication is structurally resolved — there is one canonical file. The only
residual action is editorial: the stale 2.x link in
docs/explanation/org-doctrine-layer.md is repointed at the canonical 3.x path
(done as part of this WP, with a one-line out-of-map rationale).
More Information
- Mission spec (FR-007):
kitty-specs/doctrine-glossary-architecture-consolidation-01KTNWFC/spec.md - Proposal (superseded into this ADR): #1688
- Consuming epics: #1804 (Ops), #1802 (lifecycle), #1810 (dispatch collapse)
- Current Op surface in code:
src/specify_cli/invocation/(OpStartedEvent/OpCompletedEvent,ModeOfWork),src/specify_cli/cli/commands/{dispatch,profile_invocation,invocations_cmd}.py - Canonical execution surfaces: ADR
2026-06-07-1-execution-state-canonical-surface.md(mission_runtime); ADR2026-06-03-2-executioncontext-owner-and-committarget.md+ its 2026-06-10 addendum (CommitTarget(ref, kind), step 7 delivered);src/specify_cli/core/commit_guard.py(GuardCapability, singleevaluate) - Correlation matrix:
work/EPIC_ARCHITECTURE_CORRELATION.md(Ops gap → SC-2)