T02

The repair belongs in shared vendor_mirror_canonical, not individual commands. Static inspection establishes a normalization mismatch; no behavior was executed.

src/gzkit/governance/brief_path_validity.py:94 strips trailing slashes but retains leading ./ before matching its five mirror prefixes. Consequently, ./.claude/rules/tests.md evades mirror classification. An existing path then passes filesystem resolution; a missing path gets only an existence error, or passes when a creates declaration supplies its normalized spelling. The unprefixed equivalent instead produces canonical advice before either exemption. This violates the module’s stated refusal invariant and .gzkit/skills/gz-obpi-specify/SKILL.md’s “a vendor-mirror path fails regardless” policy.

Normalize one literal leading ./ inside the classifier before both prefix matching AND suffix slicing. Otherwise the advice tail acquires an offset error. Preserve the original input in diagnostics and retain component-boundary matching, directory-root advice, trailing-slash handling, and glob suffixes. Use removeprefix, never lstrip("./"): the latter destroys dot-root names, a failure already documented and tested for creates extraction. Keep mirror refusal ahead of existence and creates checks. No registry expansion, filesystem canonicalization, or change to creates eligibility is needed.

The coupled trace is:

  • Authors produce backticked Allowed Paths; extract_allowed_paths preserves their spelling. Brief/plan creates extractors normalize leading ./ and trailing /. The scaffold producer _build_discovery_prerequisites also emits “intentionally created in this OBPI,” which the shared extractor recognizes. Verify actual extraction plus validation, including this marker, rather than passing only hand-built creates sets.
  • _gather_brief_path_gaps in commands/plan_audit_cmd.py unions brief and plan creates, calls the shared checker, and passes gaps to _emit_result. Test prefixed mirrors with existing files, missing files, brief-only creates, and plan-only creates. Assert exit 1, FAIL receipt, correct original-path/canonical-path pair, and gap count. Its imported compatibility classifier must give identical advice.
  • commands/obpi_cmd.py invokes the wrapper for both single and batch validation only with --authored. Test both command routes with otherwise valid fixtures and retain lenient non-authored behavior. Existing wiring tests mostly call the helper and do not prove command dispatch.
  • commands/adr_promote.py::_check_scaffold_obpis also consumes the wrapper. Verify prefixed mirror errors survive its filename decoration and creates markers. The command applies promotion before this check and honors --force; do not describe this as atomic pre-write refusal or change that separate behavior here.
  • Receipt consumers distinguish valid evidence from success: pipeline_markers.load_plan_audit_receipt returns fail; the generated plan-exit validator accepts either PASS or FAIL as a valid receipt. Preserve this schema and interpretation. A test should show the newly produced FAIL reaches the reader; changing hooks to reject every FAIL is outside this repair.

Authority and documentation must agree. The runtime mapping identifies refusal subjects, including legacy GitHub paths; .gzkit.json and data/vendor-manifest.json describe delivery, not permission to edit an inactive mirror. Skill copying overwrites canonical counterparts, and rule rendering writes derived targets, supporting canonical-edit advice. Keep the existing refusal set. Document ./ equivalence and creation-independent refusal in the two command manpages, whose inspected descriptions omit these checks; clarify the primary specify skill if adding examples and regenerate its distributed mirrors through the existing delivery process. Verify published examples against the shared checker, and distribution parity for changed skill content.

Add table-driven classifier tests for every mapped root: unprefixed/prefixed files, roots with/without trailing slash, and globs. Assert exact canonical results independently of the implementation table. Negative controls include canonical .gzkit paths, .claude/plans, similar-but-distinct prefixes, ordinary missing paths, and valid newly created canonical paths. Existing tests cover unprefixed refusal and creates precedence; retain them.

Independent leads recorded: _path_within_allowed always returns True despite the plan manpage’s scope-check claim; promotion checks occur after mutation. Reconciliation uses separate drift/status semantics, not this mirror classifier. These warrant separate assessment, not automatic expansion. Repeated ./, traversal, custom mirror roots, and alternate separators remain unassessed. Missing governance documents and excluded ADR/history prevent a completeness or ownership claim.

Read inventory: paths below are relative to /tmp/gzkit-remedy-snapshot; some reads were excerpts/search output, and one output was truncated. AGENTS.md; .gzkit.json; data/vendor-manifest.json; .gzkit/skills/{gz-obpi-specify,gz-plan-audit}/SKILL.md; docs/user/manpages/{plan-audit,obpi-validate}.md; src/gzkit/{config,pipeline_runtime,pipeline_markers,sync_skills,sync_surfaces}.py; src/gzkit/governance/{brief_path_validity,brief_reconcile}.py; src/gzkit/commands/{plan_audit_cmd,obpi_cmd,adr_promote,specify_cmd}.py; src/gzkit/rules/__init__.py; src/gzkit/hooks/scripts/{pipeline,routing}.py; tests/governance/{test_brief_path_validity,test_brief_path_validity_wiring}.py; tests/test_plan_audit_cmd.py. Broader rg searches returned additional path/line matches, not implementation reads. Failed reads: docs/governance/{trust-doctrine,state-doctrine,advisory-rules-audit}.md, nonexistent src/gzkit/hooks/scripts/pipeline-gate.py. Also read /tmp/gzkit-remedy-inputs/T02.md. Actual calls: 12 including output writing and word-count verification. No checks, workflows, or source edits executed.