Implementation Plan: Op record auto-commit skips repos that do not track kitty-ops
Branch: issue-6410-op-record-commit-target (plans and lands here; it reaches main through a PR) | Date: 2026-10-18 | Spec: spec.md Input: Mission specification from kitty-specs/op-record-commit-skip-untracked-01M57Z15/spec.md
Summary
Before the Op executor's best-effort auto-commit runs safe_commit, it asks two questions.
1. Is the exact path about to be committed gitignored? This uses git check-ignore --no-index. 2. Does the primary branch track any Op-record-shaped file? The primary branch is resolved without current-branch bias, and the answer is cached per executor.
Either "ignored" or "not tracked" yields a skip. The record stays on disk, and last_op_commit carries a skipped reason distinct from a refusal. The CLI complete and doctor ops human output render the skip, and their refusal text drops the forbidden hatch advice and the false "will not survive a checkout or reset" claim. Repositories that track Op records, this one included, keep today's current-branch commit.
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: git CLI (subprocess); no new package dependency Storage: files (kitty-ops/*.jsonl) Testing: pytest. A red-first regression test goes through the CLI complete entry point. It is converted to focused unit tests for the new policy module, and existing executor, doctor and e2e fixtures are reworked to track one ULID record on main. Target Platform: Linux/macOS/Windows CLI Project Type: single Performance Goals: NFR-001: at most 12 extra git calls for the first close per executor, then 1 per close Constraints: C-001 (the complete --json payload is frozen), C-002 (no new ref, config key or transport), complexity ≤15, ruff/mypy clean Scale/Scope: about 5 source files, about 6 test files, 2 docs plus the changelog
Supply chain: no dependency decision, so DIRECTIVE_051 controls are not triggered.
Charter Check
- Single canonical authority. The commit-or-skip decision lives in one new module,
src/specify_cli/invocation/op_commit_policy.py. The executor is its only caller, and the Op-record regex is reused, not re-invented. The existing pattern lives inline incoordination/coherence.py, so the policy module exposes its own named constant and a test pins that both agree. Status: pass. - ATDD/red-first. An issue-pinned
@pytest.mark.regressionrepro goes RED before the fix, then is converted. Status: pass. - Terminology. "branch-target routing", Mission. Status: pass.
- No full heavy suites. Targeted runs only. Status: pass.
Project Structure
Documentation (this mission)
kitty-specs/op-record-commit-skip-untracked-01M57Z15/
├── spec.md
├── plan.md
├── traces/
└── tasks.md / tasks/ (from /spec-kitty.tasks)
Source Code (repository root)
src/specify_cli/invocation/op_commit_policy.py # NEW: resolve primary refs, tracked + ignored probes, skip decision
src/specify_cli/invocation/executor.py # OpCommitOutcome.skipped; _commit_op_record consults policy (cached)
src/specify_cli/cli/commands/profile_invocation.py # skip line; corrected refusal warning
src/specify_cli/doctor/ops.py # skipped:<reason> sweep entry value
src/specify_cli/cli/commands/doctor.py # skip line; corrected refusal text
src/specify_cli/state/contract.py # note: commit is conditional
docs/architecture/trail-model.md # describe skip + durability truthfully
docs/changelog/CHANGELOG.md # [Unreleased] Fixed entry
tests/specify_cli/invocation/test_op_commit_policy.py # NEW focused unit tests
tests/specify_cli/invocation/test_executor.py # fixture rework + skip tests
tests/specify_cli/invocation/cli/test_complete.py # rendering
tests/specify_cli/invocation/test_doctor_ops.py # fixture rework + skipped value
tests/specify_cli/invocation/cli/test_doctor_ops_cli.py # rendering
tests/specify_cli/invocation/test_invocation_e2e.py # fixture rework
Structure Decision: single project. All behaviour changes stay inside the invocation package plus its two CLI renderers.
Design
resolve_primary_refs(repo_root) -> tuple[str, ...]:- Resolution order:
git symbolic-ref --quiet --short refs/remotes/origin/HEAD(strip theorigin/prefix), then the first ofmain,master,developthat exists as a local ororigin/ref. - Returns the existing refs among
<primary>andorigin/<primary>. - If nothing resolves, returns an empty tuple. It never falls back to the current branch (FR-006).
primary_tracks_op_records(repo_root, refs) -> bool:- Runs
git ls-tree -r --name-only <ref> -- kitty-ops/for each ref and returns True on the first path matchingOP_RECORD_PATH_RE. - Probe failures raise
OpCommitProbeError. is_path_ignored(repo_root, rel_path) -> bool:git check-ignore -q --no-index -- <path>. Exit 0 → True, exit 1 → False, anything else raisesOpCommitProbeError.op_commit_skip_reason(repo_root, rel_path, *, tracked: bool | None) -> str | None:- Checks ignore first, because one call is cheap, then the tracked decision supplied by the caller.
- Returns a human-readable reason or None.
- Executor:
_ops_tracked_on_primary: bool | Noneis computed lazily once per instance._commit_op_recordcomputes the skip reason after the current-branch check.- A probe error becomes a skip whose reason names the probe failure (spec edge case: best-effort, never raises).
- On a skip it returns
OpCommitOutcome(committed=False, skipped=<reason>)and does not callsafe_commit. OpCommitOutcomegainsskipped: str | None = None.- CLI
complete: - A skip prints a dim note:
Op record kept on disk at kitty-ops/<id>.jsonl, not committed: <reason>. - A refusal prints a corrected warning: the record is on disk and uncommitted, and the operator should commit it on a branch where it belongs. The hatch is no longer mentioned.
- Doctor:
- The entry value is
skipped:<reason>. - Human output gets a separate "kept on disk, not committed (repository does not track Op records)" line.
- The refusal line drops the false checkout/reset claim.
Risks
- Fixture churn hides a real regression. Mitigation: the reworked fixtures keep the original commit assertions, and new tests cover the skip branch on the same fixture with one change (positive control).
- Pinned qualname.
tests/architectural/test_no_worktree_name_guess.pypins the_commit_op_recordqualname. Keep the name and run that gate file.
Implementation Concern Map
- IC-01 Op commit policy: the new module, executor integration and outcome type (FR-001, FR-002, FR-003, FR-006, NFR-001, NFR-002).
- IC-02 Operator-facing reporting: CLI and doctor rendering, plus the doctor entry value (FR-004, FR-005, C-001).
- IC-03 Docs/contract truth: trail-model doc, state contract note and changelog.