Implementation Plan: Do Dispatch Open-Op Lifecycle
Branch: kitty/mission-do-dispatch-open-op-lifecycle-01KTSJ2H | Date: 2026-06-10 | Spec: spec.md Input: Feature specification from kitty-specs/do-dispatch-open-op-lifecycle-01KTSJ2H/spec.md
Summary
spec-kitty do currently auto-closes its Op record with outcome="done" at dispatch time (src/specify_cli/cli/commands/do_cmd.py:153), producing false audit records. This mission converts do to the open→work→close lifecycle that ask/advise already follow: do opens the Op and prints the close contract; the host agent does the work and closes via profile-invocation complete with a real outcome; Claude Code session presence surfaces open Ops; doctor ops --close-stale sweeps stale orphans to abandoned. The InvocationRecord schema is split into distinct started/completed event models (completed requires a non-null outcome and carries a closed_by discriminator), legacy records are migrated (rewrite-or-delete), and do gains the SaaS propagator that ask/advise already have. The SaaS envelope changes shape freely with the new schema (decision 01KTSJEQANMNEV16WMSAJP6FR1) — SaaS handlers are unimplemented (#1720/#1693), so CLI behavior locks first.
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: typer (CLI), rich (console output), pydantic v2 (event models), existing specify_cli.invocation package (executor, router, writer, propagator) Storage: Append-only JSONL files under kitty-ops/ (one file per Op + ops-index.jsonl); evidence under .kittify/evidence/<id>/; no database Testing: pytest (unit + CLI integration via typer runner), mypy --strict, ruff; ≥90% coverage on new code (charter policy) Target Platform: CLI on macOS/Linux developer machines; 19 host harnesses consume the output, hook work scoped to Claude Code Project Type: Single project (src/specify_cli/ + tests/) Performance Goals: do dispatch latency regression ≤10%; doctor ops --close-stale <5 s at 10,000 Op files (NFR-001/NFR-002) Constraints: Append-only JSONL contract preserved (C-004); breaking changes allowed, no compat flags (C-001); canonical templates edited at source under src/doctrine/ and session_presence/content.py, never generated agent copies (C-005); dispatch rename out of scope (C-002) Scale/Scope: ~6 modules in src/specify_cli/invocation/, 2 CLI command files, doctor ops module, session-presence content, 1 upgrade migration, doctrine skill/template text
Charter Check
GATE: evaluated against .kittify/charter/charter.md.
- Stack compliance: typer/rich/pydantic/pytest/mypy — all already in use by the touched modules. PASS.
- DIRECTIVE_001 (Architectural Integrity): The change strengthens an existing boundary (record lifecycle owned by
invocationpackage; CLI commands stay thin). No new packages or layers. PASS. - DIRECTIVE_003 (Decision Documentation): Lifecycle decision recorded in spec + decision 01KTSJEQANMNEV16WMSAJP6FR1 (SaaS envelope freedom). The schema split rationale is captured in research.md R2. PASS.
- Coverage/typing: ≥90% new-code coverage, mypy --strict, zero suppressions — carried as NFR-003. PASS (enforced at review).
- Re-check after Phase 1: design artifacts introduce no charter conflicts (no new dependencies, no new top-level packages).
Project Structure
Documentation (this mission)
kitty-specs/do-dispatch-open-op-lifecycle-01KTSJ2H/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
├── contracts/ # Phase 1 output
│ ├── op-record-events.md # Started/completed event schemas + JSONL file contract
│ ├── cli-do-output.md # do command output contract (rich + JSON)
│ └── doctor-ops-close-stale.md # doctor ops sweep contract
└── tasks.md # Phase 2 output (/spec-kitty.tasks — not created here)
Source Code (repository root)
src/specify_cli/
├── invocation/
│ ├── record.py # SPLIT: OpStartedEvent / OpCompletedEvent models (+ legacy parse helpers)
│ ├── writer.py # write_completed gains closed_by; serialization drops blank defaults
│ ├── executor.py # complete_invocation signature: closing actor; do gains propagator wiring
│ ├── propagator.py # envelope builders follow new event shapes (free-form change)
│ └── modes.py # unchanged (mode gate logic reused)
├── cli/commands/
│ ├── do_cmd.py # remove auto-close; add propagator; new capsule close-contract text (rich+JSON)
│ └── advise.py # shared close surface: `profile-invocation complete` outcome/evidence unchanged API
├── doctor/
│ └── ops.py # list_orphan_ops + new close_stale_ops(threshold)
├── cli/commands/doctor*.py # `doctor ops --close-stale --threshold` flags (locate exact wiring in tasks)
├── session_presence/
│ └── content.py # orientation text: open→work→close contract + open-Op listing at session start
├── upgrade/migrations/
│ └── m_<ver>_op_record_schema_v2.py # legacy kitty-ops rewrite-or-delete migration
└── doctrine/ (src/doctrine/...)
└── skills/spec-kitty.advise/SKILL.md + standalone command templates # contract wording updates
tests/
├── specify_cli/invocation/ # record/writer/executor/propagator unit + e2e updates
├── specify_cli/invocation/cli/ # test_do.py rewritten for open-Op behavior
├── specify_cli/invocation/test_doctor_ops.py # close-stale coverage
└── upgrade/ # migration idempotency tests
Structure Decision: Single-project layout; all changes land inside existing packages (invocation, cli/commands, doctor, session_presence, upgrade/migrations, src/doctrine). No new top-level modules.
Complexity Tracking
No charter violations to justify.
Implementation Concern Map
> Implementation concerns are NOT work packages. /spec-kitty.tasks translates these into WPs.
IC-01 — Event schema split
- Purpose: Replace the single frozen
InvocationRecorddoing double duty with distinctOpStartedEvent/OpCompletedEventmodels so completed events require a real outcome and cannot carry blank-default started fields. - Relevant requirements: FR-004, FR-005
- Affected surfaces:
src/specify_cli/invocation/record.py,writer.py(serialization), readers (invocations list, doctor, lifecycle tooling) - Sequencing/depends-on: none (foundation)
- Risks: Many readers parse the old shape; grep all
InvocationRecordconsumers. Keepartifact_link/commit_link/glossary_checkedevent shapes unchanged.
IC-02 — do lifecycle change
- Purpose: Remove auto-close from
do, add the SaaS propagator, and emit the close contract in both rich and JSON output. - Relevant requirements: FR-001, FR-002, FR-008
- Affected surfaces:
src/specify_cli/cli/commands/do_cmd.py(executor builder, lines ~39–42 and ~150–167), capsule rendering shared withadvise.py - Sequencing/depends-on: IC-01 (new started-event shape)
- Risks: JSON consumers (orchestrators) must get a machine-readable close contract; pin with integration tests.
IC-03 — Close surface and closing actor
- Purpose:
profile-invocation completerecords who closed the Op (closed_by: agent vs doctor sweep) on the completed event; idempotent double-close preserved; auto-commit at close (including sweep closes). - Relevant requirements: FR-003, FR-012
- Affected surfaces:
executor.py(complete_invocation,_commit_op_record),advise.py(complete_invocationCLI),writer.py - Sequencing/depends-on: IC-01
- Risks: Auto-commit on protected branches uses the documented exception path — keep
allow_completed_op_on_protected_branchsemantics.
IC-04 — Doctor stale sweep
- Purpose:
doctor ops --close-stale [--threshold HOURS]closes open Ops older than the threshold (default 24 h; 0 = all) withoutcome=abandoned,closed_by=doctor_sweep; report-only behavior unchanged without the flag. - Relevant requirements: FR-006, FR-007, NFR-002
- Affected surfaces:
src/specify_cli/doctor/ops.py, doctor CLI wiring, reusesexecutor.complete_invocationfor the close path - Sequencing/depends-on: IC-03
- Risks: Race with a concurrent manual close → must surface the idempotent error gracefully, not crash the sweep.
IC-05 — Legacy record migration
- Purpose: Upgrade migration rewrites old-schema
kitty-ops/records to the new event shapes; unsalvageable records (no recoverable started identity) are deleted; idempotent. - Relevant requirements: FR-011, NFR-004
- Affected surfaces:
src/specify_cli/upgrade/migrations/(new migration; useget_agent_dirs_for_projectpattern only where applicable — this migration toucheskitty-ops/, not agent dirs) - Sequencing/depends-on: IC-01
- Risks: Migration is the sole sanctioned in-place mutation of records (C-004 exception); must be byte-careful and idempotent.
IC-06 — Session presence + contract prose
- Purpose: Claude Code orientation lists open Ops with close commands at session start; canonical doctrine/skill/template text states the open→work→close contract and stops describing
doas single-shot; commit-hint text replaced by close-contract text. - Relevant requirements: FR-009, FR-010
- Affected surfaces:
src/specify_cli/session_presence/content.py,src/doctrine/skills/spec-kitty.advise/SKILL.md, standalone command templates, CHANGELOG entry (C-001) - Sequencing/depends-on: IC-02 (final capsule wording), IC-04 (doctor command name/flags referenced in prose)
- Risks: Edit source templates only (C-005); run
pytest tests/architectural/test_no_legacy_terminology.pybefore push. Stop-hook feasibility resolved in research.md R5 — session-start listing is in scope; Stop hook documented as follow-up if the surface needs new harness work.
IC-07 — SaaS propagator parity and envelope update
- Purpose: Wire the existing
InvocationSaaSPropagatorintodo's executor; rebuild envelope dicts from the new event models (shape changes freely per decision 01KTSJEQANMNEV16WMSAJP6FR1). - Relevant requirements: FR-008, NFR-001
- Affected surfaces:
do_cmd.pybuilder,propagator.pyevent builders, sync-gate behavior unchanged - Sequencing/depends-on: IC-01, IC-02
- Risks: Keep propagation async/best-effort; do not add latency to dispatch (NFR-001).