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.
Proposed
Section titled “Proposed”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-remoteagainst the configured authority). - Looks for an exact-match literal ref at
refs/sdlc(notrefs/sdlc/...). - Raises
NamespaceConflictwith 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:setupskill. 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.
Approach
Section titled “Approach”- Decide where the guard lives in the library. Likely
plugin/lib/lease/guard.py(new file) withcheck_namespace_conflict(authority) -> Noneraising on conflict. Could go inprimitives.pyif it ends up being one short function. Decision deferred until reading the library shape from T-S0PK-add-lease-protocol-library-and-schemas. - Implement the check. Subprocess to
git ls-remote <authority> refs/sdlc. If the output contains a line whose ref is exactlyrefs/sdlc(no trailing path component), raiseNamespaceConflict(ref='refs/sdlc', sha=<sha>, remediation=<message>). Otherwise return cleanly. - Add per-process caching. A module-level
_checked: set[str]keyed on authority identifier; subsequent calls within the same process are no-ops. - Wire the startup hook. In the top-level
sdlc leasedispatcher (plugin/scripts/sdlc_lease.pyfrom T-QC31-add-sdlc-lease-cli-commands), callcheck_namespace_conflict(resolved_authority)after argument parsing and before dispatching to the subcommand handler. Map theNamespaceConflictexception to exit code 7 withNAMESPACE-CONFLICT ref=<ref> sha=<sha>on stderr plus the remediation message. - Wire the setup hook. Locate the
/sdlc:setupskill’s preflight-check step (inplugin/skills/setup/setup_planning.pyor 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. - Add tests. A test that constructs a local bare-repo authority with an intentionally-created
refs/sdlcleaf ref (viagit 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. - Document the guard’s behavior in
plugin/lib/lease/README.md(one section) and in the setup skill’s prose. Cross-link the ADR.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/lib/lease/guard.py | new | check_namespace_conflict(authority) plus per-process caching; raises NamespaceConflict from exceptions.py |
plugin/lib/lease/tests/test_guard.py | new | Positive tests (conflict detected) + negative tests (no false-positives) + cache-hit test |
plugin/lib/lease/__init__.py | modify | Re-export check_namespace_conflict and NamespaceConflict |
plugin/lib/lease/README.md | modify | Document the guard’s purpose, when it fires, and the remediation it surfaces |
plugin/scripts/sdlc_lease.py | modify | Call 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.py | modify | Add EXIT_NAMESPACE_CONFLICT = 7 constant + exception-mapping entry (mirrors the existing exit-code map) |
plugin/scripts/lease_cli/README.md | modify | Add exit code 7 + NAMESPACE-CONFLICT marker to the documented surface |
plugin/scripts/lease_cli/tests/test_subcommands.py | modify | Integration test invoking the CLI against a conflicting bare-repo and asserting exit 7 + stderr marker |
plugin/skills/setup/SKILL.md | modify | Document the new preflight check; cross-link the ADR’s Namespace conflict guard section |
plugin/skills/setup/setup_planning.py | modify | Add preflight invocation of the guard against the configured authority; stop setup with remediation message on NamespaceConflict |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: A fixture local bare-repo authority with
git update-ref refs/sdlc <sha>applied causescheck_namespace_conflict(authority)to raiseNamespaceConflictwith 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 literalrefs/sdlcref produces no false-positive —check_namespace_conflictreturns cleanly. - AC-3: Calling
sdlc lease task claim Xagainst an authority with the namespace conflict exits with code 7, emitsNAMESPACE-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:setupagainst 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 togit 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 → releasechain (from T-QC31-add-sdlc-lease-cli-commands) runs without the guard intercepting.
Out of scope
Section titled “Out of scope”- 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 justrefs/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.
Dependencies
Section titled “Dependencies”- T-S0PK-add-lease-protocol-library-and-schemas — must land first. The guard sits in the same module hierarchy and uses the same exception conventions.
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.
Discovery context
Section titled “Discovery context”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.
Post-mortem
Section titled “Post-mortem”_Captured by /sdlc:task-work on 2026-05-24. PR: pending. Triple-stacked on PR #113 (library) ← PR
114 (CLI)._
Section titled “114 (CLI)._”Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
test_check_raises_on_literal_refs_sdlcplants a literalrefs/sdlcref viagit update-refand assertsNamespaceConflictwith populated.ref+.sha. - AC-2: auto —
test_check_passes_when_only_subrefs_presentpopulatesrefs/sdlc/tasks/T1+refs/sdlc/control-planeand asserts the guard returns cleanly. - AC-3: auto —
test_exit_7_namespace_conflict_on_task_claiminvokes the CLI against a conflicting bare-repo via subprocess and asserts exit 7 +NAMESPACE-CONFLICTstderr marker + the remediation message. - AC-4: agent-manual — hand-ran
setup_planning.pyagainst a planted-conflict authority; observed exit 3,NAMESPACE-CONFLICTmarker, 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_authoritypatch 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.
What worked
Section titled “What worked”- Triple-stacked branch landed cleanly. The library’s
NamespaceConflictexception was already inexceptions.pyfrom the foundation task; just extended the constructor to takeref/sha/remediationwhile 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.
Friction and automation gaps
Section titled “Friction and automation gaps”- Planting a literal
refs/sdlcviagit pushis rejected by git’s funny-refname guard. Worked around by pushing the SHA via a normal branch first, thengit 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 inplugin/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_clidoes a bare substring grep for"git push". Adding the namespace-guard wiring required a comment that contained the phrase (“translate agit pushplumbing 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.NamespaceConflictcarried 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-ctorsetup_planning.py# /// scriptblock didn’t declare deps forlease. The dispatcher transitively imports pydantic + pyyaml viafrom lease import …. Bumped the inlinedependencies = [...]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
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-5AMB-lease-tests-plant-literal-namespace-ref — created; expose a fixture helper that plants
a literal
refs/sdlcref so future operator-damage tests skip the funny-refname workaround. - T-HQ56-safe-grep-helper-for-ac-shell — linked; existing task already owns the AC-10 substring-grep footgun fix.
- T-V3FZ-deprecate-namespaceconflict-legacy-ctor — created; slice-2 cleanup that removes the
runtime-shape-detection branch from
NamespaceConflict.__init__once external callers are confirmed gone. - T-88JG-lint-inline-script-deps-against-imports — created; CI lint that diffs
# /// scriptdependencies = [...]blocks against actual imports so transitive-dep drift fails early.