Skip to content

T-K3RR-add-lease-namespace-conflict-guard

Status: closed/done · Impact: medium · Complexity: small

Make it impossible for a repo to start using the lease protocol if its ref authority already carries a literal refs/<namespace> ref that would collide with the lease namespace (refs/sdlc/). The guard runs in two places: at library process startup (every invocation refuses to proceed if the conflict is present) and at /sdlc:setup time (the setup skill detects the conflict and reports it as a precondition failure with a clear remediation path). This closes the safety hole described in the ADR’s Namespace conflict guard section.

There is no guard. If someone happened to have created a ref at exactly refs/sdlc (without trailing slash — a leaf ref shadowing the namespace), the first CAS-CREATE attempt under refs/sdlc/... would fail with a confusing Git error about ref-conflict, and the system would have no graceful recovery. The shape of that error message is hostile (it’s a git push plumbing error, not a protocol-level explanation). No tooling exists in /sdlc:setup to inspect the authority’s ref namespace for collisions before the protocol goes live.

The collision is unlikely in practice — no one would normally create a leaf refs/sdlc ref — but per the ADR it’s the one configuration that silently breaks the entire protocol, and the cost of guarding against it is small.

A check_namespace_conflict(authority) function in the lease library (added in T-S0PK-add-lease-protocol-library-and-schemas but as a guard concern this task may extend it) that:

  • Fetches the authority’s ref listing (via git ls-remote against the configured authority).
  • Looks for an exact-match literal ref at refs/sdlc (not refs/sdlc/...).
  • Raises NamespaceConflict with a structured error including the conflicting ref’s SHA and the recommended remediation (delete the offending ref, or pick a different namespace).

The guard hooks in two places:

  • Library process startup. Every CLI invocation (every sdlc lease … command) calls the guard once before proceeding. The check is cached per-process so a long-running invocation doesn’t re-pay the cost. Exit code 7 (NAMESPACE-CONFLICT) reserved for this failure mode.
  • /sdlc:setup skill. The setup skill runs the guard against the configured authority as part of its preflight checks. If the conflict is present, setup exits with a clear error message pointing at the offending ref and the remediation steps. Setup does NOT auto-delete the conflicting ref (that’s destructive; the operator decides).

Reconcile reports a redundant signal — a literal refs/<namespace> ref existing alongside any active lease refs is also flagged there per the ADR’s Reconcile checklist. The reconcile signal is the safety net for repos that somehow got into the bad state after setup ran (e.g., manual ref creation by an operator); the startup guard is the prevention layer.

  1. Decide where the guard lives in the library. Likely plugin/lib/lease/guard.py (new file) with check_namespace_conflict(authority) -> None raising on conflict. Could go in primitives.py if it ends up being one short function. Decision deferred until reading the library shape from T-S0PK-add-lease-protocol-library-and-schemas.
  2. Implement the check. Subprocess to git ls-remote <authority> refs/sdlc. If the output contains a line whose ref is exactly refs/sdlc (no trailing path component), raise NamespaceConflict(ref='refs/sdlc', sha=<sha>, remediation=<message>). Otherwise return cleanly.
  3. Add per-process caching. A module-level _checked: set[str] keyed on authority identifier; subsequent calls within the same process are no-ops.
  4. Wire the startup hook. In the top-level sdlc lease dispatcher (plugin/scripts/sdlc_lease.py from T-QC31-add-sdlc-lease-cli-commands), call check_namespace_conflict(resolved_authority) after argument parsing and before dispatching to the subcommand handler. Map the NamespaceConflict exception to exit code 7 with NAMESPACE-CONFLICT ref=<ref> sha=<sha> on stderr plus the remediation message.
  5. Wire the setup hook. Locate the /sdlc:setup skill’s preflight-check step (in plugin/skills/setup/setup_planning.py or the equivalent) and add a new preflight item that runs the guard against the configured authority. Failure stops setup, prints the conflict + remediation, and points at the ADR’s namespace-conflict guard section for context.
  6. Add tests. A test that constructs a local bare-repo authority with an intentionally-created refs/sdlc leaf ref (via git update-ref refs/sdlc <sha>) and asserts that the guard raises. A complementary test against a clean authority asserts no false-positives. Both tests live alongside the lease-library tests added in T-S0PK-add-lease-protocol-library-and-schemas.
  7. Document the guard’s behavior in plugin/lib/lease/README.md (one section) and in the setup skill’s prose. Cross-link the ADR.
