Mission Specification: Mission Coordination Branch with Atomic Event Log

Mission: mission-coordination-branch-atomic-event-log-01KSPTVW Mission ID: 01KSPTVWZ9GGWK5NC9RYNMWPVV Mission Type: software-dev Target Branch: main Source Issue: Priivacy-ai/spec-kitty#1348 Status: Draft


Purpose

TL;DR: Route mission bookkeeping to a per-mission coordination branch and make event-log emission atomically reversible so implement/review never leaves dangling commits on main.

Context: Today, spec-kitty agent action implement and spec-kitty agent action review silently commit planning artifacts to protected branches while loudly rejecting WP transition tracking commits, and they append events to status.events.jsonl before attempting the tracking commit — leaving dirty working trees on main when the commit fails. This produces a working tree where on-disk lane state diverges from committed history, and spec-kitty agent tasks status (which reads the event log) disagrees with git log (which reads the commits). Operators have to manually git checkout -- status.events.jsonl status.json or stand up throwaway prep/... branches to recover — and those prep branches then leak into finalize-tasks output as the "canonical" target_branch for WPs, causing later lane allocation to crash when the prep branch is deleted.

This mission introduces a per-mission coordination branch that owns mission-wide bookkeeping, parents each lane, and is merged back to the canonical target at mission close — combined with a pre-flight workflow-mutation policy gate (cheap refusal before any write) and a surgical rollback of event-log appends when a tracking commit fails after passing the pre-flight gate. As a side effect, finalize-tasks is hardened to record the canonical merge target branch (from mission create --jsonmerge_target_branch) rather than the current checkout branch at the moment it runs.

Core invariant

> No workflow mutation may occur unless the corresponding git mutation is permitted.

