Phase 0 Research: Specify on Protected Primary + Branch-Protection Config
Consolidated decisions. The carrier decision has a full record in research/protected-branch-carrier-decision.md (5-agent design squad); summarized here with the two other resolved unknowns.
D1 — Protection carrier shape (DECIDED — squad + ADR 2026-06-21-1)
(protected_branches: frozenset[str], operator_hatch_active: bool, is_protected(ref) -> bool) with one resolve(repo_root) boundary resolver, passed explicitly, feeding the existing core/commit_guard.evaluate(ProtectionState) decision seam.
safe-commit deadlock process (its only factory resolve_action_context fails closed without a mission). A context-nested carrier cannot reach the callsite the mission exists to fix. The decision seam already exists; only the input is scattered.
WorkspaceContext (per-WP JSON-persisted → owner-config snapshot leak); no new object / config-aware protected_branches() (read stays at the callsite → fails FR-007/FR-010). All rejected; see ADR.
- Decision: a standalone, frozen
ProtectionPolicyvalue object - Rationale: no protection callsite holds a built
ExecutionContext— least of all the standalone - Alternatives considered: nest on
ExecutionContext(can't reach the deadlock site); nest on
D2 — On-demand materialization at the spec commit boundary (pillar A)
on-demand materializer that _planning_commit_worktree already invokes for plan/tasks) at a new mission-aware spec-commit entrypoint (spec_commit_cmd.py → the extracted coordination/commit_router.py helper; the generic safe_commit_cmd.py stays mission-blind and unchanged — operator/post-tasks decision): when the resolved policy says the destination is protected, materialize the coordination worktree and route the commit there (materialize-then-retry); make the refusal error actionable.
path-coverage (no call on the specify path). Reusing it honors canonical-sources discipline (C-001) and avoids a parallel materialization path.
materializes worktrees for missions that never need them; (b) make the guard error merely say the command — leaves the operator to run it manually (off-runbook). Rejected in favor of materialize-then- retry at the commit boundary; the actionable-error wording is a secondary safety net (FR-003).
the create→first-write window still resolves reads to the primary (NFR-001).
- Decision: reuse the canonical
coordination/workspace.CoordinationWorkspace.resolve()(the same - Rationale: debugger investigation confirmed the materializer exists and works; the bug is pure
- Alternatives considered: (a) eager materialization at
mission create— larger blast radius, - #1718 preservation: materialization is triggered at the commit boundary, not at read time, so
D3 — .kittify protected-branch configuration schema (pillar B)
key → default {main, master} (+ remote-default augmentation, preserved). Read via the existing config-loader pattern (core/agent_config.py load_config), surfaced only through ProtectionPolicy.resolve.
keeps owner intent declarative (no GitHub-protection-API network dependency — out of scope per spec).
protected_branches: [..]. Empty list = nothing protected (US2 edge case — NOT a silent fallback to default). Unknown branch names are simply unmatched.
collision with future repo settings); reuse commit_guard/repo_defaults sections (overloads an existing block). Chose a dedicated protection: block with headroom for future protection settings.
- Decision: an additive key under
.kittify/config.yamldeclaring the protected-branch set; absent - Rationale: matches the repo's established config idiom; additive + backward compatible (C-004);
- Schema (see
contracts/protection-config.md): a top-levelprotection:block with - Alternatives considered: a flat top-level
protected_branches:key (less namespaced, risks
D4 — Single-authority guard (FR-010 / #1868)
hardcoded {main, master} protection decisions to the resolver + demoted-delegate allowlist.
re-reading — a live #1868 instance. The guard makes the single-authority property enforceable, the same load-bearing-guard pattern used by the untrusted-path audit.
- Decision: a
tests/architectural/guard restrictingprotected_branches(repo_root)/ - Rationale:
coordination/policy.pyalready self-describes as "the single chokepoint" while - Alternatives considered: rely on review discipline (insufficient — the drift already happened).
Out of scope (confirmed)
- #2040 read/write surface-authority desync (distinct seam — C-005).
- Real GitHub branch-protection API detection (replaced by owner config).
- Consolidating the four scattered
.kittify/config.yamlloaders (deferred strangler — Paula). - Attaching
ProtectionPolicyas anExecutionContextfragment (optional future coherence; non-critical).