LocationKindChange
plugin/lib/lease/guard.pynewcheck_namespace_conflict(authority) plus per-process caching; raises NamespaceConflict from exceptions.py
plugin/lib/lease/tests/test_guard.pynewPositive tests (conflict detected) + negative tests (no false-positives) + cache-hit test
plugin/lib/lease/__init__.pymodifyRe-export check_namespace_conflict and NamespaceConflict
plugin/lib/lease/README.mdmodifyDocument the guard’s purpose, when it fires, and the remediation it surfaces
plugin/scripts/sdlc_lease.pymodifyCall check_namespace_conflict(resolved_authority) after argparse and before subcommand dispatch; map NamespaceConflict to exit 7 + NAMESPACE-CONFLICT stderr marker
plugin/scripts/lease_cli/_common.pymodifyAdd EXIT_NAMESPACE_CONFLICT = 7 constant + exception-mapping entry (mirrors the existing exit-code map)
plugin/scripts/lease_cli/README.mdmodifyAdd exit code 7 + NAMESPACE-CONFLICT marker to the documented surface
plugin/scripts/lease_cli/tests/test_subcommands.pymodifyIntegration test invoking the CLI against a conflicting bare-repo and asserting exit 7 + stderr marker
plugin/skills/setup/SKILL.mdmodifyDocument the new preflight check; cross-link the ADR’s Namespace conflict guard section
plugin/skills/setup/setup_planning.pymodifyAdd preflight invocation of the guard against the configured authority; stop setup with remediation message on NamespaceConflict
  • AC-1: A fixture local bare-repo authority with git update-ref refs/sdlc <sha> applied causes check_namespace_conflict(authority) to raise NamespaceConflict with the conflicting ref name and SHA populated.
  • AC-2: A fixture local bare-repo authority with refs at refs/sdlc/tasks/T1, refs/sdlc/control-plane, etc. but NO literal refs/sdlc ref produces no false-positive — check_namespace_conflict returns cleanly.
  • AC-3: Calling sdlc lease task claim X against an authority with the namespace conflict exits with code 7, emits NAMESPACE-CONFLICT ref=refs/sdlc sha=<sha> on stderr, and the remediation message points the operator at deleting the offending ref or picking a different namespace.
  • AC-4: Calling /sdlc:setup against the same conflicting authority exits the preflight phase with an error pointing at the conflict, and the setup skill prints the same remediation message as the CLI guard. Setup does not auto-delete the offending ref.
  • AC-5: Per-process caching works: in a single Python process, the second invocation of check_namespace_conflict(authority) for the same authority does not re-shell to git ls-remote. Verified by patching the subprocess call and asserting it’s only invoked once.
  • AC-6: A repo that passes the guard once does not break under normal protocol operation. End-to-end smoke: against a clean local bare-repo authority, the full sdlc lease task claim → heartbeat → transition → release chain (from T-QC31-add-sdlc-lease-cli-commands) runs without the guard intercepting.
  • Auto-remediation (auto-deleting the conflicting ref). Destructive; operator decides. The guard reports + points at the fix.
  • Configurable namespace. The ADR specifies refs/sdlc/... as the namespace; making it project-configurable is a future expansion mentioned in the ADR’s Open Questions territory (it isn’t there explicitly today, but should it become relevant the guard would need to read the configured namespace, not just refs/sdlc).
  • Migrating an existing conflicting authority. If someone genuinely has work at refs/sdlc, dealing with it is one-off operator work, not protocol territory.
  • Reconcile-side detection. Already covered by the ADR’s reconcile checklist; this task does not implement reconcile (that’s slice 3). But this task’s guard should produce structured output that a future reconcile implementation can consume without re-parsing.

May land in parallel with:

  • T-QC31-add-sdlc-lease-cli-commands — the CLI task adds the dispatcher this task hooks into. If the CLI task lands first, this task adds the guard call to it. If this task lands first, it stubs the hook and the CLI task wires through.