Every write that the runtime makes during implement/review — appending to status.events.jsonl, re-materializing status.json, mutating WP frontmatter, emitting planning artifacts (decisions/index.json, issue-matrix.md), and dispatching outbound SaaS/dossier sync — is paired with a tracking commit. The invariant says: ask the git-policy layer before the write; if the commit cannot succeed, the write must not happen. Failures that only manifest at commit time (pre-commit hook reject, disk full, branch-protection rule that didn't exist at pre-flight) trigger the surgical rollback path so the workflow state on disk is restored to its pre-mutation form. Outbound side effects (SaaS, dossier) are deferred until after the commit succeeds.


User Scenarios & Testing

Primary Scenario — Multi-lane mission progresses cleanly

1. Operator runs spec-kitty agent mission create my-feature --json. The command mints mission_id, mid8, mission_slug, and a coordination branch kitty/mission-my-feature-<mid8> parented off main (the canonical target). 2. Operator runs /spec-kitty.tasks and then spec-kitty agent mission finalize-tasks. WP frontmatter for every WP records planning_base_branch: main and merge_target_branch: main (the canonical target from mission create --json), regardless of which branch the operator was on when finalize ran. (These are the existing field names in the WP template; no schema migration is required.) 3. Operator A runs spec-kitty implement WP01 --agent claude. The runtime creates .worktrees/my-feature-<mid8>-lane-a/ (lane worktree) and ensures a dedicated coordination worktree exists at .worktrees/my-feature-<mid8>-coord/ checked out on the coordination branch. The runtime opens a BookkeepingTransaction against the coordination worktree (acquires the feature status lock, stages the planned → claimed event, materializes status.json, calls safe_commit() on the coordination worktree, then releases the lock). Code edits land on the lane branch via the lane worktree. The agent process itself runs inside the lane worktree; it never checkouts the coordination branch into the lane worktree. 4. Operator B concurrently runs spec-kitty implement WP02 --agent opencode. Lane B's worktree is parented off the coordination branch (which already contains lane A's claim event). Lane B's claim event appends after lane A's. Both transitions are visible in spec-kitty agent tasks status from either lane worktree or the coordination branch. 5. As each lane reaches for_review, the runtime auto-rebases that lane from the coordination branch (sync point #1: claim/start was the first; sync point #2 is for_review → in_review). New peer events are pulled in. 6. After all WPs reach done, spec-kitty merge fast-forwards the coordination branch to main and deletes the lane branches and the coordination branch.

Exception Scenario A — Operator runs implement from a protected branch

Note: the operator's current branch is not the policy input — the destination_ref of the would-be tracking commit is. The two relevant cases below differ only in what destination_ref resolves to.

Case A.1: Operator runs implement for a legacy mission (no coordination branch). The runtime resolves destination_ref to the lane branch. If the lane branch is protected (rare), pre-flight refuses; otherwise pre-flight passes and bookkeeping commits land on the lane branch.

Case A.2: Operator runs implement for a coordination-branch mission, but the coordination branch is itself protected (e.g. a CI rule blocks kitty/mission-*). The runtime resolves destination_ref to the coordination branch and pre-flight refuses with "Refusing to record WP01 transitions: configured policy refuses commits on destination ref 'kitty/mission-my-feature-<mid8>'. Adjust branch protection or pass a different mission."

Case A.3 (the actual #1348 reproduction): Operator runs agent action implement from a checkout of main for a coordination-branch mission. The runtime resolves destination_ref to the coordination branch (NOT main); pre-flight passes; the BookkeepingTransaction (FR-023) opens against the coordination worktree, not the operator's main checkout; main is never touched. The fix prevents #1348's symptom: it is impossible for a bookkeeping commit on main to be attempted when the coordination branch is the destination, because the policy input is destination_ref and the write goes through the coordination worktree.

In every case where pre-flight refuses, no write happens at all — status.events.jsonl is untouched, no rollback needed, the operator's working tree is byte-identical to its pre-command state. The error message names destination_ref explicitly so the operator understands which branch the policy rejected.

Exception Scenario B — Tracking commit fails after the pre-flight gate passed (residual failure class)

This scenario covers failures that the pre-flight gate cannot predict: pre-commit hooks that reject the tracking commit message, disk full mid-write, a branch-protection rule that was added between the pre-flight check and the commit attempt, or git plumbing failures. The pre-flight gate has already said "yes, a commit here would be permitted" — but the commit still fails when actually attempted.

1. Pre-flight gate passes (the lane worktree is on a non-protected branch; git-policy says yes). Lane worktree appends the planned → claimed event to status.events.jsonl. The runtime records pre_emit_size = N (the file's byte length before the append) and writes the new event line, making the file length N + L. 2. The runtime re-materializes status.json from the new event log. 3. The runtime attempts the tracking commit on the coordination branch. A pre-commit hook (or branch-protection rule, disk full, etc.) rejects it. 4. The runtime executes surgical rollback: os.truncate(status.events.jsonl, N) (drops only the appended line — preserves any other concurrent writers' state if they appended before this truncate), then re-materializes status.json from the truncated event log. 5. The operator sees a loud diagnostic that names: the rejected commit message, the branch it was attempted on, the lane transition that was rolled back, and a concrete next step (fix the hook and re-run). 6. After fixing the hook, the operator re-runs spec-kitty implement WP01 — the lane transition re-emits and commits cleanly.

Exception Scenario C — (removed)

The earlier draft included a "concurrent emit during rollback" scenario that allowed event-id-targeted removal as an alternative to truncate. That scenario cannot occur under the design adopted in FR-026 (feature status lock held across emit → materialize → commit → rollback): no second emitter can interleave with the in-progress transaction, so a simple os.truncate(path, pre_emit_size) always restores the file to its pre-emit state without clobbering any peer write. The append-only invariant (C-004) is preserved unconditionally; event-id removal is no longer a code path. This eliminates an internal contradiction in the earlier draft.

Edge Cases

  • Mission discarded mid-flight: spec-kitty mission close --discard deletes the coordination branch and all lane branches. main is untouched.
  • Mission close after partial progress: Closing a mission with some WPs done and others canceled still merges the coordination branch to main (it represents the final state of mission-wide bookkeeping).
  • Existing in-flight missions at rollout: Missions whose lanes were created before this change continue to function under a legacy fallback (FR-017). The runtime detects "no coordination branch exists for this mission," emits a one-time warning, and routes bookkeeping commits to the lane branch. The pre-flight policy gate, BookkeepingTransaction contract, status lock, surgical rollback, and outbound-deferral rule still apply (FR-027) — issue #1348's silent-bypass and dangling-event behavior cannot recur in either topology.
  • Hook reject on the rollback's re-materialize commit: The rollback path only writes to disk (truncate + re-materialize); it does not produce a new commit. Hooks do not run on file-system rollback.
  • Coordination branch already exists (e.g. operator partially completed mission create previously): mission create is idempotent — it reuses the existing branch if it points at a commit that is an ancestor of the canonical target; otherwise it refuses with a clear error.

Functional Requirements

IDDescriptionStatus
FR-001The runtime MUST refuse, with a clear error, every bookkeeping commit (both planning-artifact commits and WP transition tracking commits) that targets a protected branch during agent action implement and agent action review. There is no silent-bypass path for spec-kitty-internal commits on protected branches.Draft
FR-002The protected-branch refusal error MUST name (a) the rejected commit message, (b) the destination branch the operator should switch to (lane worktree path AND/OR coordination branch name), and (c) a one-line next-step instruction.Draft
FR-003spec-kitty agent mission create MUST mint a coordination branch named kitty/mission-<mission_slug>-<mid8> off the canonical target branch (resolved via the existing branch-context helper) at mission creation time.Draft
FR-004Lane allocation MUST parent each lane branch (e.g. kitty/mission-<mission_slug>-<mid8>-lane-a) off the coordination branch, not off the canonical target branch.Draft
FR-005Mission-wide bookkeeping — status.events.jsonl, decisions/index.json, issue-matrix.md, all WP transition tracking commits, and all planning-artifact commits — MUST be committed on the coordination branch.Draft
FR-006Lane code work (the operator's source edits inside .worktrees/...) MUST continue to commit on the lane branch and MUST NOT be retargeted to the coordination branch.Draft
FR-007Lane branches MUST auto-rebase from the coordination branch at exactly two sync points per lane lifecycle: (a) lane claim/start (the first planned → claimed for a WP in that lane), and (b) the first for_review → in_review transition for that lane.Draft
FR-008Final mission merge topology MUST be two-staged. Stage 1: when a WP reaches done, the lane branch holding that WP's code MUST be merged into the coordination branch (a "lane integration merge"), with conflict handling, ordering, and a lane_integrated status event recorded for each integrated WP. Stage 2: at mission close, the coordination branch (now holding all integrated lane code + all bookkeeping) MUST be merged into the canonical target branch. The runtime MUST NOT merge a lane branch directly into the canonical target branch; lane code reaches the target only via the coordination branch.Draft
FR-009Before appending an event line to status.events.jsonl, the emission pipeline MUST capture pre_emit_size = os.path.getsize(events_path) (or equivalent f.tell() after open-for-append) and retain it for the duration of the emit attempt.Draft
FR-010If the tracking commit for an emitted event fails (any non-zero git exit, hook reject, disk full, branch-protection refusal), the emission pipeline MUST surgically remove the appended line from status.events.jsonl by calling os.truncate(events_path, pre_emit_size) and MUST re-materialize status.json from the truncated event log. Because the feature status lock (FR-026) is held across the entire emit → materialize → commit → rollback window, no concurrent writer can interleave; the truncate is always safe. Event-id-targeted removal is explicitly NOT a code path — the append-only invariant (C-004) is preserved unconditionally.Draft
FR-011After a successful rollback, the runtime MUST emit a single diagnostic that names: (a) the failing tracking commit message, (b) the branch the commit was attempted on, (c) the lane transition that was rolled back (<from_lane> → <to_lane> for <wp_id>), and (d) the concrete next step.Draft
FR-012spec-kitty agent mission finalize-tasks MUST record the canonical merge target branch (from the mission create --json payload's merge_target_branch field, persisted in meta.jsontarget_branch) in every WP's frontmatter under the existing field names planning_base_branch and merge_target_branch, and in lanes.json. The current checkout branch at finalize-tasks runtime MUST NOT be used. No schema migration of existing WP frontmatter is required; only the value being recorded changes.Draft
FR-013The _PROTECTED_BRANCH_COMMIT_EXCEPTIONS tuple in src/specify_cli/git/commit_helpers.py MUST NOT contain any spec-kitty-internal exception that allows bookkeeping commits to land on a protected branch silently. Documented exceptions for upgrade/release workflows MAY remain but MUST be documented in the module docstring.Draft
FR-014agent action implement and agent action review output MUST include a summary section before declaring success/failure, listing each commit produced: commit message, target branch, and outcome (committed / refused). Format MUST be both human-readable and machine-parseable (JSON output mode included).Draft
FR-015All coordination and lane branch names MUST be derived from mission_id / mid8 / mission_slug. mission_number MUST NOT appear in any branch name. (Reinforces the mission 083 identity model.)Draft
FR-016spec-kitty mission close --discard MUST delete the coordination branch and all its child lane branches. Successful close (mission complete) MUST fast-forward-merge the coordination branch to the canonical target branch and then delete the coordination and lane branches.Draft
FR-017Existing in-flight missions whose lanes were created before this change (no coordination branch exists) MUST continue to function under a legacy fallback. The runtime MUST detect the missing coordination branch, emit a one-time warning, and route bookkeeping commits to the lane branch instead of a coordination branch. The pre-flight policy gate (FR-019/020), the BookkeepingTransaction contract (FR-023), the feature status lock (FR-026), the surgical rollback (FR-010), and the outbound-deferral rule (FR-022) MUST apply to legacy missions unchanged — only the destination_ref differs. Legacy missions MUST NOT inherit issue #1348's silent-bypass or dangling-event behavior under any circumstance.Draft
FR-018mission create MUST be idempotent w.r.t. coordination branch creation: re-running against a partially-created mission MUST reuse the existing coordination branch if it is an ancestor of the canonical target, or refuse with a clear error otherwise.Draft
FR-019A workflow-mutation policy gate MUST be invoked before the first workflow write of every transactional command path (agent action implement, agent action review, agent mission finalize-tasks, planning-artifact emission). The gate takes a GitChangeSet (or equivalent) carrying an explicit destination_ref — the branch that the tracking commit will land on — and asks the git-policy layer "would a commit on destination_ref be permitted?" It MUST NOT use the process's current branch or current checkout as the policy input, because the lane worktree's HEAD is the lane branch while the bookkeeping commit's destination_ref is the coordination branch. The gate refuses the entire operation if the answer is no — before any file is written and before any event is appended.Draft
FR-020The policy gate (FR-019) MUST be the single chokepoint for protected-branch refusal. Every workflow call site MUST pass an explicit destination_ref to the gate: planning-artifact emission (currently src/specify_cli/cli/commands/implement.py:236) passes the coordination branch; lifecycle status writes (currently src/specify_cli/cli/commands/agent/workflow.py:689 and :1463) pass the coordination branch; finalize-tasks passes the coordination branch; lane code commits pass the lane branch. The raw git add / git commit call site for planning artifacts MUST be replaced with safe_commit() routed through the gate.Draft
FR-021The pre-flight gate (FR-019) and the surgical rollback (FR-010) MUST compose: when the pre-flight gate refuses, no rollback machinery runs (nothing was written); when the pre-flight gate passes but the commit fails afterward, the rollback path runs. Both paths MUST produce the same diagnostic shape (FR-002/FR-011) so operators see one consistent error format regardless of which failure class fired.Draft
FR-022Outbound side effects emitted during transactional command paths — SaaS event sync, decision-thread fanout to a tracker, dossier ingress, and any analogous "push to external system" — MUST be deferred until after the corresponding local tracking commit succeeds. If the commit fails (and rollback runs), the outbound emission MUST NOT have occurred. This applies to the implement and review command paths; non-transactional read-side syncs are unaffected.Draft
FR-023Writes to the coordination branch MUST go through a dedicated BookkeepingTransaction service whose contract is: (1) resolve the coordination worktree path (.worktrees/<slug>-<mid8>-coord/); (2) acquire the feature status lock; (3) run the pre-flight policy gate (FR-019) against the coordination branch as destination_ref; (4) stage workflow mutations under the lock; (5) call safe_commit() on the coordination worktree; (6) defer outbound side effects (FR-022) until the commit returns success; (7) on commit failure, run the surgical rollback (FR-010) under the still-held lock; (8) release the lock. The transaction is the single owner of "writes that target the coordination branch." Implementation may use a dedicated coordination worktree, a ref-update transaction via git plumbing, or another concrete mechanism — but the contract MUST be honored.Draft
FR-024A coordination worktree MUST exist at .worktrees/<mission_slug>-<mid8>-coord/ for every active mission whose topology is the new coordination-branch model (not the legacy fallback). The runtime MUST create the coordination worktree at first implement/review invocation if it does not already exist (idempotent: reuse if present and pointing at the correct branch). The coordination worktree MUST NOT be checked out by the operator's agent process; it exists solely to host BookkeepingTransaction operations.Draft
FR-025spec-kitty agent action implement and agent action review MUST run the agent process inside the lane worktree (.worktrees/<slug>-<mid8>-lane-<id>/) and MUST NOT git checkout the coordination branch into the lane worktree. Coordination-branch writes happen via the coordination worktree handle held by BookkeepingTransaction.Draft
FR-026The existing feature status lock (src/specify_cli/status/locking.py, using the filelock library) MUST be held across the full atomic window of every BookkeepingTransaction: from the moment the pre-flight gate passes, through event append → status.json materialization → tracking commit → (on commit success) outbound side-effect emission OR (on commit failure) surgical truncate rollback → release. Concurrent emitters block on the lock; they cannot interleave with an in-progress transaction. This is the single mechanism that guarantees the truncate-based rollback (FR-010) preserves peer state.Draft
FR-027The pre-flight gate (FR-019/FR-020), BookkeepingTransaction contract (FR-023), and surgical rollback (FR-010) MUST apply uniformly to legacy missions (FR-017) AND to coordination-branch missions. For legacy missions the destination_ref resolves to the lane branch (or whatever the legacy code path computed); for coordination-branch missions it resolves to the coordination branch. Legacy missions MUST NOT skip the pre-flight gate or the rollback — that would re-create the issue #1348 symptom for those missions. The only thing the legacy path skips is the coordination worktree (FR-024); the policy and atomicity invariants apply universally.Draft
FR-028status.events.jsonl and status.json MUST be exclusively coordination-branch-authored. Lane branches MUST NOT contain commits that modify either file. Lane branches MUST exclude both files from any integration merge (lane → coordination) via the merge driver, .gitattributes, or equivalent mechanism — lane integration brings code only. The merge/rebase scenario the earlier draft worried about is eliminated by construction: there is exactly one status.events.jsonl in the world for a mission, and it lives on the coordination branch.Draft
FR-029Lane worktrees MUST use sparse-checkout to exclude kitty-specs/<mission>/status.events.jsonl and kitty-specs/<mission>/status.json from their working tree. The lane allocator registers the sparse-checkout pattern at worktree creation time. The primary checkout (and the coordination worktree) see those files normally. This preserves the file paths under kitty-specs/<mission>/ (no data migration) while preventing lane-side accidental edits.Draft
FR-030Lane-side reads of mission status MUST go through CLI mediation. spec-kitty agent tasks status --mission <handle>, spec-kitty agent context resolve --mission <handle>, and any other read-side query MUST resolve the coordination worktree path and read from there, regardless of the operator's CWD. Agents working in a lane never read status.events.jsonl or status.json directly from disk; they always invoke the CLI. (An on-disk snapshot mirror is explicitly NOT in scope for this mission; it is an optimization for a future ticket.)Draft
FR-031safe_commit() MUST gain a required keyword-only destination_ref parameter. Internally safe_commit() MUST assert HEAD == destination_ref for the worktree it operates against, raising a structured error if they diverge. This is the helper-level invariant that makes destination_ref a commit-target contract rather than a policy label. Every existing caller of safe_commit() MUST be migrated to pass destination_ref explicitly; mypy --strict enforces the parameter; the assertion catches at runtime any caller that does not first ensure HEAD matches. BookkeepingTransaction (FR-023) becomes a higher-level wrapper that ensures the coordination worktree is on destination_ref before delegating to safe_commit(), but the helper-level assertion is the ultimate gate against silent commit-target drift.Draft
FR-032The status domain (src/specify_cli/status/) MUST expose pure functions — build_status_event(...) (constructs a StatusEvent instance with no I/O), append_event_jsonl(events_path, event) (pure I/O — appends one line, no commits, no materialization), and materialize(events_path) (existing; reads events, returns snapshot) — and MUST NOT depend on src/specify_cli/coordination/ or take BookkeepingTransaction as a parameter. The transaction layer orchestrates these pieces; the status domain does not know transactions exist. This preserves clean DDD layering: low-level status domain is reusable; coordination application service holds the orchestration logic.Draft
FR-033EventReceipt MUST be split into two distinct return types: (a) PendingEventHandle — returned by BookkeepingTransaction.append_event(); carries event_id only; signals the event was appended to disk under the lock but not yet committed. (b) CommitReceipt — returned by BookkeepingTransaction.commit() or accumulated at __exit__; carries commit_sha, committed_at, destination_ref, worktree_root, and the list of event_ids included in the commit. This makes the lifecycle coherent: the receipt for "appended" is distinct from the receipt for "committed."Draft

Non-Functional Requirements

IDDescriptionThresholdStatus
NFR-001After a forced tracking-commit failure (with no concurrent writers), status.events.jsonl on disk MUST be byte-identical to its pre-emit state (SHA-256 equal).100% of 100 forced-failure test casesDraft
NFR-002Surgical-rollback execution time (truncate + re-materialize status.json) MUST complete within a small bounded budget on a realistic event log.< 100ms on a 10 MB / 100k-line event logDraft
NFR-003spec-kitty agent tasks status results MUST be identical when queried from any lane worktree or the coordination branch, within one rebase sync point of the most recent peer emit.≤ 1 sync point of divergence (defined in FR-007)Draft
NFR-004spec-kitty agent mission create total runtime MUST NOT regress beyond the existing budget despite adding coordination branch creation.< 2 seconds end-to-end on a 10k-file repoDraft
NFR-005The implement/review commit-summary block (FR-014) MUST add no more than a small bounded number of bytes to a typical command's stdout in success cases.≤ 1 KB per command invocation in the happy pathDraft
NFR-006Test coverage for new code (event-log rollback path, coordination branch lifecycle, finalize-tasks branch resolution) MUST meet the project's charter policy.≥ 90% line coverage on new modulesDraft
NFR-007The protected-branch refusal error (FR-001/FR-002) MUST cite a stable error code suitable for scripted detection.One stable identifier per refusal classDraft
NFR-008The pre-flight policy gate (FR-019) MUST add bounded latency to transactional command paths.< 10ms per invocation on a local git repoDraft
NFR-009After a forced commit failure (post-pre-flight class — e.g. hook reject), any SaaS event sink configured for the project MUST NOT have received the rolled-back event.100% of 100 forced-failure test casesDraft
NFR-010The feature status lock (FR-026) MUST be acquired and released within a bounded total time during the happy path of a BookkeepingTransaction.< 250ms wall-clock from acquire to releaseDraft
NFR-011Coordination worktree setup (FR-024) on first invocation MUST complete within the existing implement/review startup budget.< 1 second on a typical missionDraft

Constraints

IDDescriptionStatus
C-001Coordination branch naming MUST follow kitty/mission-<mission_slug>-<mid8> exactly, to remain consistent with the mid8 identity model from mission 083.Active
C-002Lane branch naming MUST continue to follow kitty/mission-<mission_slug>-<mid8>-lane-<id>. (No change.)Active
C-003The implementation MUST NOT regress the protected-branch guard for any commit class that is currently rejected. (The change tightens the guard for planning-artifact commits; it does not loosen it for anything else.)Active
C-004Event log entries MUST remain append-only and chronologically ordered after every rollback. (No event_id reuse across truncate; no out-of-order timestamps.)Active
C-005All identity lookups MUST continue to disambiguate by mission_id (or its mid8 prefix). mission_number remains display-only and MUST NOT appear in branch names or worktree paths.Active
C-006ASCII-only sanitization (per DIR-010 / DIR-011) applies to every input that feeds the coordination branch name derivation.Active
C-007The change MUST NOT break existing missions whose worktrees already exist parented on the canonical target branch. Backward-compatible fallback is mandated by FR-017.Active
C-008mypy --strict MUST continue to pass with no errors in any new or modified module. (Charter policy.)Active
C-009Rollback MUST NOT use git checkout -- <path> as the restore primitive for ANY transaction-owned file (not just status.events.jsonl). The reason generalizes: git checkout -- is destructive to any other legitimate uncommitted changes, races with concurrent emitters, and restores to HEAD rather than the pre-emit state. For status.events.jsonl, use os.truncate(path, pre_emit_size). For all other transaction-owned artifacts (decisions/index.json, issue-matrix.md, etc.), capture pre-write bytes (Path.read_bytes(), or None if the file did not exist) at the moment of write_artifact() and restore from that snapshot on rollback.Active
C-010No new external dependencies. The rollback path MUST use Python stdlib (os.truncate, os.path.getsize) and the existing git subprocess wrappers in specify_cli.git.Active
C-011Coordination-branch writes MUST NOT be performed by git checkout-ing the coordination branch into the lane worktree. The lane worktree's HEAD remains the lane branch for the entire duration of implement/review. Coordination writes use the coordination worktree (FR-024) or equivalent ref-update mechanism.Active
C-012The policy gate's input MUST be destination_ref — the explicit branch the would-be commit targets — and MUST NOT use process CWD, git rev-parse HEAD, or any other inferred-from-checkout value. Any call site that fails to pass destination_ref is a bug.Active
C-013The feature status lock from src/specify_cli/locking.py is the canonical concurrency primitive. New code MUST NOT introduce a parallel lock for the same scope. The existing lock contract (process-level, file-based, exclusive) is the contract honored by FR-026.Active
C-014status.events.jsonl MUST NEVER be the subject of a git merge, rebase, or cherry-pick across branches. The file's authorship is the coordination branch exclusively (FR-028). Any future code that tries to resolve a conflict on status.events.jsonl is a bug.Active
C-015safe_commit() MUST refuse to operate when its caller has not passed destination_ref or when the worktree HEAD does not match destination_ref. The helper has no inference path — it never derives destination_ref from CWD or HEAD on the caller's behalf. The public spec-kitty safe-commit CLI surface may include a deprecated SPEC_KITTY_INFER_DESTINATION_REF=1 env var that resolves the value at the CLI layer and passes it explicitly to the helper; this is scoped as "CLI-only explicit resolver," NOT a helper-level fallback, and ships with a removal timeline.Active
C-016destination_ref MUST use a single canonical internal form: the short branch name (e.g. kitty/mission-foo-01ABCDEF), not the fully-qualified refs/heads/... form. The HEAD assertion in safe_commit() MUST normalize git symbolic-ref HEAD output (strip the refs/heads/ prefix) before comparing. All persisted artifacts (meta.jsoncoordination_branch, lanes.json → branch fields) use the short form. All CLI inputs are normalized on entry. This avoids false HEAD-mismatch errors from format drift.Active

Success Criteria

IDOutcomeHow verified
SC-01Zero spec-kitty bookkeeping commits land on a protected branch in a sample of 100 simulated implement/review runs after rollout.Integration test sweep with branch-protection-on fixtures
SC-02A user running spec-kitty agent tasks status from any lane worktree or coordination branch sees the same WP lane state within one rebase sync point of the latest peer emit.Multi-lane integration test querying status from N viewpoints
SC-03A user reading the implement / review terminal output can identify every commit the command produced (message, target branch, outcome) without inspecting git log.Golden-output integration test
SC-04A mission created after rollout, with finalize-tasks run from a non-canonical branch (e.g. prep/...), produces WP frontmatter and lanes.json whose target_branch field equals the canonical merge target.Unit + integration test for finalize-tasks branch resolution
SC-05After 100 forced tracking-commit failures (via hook reject), status.events.jsonl on disk is byte-identical (SHA-256) to its pre-emit state in every case where no concurrent writers were active.Forced-failure stress test
SC-06The dangling-event reproduction from issue #1348 (event appended on main, commit rejected, on-disk state advanced) no longer reproduces on any branch.Regression test against the exact issue #1348 sequence
SC-07Existing in-flight missions (created before this change) continue to function without manual intervention; the runtime warns once and falls back to legacy topology.Migration smoke test on a pre-existing mission fixture
SC-08A pre-flight refusal on a protected branch (Exception Scenario A) leaves status.events.jsonl, status.json, WP frontmatter, planning-artifact files, and the SaaS event sink byte-identical / event-identical to their pre-command state in 100% of test cases.Pre-flight stress test (protected-branch fixture)
SC-09After a forced post-pre-flight commit failure (hook reject), the SaaS event sink has received zero events for the rolled-back transition.Forced-failure test with mocked SaaS sink
SC-10After a full multi-lane mission cycle, the canonical target branch contains every lane's code work plus all mission-wide bookkeeping. No lane code is lost; no bookkeeping commit lands directly on the target without flowing through the coordination branch.End-to-end integration test
SC-11A legacy mission (no coordination branch) hitting a protected-branch refusal under the new code path produces zero dangling event-log appends and zero silent commits on the protected branch.Legacy-mode regression test for issue #1348
SC-12Concurrent multi-lane emitters under stress (e.g. 20 simultaneous implement calls) MUST produce a status.events.jsonl whose lines are all valid, append-only, and consistent with the resulting status.json — no interleaved partial writes, no truncate-clobber of peer events.Multi-process stress test

Key Entities

  • Coordination branch (kitty/mission-<slug>-<mid8>) — per-mission branch that owns mission-wide bookkeeping. Created at mission create. Parent of every lane branch for that mission. Merged to canonical target at mission close. Deleted on --discard.
  • Lane branch (kitty/mission-<slug>-<mid8>-lane-<id>) — per-lane branch parented on the coordination branch. Owns lane code work. Auto-rebases from coordination at claim/start and for_review → in_review.
  • Event log (status.events.jsonl) — append-only JSONL file. Lives on the coordination branch (not on individual lanes). Sole authority for WP lane state (per 3.0 status model).
  • Tracking commit — bookkeeping commit produced by spec-kitty itself to record a lane transition (e.g. chore: Start WP01 implementation [claude]). Target: coordination branch.
  • Planning-artifact commit — bookkeeping commit produced during implement/review to record planning artifacts (decisions/index.json, issue-matrix.md). Target: coordination branch.
  • Pre-emit size — byte length of status.events.jsonl immediately before an event line is appended. Captured in memory for the duration of the emit attempt; used by the surgical rollback path on commit failure.
  • Rebase sync point — a moment at which a lane branch is automatically rebased from the coordination branch. Exactly two per lane lifecycle (lane claim/start; first for_review → in_review).
  • Workflow-mutation policy gate — pre-flight check invoked before the first workflow write of every transactional command path. Asks the git-policy layer: "would the corresponding tracking commit be permitted?" Refusal short-circuits the operation before any write happens. Single chokepoint for protected-branch refusal across implement, review, and finalize-tasks.
  • Transactional command path — a command (agent action implement, agent action review, agent mission finalize-tasks) whose semantics require all writes to be atomically committed. Out-of-scope: read-only command paths (status, dashboard, decision verify).
  • Outbound side effect — work performed against an external system during a transactional command path (SaaS event sync, decision-thread fanout to a tracker, dossier ingress). Deferred until the corresponding local commit succeeds.
  • Coordination worktree (.worktrees/<slug>-<mid8>-coord/) — a git worktree dedicated to holding the coordination branch as a working tree, so that BookkeepingTransaction can stage and commit without disturbing the lane worktree's HEAD. Created idempotently at first implement/review invocation. Removed at mission close.
  • BookkeepingTransaction — the single service that owns writes to the coordination branch. Contract: resolve coordination worktree → acquire feature status lock → run pre-flight gate against destination_ref → stage workflow mutations → safe_commit() on coordination worktree → defer outbound side effects until commit success → on commit failure run surgical truncate rollback (under the still-held lock) → release lock.
  • destination_ref — the branch a workflow write will land on. Always passed explicitly to the policy gate. Never inferred from the process's current branch or current checkout.
  • Lane integration merge — the per-WP merge that brings a lane branch's code into the coordination branch when the WP reaches done. Distinct from the final mission merge (coordination → target).
  • Feature status lock — the existing exclusive lock provided by src/specify_cli/locking.py. Held by BookkeepingTransaction across the entire emit → materialize → commit → rollback window so peer emitters cannot interleave.

Domain Language

Canonical termAvoidWhy
coordination branch"mission branch", "main mission""mission branch" is ambiguous (could mean any branch related to the mission); "coordination" names its role.
lane branch"WP branch", "worktree branch"WPs and lanes are not 1:1 (a lane can hold multiple WPs).
canonical target branch"main"The target branch is project-configurable; main is just the most common value.
tracking commit"status commit", "transition commit""status commit" historically referred to status.json writes; "tracking" matches the existing CLI verb.
pre-emit size"rollback offset", "save point""Pre-emit size" names exactly what the value is (file byte length before the append).
surgical rollback"rollback", "undo", "checkout"Distinguishes the targeted truncate-by-offset approach from git checkout -- (explicitly banned per C-009).
sync point"rebase", "pull", "merge"A sync point is the moment a lane rebases — not the rebase itself.
workflow-mutation policy gate"guard", "lock", "check""Guard" already refers to the protected-branch check in commit_helpers.py. The gate wraps the guard at every workflow call site; it is not the guard itself.
transactional command path"command", "operation"Many CLI commands are read-only and not bound by the gate. Naming the class avoids over-applying the invariant.

Assumptions

1. No remote push for coordination/lane branches is required by this mission. The branches remain local until the mission merges. (Operators who want to push are not blocked, but tooling does not push automatically.) 2. In-flight missions at rollout use the legacy topology. The new topology applies to missions created after rollout. Migration is opt-in (run mission close --discard and recreate, or wait for the mission to finish under the legacy topology). 3. Coordination branch is created locally by mission create. No PR is opened against the coordination branch automatically; that is the operator's choice. 4. Status files are never merged. status.events.jsonl and status.json are coordination-branch-authored exclusively (FR-028). The earlier draft's "rebase strategy" question is moot: lane branches do not author status changes, so there is nothing to merge. Lane integration merges (lane → coordination) bring code only; the .gitattributes / merge driver / sparse-checkout policy is the mechanism by which the runtime guarantees lane branches never produce a diff for these files. 5. The protected-branch list and detection logic in commit_helpers.py are unchanged. This mission tightens the guard's exception list, not the guard's detection. 6. os.truncate is portable on all platforms spec-kitty targets (per DIR-001: Linux, macOS, Windows 10+). The existing feature status lock (src/specify_cli/status/locking.py) is built on the filelock library, which is already cross-platform; no fcntl usage anywhere in this mission. 6a. destination_ref canonical form is the short branch name (e.g. kitty/mission-foo-01ABCDEF). Tools that produce fully-qualified refs (git symbolic-ref HEADrefs/heads/...) normalize at the boundary. See C-016. 7. Issue #1348's stated acceptance criteria are fully addressed by this spec and require no additional sub-features. (See SC-06 for the explicit regression check.) 8. The bulk-edit gate does not apply: this mission introduces new behavior + bug fix; it does not rename existing identifiers across files. 9. The policy-gate abstraction is new code, not a rename. Existing call sites (planning-artifact emit in implement.py, lifecycle writes in agent/workflow.py) gain a single new precondition call; their internal logic is otherwise unchanged. The git-policy layer they delegate to is the existing commit_helpers.py protected-branch check.


Out of Scope

  • True architectural atomicity (build event in memory, stage with git hash-object + git update-index --add --cacheinfo, commit, then persist to working tree). Explicitly deferred as too large for this mission; surgical truncate-on-failure is the chosen pragmatic approach.
  • A custom git merge driver registered globally for status.events.jsonl line-append conflicts. May be added as a /plan implementation choice if stock git rebase is insufficient, but is not an FR of this mission.
  • Auto-pushing coordination/lane branches to a remote.
  • Automatic migration of in-flight missions onto the new topology.
  • Changes to the protected-branch detection list itself (which branches count as "protected"). Only the exception list within that guard changes.
  • Changes to mission identity (mission_id / mid8 / mission_slug / mission_number) — that model is settled by mission 083 and is preserved unchanged here.

References

  • Source issue: Priivacy-ai/spec-kitty#1348 — protected-branch guard bypass inconsistency and dangling event-log writes.
  • Identity model (prior mission): architecture/adrs/2026-04-09-1-mission-identity-uses-ulid-not-sequential-prefix.md — establishes mission_id / mid8 as canonical identity; this mission preserves that contract.
  • Status model: src/specify_cli/status/ and kitty-specs/034-feature-status-state-model-remediation/ — the event log as sole authority for WP lane state. This mission keeps that invariant; the coordination branch becomes the canonical place where the event log lives.
  • Commit helpers: src/specify_cli/git/commit_helpers.py:439 — the protected-branch guard (safe_commit() precondition). Confirmed by cross-review: the guard itself is correct; the bugs are at the call sites that bypass it.
  • Cross-review file/line evidence (independent investigation):
  • src/specify_cli/cli/commands/implement.py:236 — raw git add / git commit for planning artifacts, bypasses safe_commit(). Target of FR-001 / FR-013 / FR-020.
  • src/specify_cli/cli/commands/agent/workflow.py:689 and :1463 — lifecycle status writes occur before safe_commit() is called. Target of FR-009 / FR-010 / FR-019 / FR-021.
  • src/specify_cli/status/emit.py:468 — event append and status.json re-materialization happen before commit policy runs. Target of FR-009 / FR-010 / FR-019.
  • src/specify_cli/cli/commands/agent/mission.py:321 — returns the current checkout branch, causing _resolve_planning_branch() to leak prep branches into WP frontmatter. Target of FR-012.
  • src/specify_cli/runtime/root_resolver.py:164 — canonicalizes status writes to the primary checkout. Today this constrains where bookkeeping can land; FR-023 / FR-024 introduce the coordination worktree so this canonicalization works with the new topology rather than against it.
  • src/specify_cli/locking.py:1 — existing feature status lock. Reused (per C-013) as the concurrency primitive for BookkeepingTransaction (FR-026).
  • tasks/README.md:15 — confirms the WP frontmatter field names already in use are planning_base_branch and merge_target_branch. FR-012 now matches this schema (P2 finding from cross-review).
  • Charter directives in scope: DIRECTIVE_003 (Decision Documentation), DIRECTIVE_010 (Specification Fidelity), DIR-010 / DIR-011 (ASCII-only identifier sanitization).