This task realizes Rollout Plan item 4 from the ADR’s protocol.md, responding directly to the ADR’s Namespace conflict guard section. It exists as a separate task (rather than folded into the library or CLI task) because it touches both setup and library-startup surfaces — co-locating it with either would mix concerns.

_Captured by /sdlc:task-work on 2026-05-24. PR: pending. Triple-stacked on PR #113 (library) ← PR

  • AC-1: auto — test_check_raises_on_literal_refs_sdlc plants a literal refs/sdlc ref via git update-ref and asserts NamespaceConflict with populated .ref + .sha.
  • AC-2: auto — test_check_passes_when_only_subrefs_present populates refs/sdlc/tasks/T1 + refs/sdlc/control-plane and asserts the guard returns cleanly.
  • AC-3: auto — test_exit_7_namespace_conflict_on_task_claim invokes the CLI against a conflicting bare-repo via subprocess and asserts exit 7 + NAMESPACE-CONFLICT stderr marker + the remediation message.
  • AC-4: agent-manual — hand-ran setup_planning.py against a planted-conflict authority; observed exit 3, NAMESPACE-CONFLICT marker, the remediation message, an explicit “did NOT delete” line, and the ADR cross-link. The setup eval harness covers .gitignore/dir/sdlc.yaml paths only — a lease-namespace eval would need new fixture infrastructure (bare authority + sdlc.yaml) that the existing harness doesn’t model.
  • AC-5: auto — test_cache_hit_skips_subprocess_on_second_call + test_cache_miss_after_reset + test_cache_keyed_per_authority patch the subprocess call and assert single invocation per (authority) tuple.
  • AC-6: auto — implicitly proven by the 20 existing CLI integration tests in test_subcommands.py (claim → heartbeat → transition → release → inspect → list) now running through the dispatcher with the guard wired in. All pass.
  • Triple-stacked branch landed cleanly. The library’s NamespaceConflict exception was already in exceptions.py from the foundation task; just extended the constructor to take ref/sha/remediation while preserving the legacy positional form (a small compat carry).
  • Per-process caching via _checked: set[str] is exactly the right shape — one extra subprocess call shaved off every subsequent CLI invocation in the same process; tested directly with patching.
  • Setup-skill integration was a one-line addition to setup_planning.py’s preflight loop; the existing pattern made this very small.
  • Planting a literal refs/sdlc via git push is rejected by git’s funny-refname guard. Worked around by pushing the SHA via a normal branch first, then git update-ref refs/sdlc <sha> inside the bare repo (which has no funny-ref guard for local writes). Cost ~10 min of investigation. Suggests: a small fixture helper in plugin/lib/lease/tests/conftest.py (e.g. plant_literal_namespace_ref(authority, sha)) so future tests simulating operator-induced ref damage don’t re-derive the workaround. → T-5AMB-lease-tests-plant-literal-namespace-ref
  • AC-10’s grep guard tripped on a comment string. The pre-existing CLI test test_no_git_push_shells_in_cli does a bare substring grep for "git push". Adding the namespace-guard wiring required a comment that contained the phrase (“translate a git push plumbing error into …”); had to rephrase to "git-plumbing error" to satisfy the grep. The substring-grep AC has known false-positive shape — see T-HQ56-safe-grep-helper-for-ac-shell (already spawned from the CLI task’s post-mortem) for the fix. → T-HQ56-safe-grep-helper-for-ac-shell
  • exceptions.NamespaceConflict carried a compat burden. Constructor was (namespace: str) before; this task needs (ref, sha, remediation). Extended with a runtime-shape detection so legacy single-positional callers still work. Worth deprecating the legacy form once external callers are confirmed gone — flagged for a slice-2 cleanup pass. → T-V3FZ-deprecate-namespaceconflict-legacy-ctor
  • setup_planning.py # /// script block didn’t declare deps for lease. The dispatcher transitively imports pydantic + pyyaml via from lease import …. Bumped the inline dependencies = [...] block. First fresh-cache setup invocation is now slightly slower (uv has to resolve pydantic); subsequent runs are unaffected. → T-88JG-lint-inline-script-deps-against-imports

← Back to Tasks