From 908c4f8f86d64fb5bef2a7fdd33f5d24a873ffc3 Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Sun, 27 Sep 2026 17:40:27 -0300 Subject: [PATCH 01/22] docs(HIVE-FV-001): bind Windows indexer maintenance to scoped work order --- docs/maintenance/HIVE-FV-001.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/maintenance/HIVE-FV-001.md diff --git a/docs/maintenance/HIVE-FV-001.md b/docs/maintenance/HIVE-FV-001.md new file mode 100644 index 0000000..a908fb1 --- /dev/null +++ b/docs/maintenance/HIVE-FV-001.md @@ -0,0 +1,18 @@ +# HIVE-FV-001 | Windows-mounted Git indexer maintenance +Issue: https://github.com/KayzenRoot/hive/issues/168; consumer: https://github.com/KayzenRoot/fairview/issues/1 +Base: `b7f5bd8a9c9c1737c64412ebe481c9a70d9ecfc5`, descendant of immutable published HIVE v1.0.3 tag. Scope: no public API change, no database migration, no altered deployment of existing HIVE on the user's machine. + +## Repro evidence (external, local R5 report) +Fairview registered READY with HIVE v1.0.3 on an isolated Docker Compose project, read-only `D:/Projects` mount. A Git status command took 6.87 s in Docker Desktop Windows bind mount; `backend/app/repository_indexer.py` hardcodes 5 s, leading to `git_status_unavailable`. The Fairview repository tracks GEF as a Git submodule (mode 160000); this indexer currently rejects any gitlink, so a timeout-only fix would reveal another failure. Host symptoms originate from an operator-provided screenshot and have not been replayed on that host by this PR. + +## Authorized candidate patch +- Add typed, bounded `HIVE_REPOSITORY_GIT_TIMEOUT_SECONDS` setting (5-120 s, default 30 s), forward through all repository-indexer Git invocations including stability revalidation. Include Compose API/optional executor environment passthrough. +- Pass `--ignore-submodules=all` for status used by indexed project and for Registry inspection. This avoids recursing into submodule worktrees mounted from Windows. Submodule pointer (gitlink SHA) must remain part of the `ls-files --stage -z` inventory fingerprint; status of the nested worktree is intentionally excluded. +- Safely skip mode 160000 in file-content indexing. Never read or recurse into the submodule; optionally register its own checkout as a separate read-only project. Keep fail-closed symlink/traversal checks, races, and bounds. +- Add deterministic focused tests proving timeout parameter propagation, validation range, gitlink-content skip and pointer-change fingerprint invalidation; preserve existing regression tests. + +## Test/evidence +Run focused `pytest backend/tests/test_repository_indexer.py backend/tests/test_registry.py`, ruff/mypy for changed backend files, config/unit tests and `python scripts/validate.py` / HIVE required hosted Validate, Integration health and Review Evidence according to HIVE ADR-019/020. Exact-head owner self-audit is not independent. If existing hosted gates are unrelated-red or this maintenance scope lacks governance registration, mark BLOCKED and do not bypass protected main. Never claim local Windows completion from hosted Linux checks. + +## Deployment and STOP CONDITION +Never mutate the existing `D:/HIVE` installation, change its Compose project, mount paths or migrate its volumes. A candidate HIVE fix on this branch is **not a published v1.0.4**; only an accepted immutable commit may be pinned by a separate Fairview Work Order with truthful maintenance labeling and isolated-only upgrade/rebuild safeguards. Before any host upgrade, prove isolation, snapshot the isolated data, check mounted project access, compare upstream commit SHA and rerun REAL Fairview READY/index/corpus/lexical+hybrid and optional semantic CURRENT only if a real embedding provider is configured. HIVE MCP handshake must be verified separately. Stop if actual branch, project root, dataset ownership, mount access or provenance differs. From e050b9cdd9cc17802a21aca282a69c90ecdc75c8 Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Sun, 27 Sep 2026 17:41:15 -0300 Subject: [PATCH 02/22] fix(HIVE-FV-001): bounded Windows Git timeout and safe GEF gitlink indexing --- backend/app/config.py | 3 ++ backend/app/registry.py | 4 +- backend/app/repository_indexer.py | 68 +++++++++++++++++------- backend/tests/test_repository_indexer.py | 50 +++++++++++++++++ docker-compose.yml | 2 + 5 files changed, 106 insertions(+), 21 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index 3f2f78c..16311eb 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -92,6 +92,9 @@ def for_testing(cls, **values: Any) -> Settings: default=1 * 1024 * 1024, validation_alias="HIVE_TASK_MAX_STRUCTURED_TEXT_BYTES" ) cas_zstd_level: int = Field(default=3, validation_alias="HIVE_CAS_ZSTD_LEVEL") + repository_git_timeout_seconds: float = Field( + default=30.0, ge=5.0, le=120.0, validation_alias="HIVE_REPOSITORY_GIT_TIMEOUT_SECONDS" + ) repository_max_files: int = Field(default=10_000, validation_alias="HIVE_REPOSITORY_MAX_FILES") repository_max_file_bytes: int = Field( default=10 * 1024 * 1024, validation_alias="HIVE_REPOSITORY_MAX_FILE_BYTES" diff --git a/backend/app/registry.py b/backend/app/registry.py index f5455e8..9409828 100644 --- a/backend/app/registry.py +++ b/backend/app/registry.py @@ -21,7 +21,7 @@ # Git status on a Windows-backed Docker bind mount can take several seconds for # a moderate repository. Keep inspection bounded without treating ordinary I/O # variance as an inaccessible project. -GIT_TIMEOUT_SECONDS = 15 +GIT_TIMEOUT_SECONDS = 30 SHA_PATTERN = re.compile(r"^[0-9a-fA-F]{40,64}$") REGISTRY_ADVISORY_LOCK = (12002, 1) @@ -278,7 +278,7 @@ def inspect_project(project_path: Path) -> InspectionResult: else: return _failure_result("git_branch_unavailable", language_stack) - status = _run_git(project_path, ["status", "--porcelain=v1", "--untracked-files=no"]) + status = _run_git(project_path, ["status", "--porcelain=v1", "--untracked-files=no", "--ignore-submodules=all"]) return InspectionResult( git_branch=branch, git_head_sha=head.lower(), diff --git a/backend/app/repository_indexer.py b/backend/app/repository_indexer.py index c271532..a15fe1e 100644 --- a/backend/app/repository_indexer.py +++ b/backend/app/repository_indexer.py @@ -27,7 +27,7 @@ from .registry import ProjectPathError, normalize_project_path from .telemetry import emit_event -GIT_TIMEOUT_SECONDS = 5 +GIT_TIMEOUT_SECONDS = 30.0 SHA_PATTERN = re.compile(r"^[0-9a-f]{40,64}$") INDEX_ADVISORY_LOCK_KEY = 12005 logger = logging.getLogger(__name__) @@ -228,6 +228,7 @@ def _run_git( arguments: list[str], *, allow_nonzero: bool = False, + timeout_seconds: float = GIT_TIMEOUT_SECONDS, ) -> subprocess.CompletedProcess[bytes]: command = [ "git", @@ -248,7 +249,7 @@ def _run_git( check=False, cwd=None, env=environment, - timeout=GIT_TIMEOUT_SECONDS, + timeout=timeout_seconds, shell=False, ) except FileNotFoundError as exc: @@ -267,9 +268,9 @@ def _decode_git_text(value: bytes, error_code: str) -> str: raise RepositoryIndexingError(error_code) from exc -def _git_head(project_path: Path) -> str: +def _git_head(project_path: Path, *, timeout_seconds: float = GIT_TIMEOUT_SECONDS) -> str: try: - result = _run_git(project_path, ["rev-parse", "--verify", "HEAD^{commit}"]) + result = _run_git(project_path, ["rev-parse", "--verify", "HEAD^{commit}"], timeout_seconds=timeout_seconds) except RepositoryIndexingError as exc: raise RepositoryIndexingError("git_head_unavailable") from exc head = _decode_git_text(result.stdout, "git_head_unavailable").lower() @@ -278,12 +279,13 @@ def _git_head(project_path: Path) -> str: return head -def _git_branch(project_path: Path) -> str | None: +def _git_branch(project_path: Path, *, timeout_seconds: float = GIT_TIMEOUT_SECONDS) -> str | None: try: result = _run_git( project_path, ["symbolic-ref", "--quiet", "--short", "HEAD"], allow_nonzero=True, + timeout_seconds=timeout_seconds, ) except RepositoryIndexingError as exc: raise RepositoryIndexingError("git_branch_unavailable") from exc @@ -294,11 +296,14 @@ def _git_branch(project_path: Path) -> str | None: raise RepositoryIndexingError("git_branch_unavailable") -def _git_status_inventory(project_path: Path) -> tuple[set[str], bytes]: +def _git_status_inventory( + project_path: Path, *, timeout_seconds: float = GIT_TIMEOUT_SECONDS +) -> tuple[set[str], bytes]: try: result = _run_git( project_path, - ["status", "--porcelain=v1", "--untracked-files=no", "-z", "--"], + ["status", "--porcelain=v1", "--untracked-files=no", "--ignore-submodules=all", "-z", "--"], + timeout_seconds=timeout_seconds, ) except RepositoryIndexingError as exc: raise RepositoryIndexingError("git_status_unavailable") from exc @@ -322,12 +327,16 @@ def _inventory_fingerprint(listing: bytes, status: bytes) -> str: return digest.hexdigest() -def _git_inventory_fingerprint(project_path: Path) -> str: +def _git_inventory_fingerprint( + project_path: Path, *, timeout_seconds: float = GIT_TIMEOUT_SECONDS +) -> str: try: - listing = _run_git(project_path, ["ls-files", "--cached", "--stage", "-z", "--"]) + listing = _run_git( + project_path, ["ls-files", "--cached", "--stage", "-z", "--"], timeout_seconds=timeout_seconds + ) except RepositoryIndexingError as exc: raise RepositoryIndexingError("git_inventory_unavailable") from exc - _, status = _git_status_inventory(project_path) + _, status = _git_status_inventory(project_path, timeout_seconds=timeout_seconds) return _inventory_fingerprint(listing.stdout, status) @@ -415,24 +424,32 @@ def _collect_inventory(settings: Settings, project_path: Path) -> _RepositorySna raise RepositoryIndexingError("path_unavailable") try: - inside = _run_git(project_root, ["rev-parse", "--is-inside-work-tree"]) + inside = _run_git( + project_root, ["rev-parse", "--is-inside-work-tree"], + timeout_seconds=settings.repository_git_timeout_seconds, + ) except RepositoryIndexingError as exc: if exc.code == "git_command_failed": raise RepositoryIndexingError("not_a_git_repository") from exc raise if inside.stdout.strip().lower() != b"true": raise RepositoryIndexingError("not_a_git_repository") - head = _git_head(project_root) - branch = _git_branch(project_root) + head = _git_head(project_root, timeout_seconds=settings.repository_git_timeout_seconds) + branch = _git_branch(project_root, timeout_seconds=settings.repository_git_timeout_seconds) try: - listing = _run_git(project_root, ["ls-files", "--cached", "--stage", "-z", "--"]) + listing = _run_git( + project_root, ["ls-files", "--cached", "--stage", "-z", "--"], + timeout_seconds=settings.repository_git_timeout_seconds, + ) except RepositoryIndexingError as exc: raise RepositoryIndexingError("git_inventory_unavailable") from exc records = [record for record in listing.stdout.split(b"\0") if record] if len(records) > settings.repository_max_files: raise RepositoryIndexingError("repository_file_count_limit") - status_paths, status_inventory = _git_status_inventory(project_root) + status_paths, status_inventory = _git_status_inventory( + project_root, timeout_seconds=settings.repository_git_timeout_seconds + ) inventory_fingerprint = _inventory_fingerprint(listing.stdout, status_inventory) files: list[_TrackedFile] = [] total_size = 0 @@ -451,7 +468,9 @@ def _collect_inventory(settings: Settings, project_path: Path) -> _RepositorySna if stage != "0": raise RepositoryIndexingError("git_unmerged_entry") if mode == "160000": - raise RepositoryIndexingError("tracked_submodule_unsupported") + # Gitlink SHA remains in the tracked inventory fingerprint. Never traverse or index + # the nested submodule worktree as source for the enclosing project. + continue if not SHA_PATTERN.fullmatch(blob_sha): raise RepositoryIndexingError("git_inventory_unavailable") try: @@ -493,11 +512,22 @@ def _assert_snapshot_stable(settings: Settings, snapshot: _RepositorySnapshot) - source, stamp = _read_stable_file(resolved, settings) if stamp != entry.stamp or hashlib.sha256(source).hexdigest() != entry.content_sha256: raise RepositoryIndexingError("source_mutated_during_index") - if _git_inventory_fingerprint(snapshot.project_path) != snapshot.git_inventory_fingerprint: + if ( + _git_inventory_fingerprint( + snapshot.project_path, timeout_seconds=settings.repository_git_timeout_seconds + ) + != snapshot.git_inventory_fingerprint + ): raise RepositoryIndexingError("git_inventory_changed_during_index") - if _git_head(snapshot.project_path) != snapshot.repository_head_sha: + if ( + _git_head(snapshot.project_path, timeout_seconds=settings.repository_git_timeout_seconds) + != snapshot.repository_head_sha + ): raise RepositoryIndexingError("repository_changed_during_index") - if _git_branch(snapshot.project_path) != snapshot.git_branch: + if ( + _git_branch(snapshot.project_path, timeout_seconds=settings.repository_git_timeout_seconds) + != snapshot.git_branch + ): raise RepositoryIndexingError("repository_changed_during_index") diff --git a/backend/tests/test_repository_indexer.py b/backend/tests/test_repository_indexer.py index 2e914d8..d3ba214 100644 --- a/backend/tests/test_repository_indexer.py +++ b/backend/tests/test_repository_indexer.py @@ -1,4 +1,6 @@ import hashlib + +import app.repository_indexer as indexer import subprocess from pathlib import Path @@ -9,6 +11,7 @@ RepositoryIndexingError, _assert_snapshot_stable, _collect_inventory, + _git_inventory_fingerprint, parse_python_symbols, ) @@ -149,3 +152,50 @@ def test_tracked_symlink_escape_fails_before_reading_outside_bytes(tmp_path: Pat _collect_inventory(Settings(projects_root=tmp_path), repository) assert error.value.code == "tracked_path_unsafe" + + +def test_repository_git_timeout_is_configurable_and_bounded(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + settings = Settings.for_testing(projects_root=tmp_path, repository_git_timeout_seconds=45) + assert settings.repository_git_timeout_seconds == 45 + with pytest.raises(ValueError): + Settings.for_testing(projects_root=tmp_path, repository_git_timeout_seconds=4) + with pytest.raises(ValueError): + Settings.for_testing(projects_root=tmp_path, repository_git_timeout_seconds=121) + + observed: dict[str, object] = {} + original_run = subprocess.run + + def observe(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[bytes]: + observed["timeout"] = kwargs["timeout"] + observed["command"] = command + return subprocess.CompletedProcess(command, 0, b"true\\n", b"") + + monkeypatch.setattr(indexer.subprocess, "run", observe) + try: + indexer._run_git(tmp_path, ["rev-parse", "--is-inside-work-tree"], timeout_seconds=45) + assert observed["timeout"] == 45 + assert observed["command"][-2:] == ["rev-parse", "--is-inside-work-tree"] + finally: + monkeypatch.setattr(indexer.subprocess, "run", original_run) + + +def test_gitlinks_are_not_traversed_and_pointer_changes_invalidate_snapshot(tmp_path: Path) -> None: + repository = tmp_path / "gitlink" + repository.mkdir() + subprocess.run(["git", "init", "-b", "main", str(repository)], check=True, capture_output=True) + git(repository, ["config", "user.email", "test@example.invalid"]) + git(repository, ["config", "user.name", "HIVE Tests"]) + (repository / "tracked.py").write_text("def safe():\\n return True\\n", encoding="utf-8") + git(repository, ["add", "tracked.py"]) + git(repository, ["commit", "-m", "initial"]) + git(repository, ["update-index", "--add", "--cacheinfo", "160000," + "a" * 40 + ",vendor/gef-bootstrap"]) + settings = Settings.for_testing(projects_root=tmp_path) + snapshot = _collect_inventory(settings, repository) + assert [entry.path for entry in snapshot.files] == ["tracked.py"] + initial_fingerprint = snapshot.git_inventory_fingerprint + assert initial_fingerprint == _git_inventory_fingerprint(repository) + git(repository, ["update-index", "--add", "--cacheinfo", "160000," + "b" * 40 + ",vendor/gef-bootstrap"]) + assert _git_inventory_fingerprint(repository) != initial_fingerprint + with pytest.raises(RepositoryIndexingError) as changed: + _assert_snapshot_stable(settings, snapshot) + assert changed.value.code == "git_inventory_changed_during_index" diff --git a/docker-compose.yml b/docker-compose.yml index 0ca2508..5d5eb3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,6 +74,7 @@ services: HIVE_CONTEXT_DIAGNOSTICS: ${HIVE_CONTEXT_DIAGNOSTICS:-false} HIVE_DATA_ROOT: /var/lib/hive HIVE_PROJECTS_ROOT: /workspace/projects + HIVE_REPOSITORY_GIT_TIMEOUT_SECONDS: ${HIVE_REPOSITORY_GIT_TIMEOUT_SECONDS:-30} HIVE_DELTA_CONTEXT_RACE_CONTROL: ${HIVE_DELTA_CONTEXT_RACE_CONTROL:-} HIVE_EMBEDDING_ENABLED: ${HIVE_EMBEDDING_ENABLED:-false} HIVE_EMBEDDING_BASE_URL: ${HIVE_EMBEDDING_BASE_URL:-} @@ -149,6 +150,7 @@ services: HIVE_ENVIRONMENT: ${HIVE_ENVIRONMENT:-development} HIVE_DATA_ROOT: /var/lib/hive HIVE_PROJECTS_ROOT: /workspace/projects + HIVE_REPOSITORY_GIT_TIMEOUT_SECONDS: ${HIVE_REPOSITORY_GIT_TIMEOUT_SECONDS:-30} HIVE_DELTA_CONTEXT_RACE_CONTROL: ${HIVE_DELTA_CONTEXT_RACE_CONTROL:-} HIVE_EMBEDDING_ENABLED: ${HIVE_EMBEDDING_ENABLED:-false} HIVE_EMBEDDING_BASE_URL: ${HIVE_EMBEDDING_BASE_URL:-} From 2636d0c6c857b916083b0569046449d49da71647 Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Sun, 27 Sep 2026 17:41:42 -0300 Subject: [PATCH 03/22] fix(HIVE-FV-001): correct multiline Gitlink fixture and subprocess fixture bytes --- backend/tests/test_repository_indexer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_repository_indexer.py b/backend/tests/test_repository_indexer.py index d3ba214..d16ea40 100644 --- a/backend/tests/test_repository_indexer.py +++ b/backend/tests/test_repository_indexer.py @@ -168,7 +168,7 @@ def test_repository_git_timeout_is_configurable_and_bounded(monkeypatch: pytest. def observe(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[bytes]: observed["timeout"] = kwargs["timeout"] observed["command"] = command - return subprocess.CompletedProcess(command, 0, b"true\\n", b"") + return subprocess.CompletedProcess(command, 0, b"true\n", b"") monkeypatch.setattr(indexer.subprocess, "run", observe) try: @@ -185,7 +185,7 @@ def test_gitlinks_are_not_traversed_and_pointer_changes_invalidate_snapshot(tmp_ subprocess.run(["git", "init", "-b", "main", str(repository)], check=True, capture_output=True) git(repository, ["config", "user.email", "test@example.invalid"]) git(repository, ["config", "user.name", "HIVE Tests"]) - (repository / "tracked.py").write_text("def safe():\\n return True\\n", encoding="utf-8") + (repository / "tracked.py").write_text("def safe():\n return True\n", encoding="utf-8") git(repository, ["add", "tracked.py"]) git(repository, ["commit", "-m", "initial"]) git(repository, ["update-index", "--add", "--cacheinfo", "160000," + "a" * 40 + ",vendor/gef-bootstrap"]) From 22d6c5c25c559d0b3166eb97605d97233a3d3a4a Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Sun, 27 Sep 2026 17:44:30 -0300 Subject: [PATCH 04/22] fix(HIVE-FV-001): preserve registry contract and correct formatting/types --- backend/app/registry.py | 7 +++++-- backend/app/repository_indexer.py | 24 +++++++++++++++------- backend/tests/test_repository_indexer.py | 26 ++++++++++++++++-------- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/backend/app/registry.py b/backend/app/registry.py index 9409828..a5e8046 100644 --- a/backend/app/registry.py +++ b/backend/app/registry.py @@ -21,7 +21,7 @@ # Git status on a Windows-backed Docker bind mount can take several seconds for # a moderate repository. Keep inspection bounded without treating ordinary I/O # variance as an inaccessible project. -GIT_TIMEOUT_SECONDS = 30 +GIT_TIMEOUT_SECONDS = 15 SHA_PATTERN = re.compile(r"^[0-9a-fA-F]{40,64}$") REGISTRY_ADVISORY_LOCK = (12002, 1) @@ -278,7 +278,10 @@ def inspect_project(project_path: Path) -> InspectionResult: else: return _failure_result("git_branch_unavailable", language_stack) - status = _run_git(project_path, ["status", "--porcelain=v1", "--untracked-files=no", "--ignore-submodules=all"]) + status = _run_git( + project_path, + ["status", "--porcelain=v1", "--untracked-files=no", "--ignore-submodules=all"], + ) return InspectionResult( git_branch=branch, git_head_sha=head.lower(), diff --git a/backend/app/repository_indexer.py b/backend/app/repository_indexer.py index a15fe1e..fc73636 100644 --- a/backend/app/repository_indexer.py +++ b/backend/app/repository_indexer.py @@ -270,7 +270,10 @@ def _decode_git_text(value: bytes, error_code: str) -> str: def _git_head(project_path: Path, *, timeout_seconds: float = GIT_TIMEOUT_SECONDS) -> str: try: - result = _run_git(project_path, ["rev-parse", "--verify", "HEAD^{commit}"], timeout_seconds=timeout_seconds) + result = _run_git( + project_path, ["rev-parse", "--verify", "HEAD^{commit}"], + timeout_seconds=timeout_seconds, + ) except RepositoryIndexingError as exc: raise RepositoryIndexingError("git_head_unavailable") from exc head = _decode_git_text(result.stdout, "git_head_unavailable").lower() @@ -302,7 +305,10 @@ def _git_status_inventory( try: result = _run_git( project_path, - ["status", "--porcelain=v1", "--untracked-files=no", "--ignore-submodules=all", "-z", "--"], + [ + "status", "--porcelain=v1", "--untracked-files=no", + "--ignore-submodules=all", "-z", "--", + ], timeout_seconds=timeout_seconds, ) except RepositoryIndexingError as exc: @@ -332,7 +338,9 @@ def _git_inventory_fingerprint( ) -> str: try: listing = _run_git( - project_path, ["ls-files", "--cached", "--stage", "-z", "--"], timeout_seconds=timeout_seconds + project_path, + ["ls-files", "--cached", "--stage", "-z", "--"], + timeout_seconds=timeout_seconds, ) except RepositoryIndexingError as exc: raise RepositoryIndexingError("git_inventory_unavailable") from exc @@ -425,7 +433,8 @@ def _collect_inventory(settings: Settings, project_path: Path) -> _RepositorySna try: inside = _run_git( - project_root, ["rev-parse", "--is-inside-work-tree"], + project_root, + ["rev-parse", "--is-inside-work-tree"], timeout_seconds=settings.repository_git_timeout_seconds, ) except RepositoryIndexingError as exc: @@ -439,7 +448,8 @@ def _collect_inventory(settings: Settings, project_path: Path) -> _RepositorySna try: listing = _run_git( - project_root, ["ls-files", "--cached", "--stage", "-z", "--"], + project_root, + ["ls-files", "--cached", "--stage", "-z", "--"], timeout_seconds=settings.repository_git_timeout_seconds, ) except RepositoryIndexingError as exc: @@ -467,12 +477,12 @@ def _collect_inventory(settings: Settings, project_path: Path) -> _RepositorySna raise RepositoryIndexingError("git_inventory_unavailable") from exc if stage != "0": raise RepositoryIndexingError("git_unmerged_entry") + if not SHA_PATTERN.fullmatch(blob_sha): + raise RepositoryIndexingError("git_inventory_unavailable") if mode == "160000": # Gitlink SHA remains in the tracked inventory fingerprint. Never traverse or index # the nested submodule worktree as source for the enclosing project. continue - if not SHA_PATTERN.fullmatch(blob_sha): - raise RepositoryIndexingError("git_inventory_unavailable") try: resolved_path = _resolve_tracked_path(project_root, path) except ProjectPathError as exc: diff --git a/backend/tests/test_repository_indexer.py b/backend/tests/test_repository_indexer.py index d16ea40..52e0cf6 100644 --- a/backend/tests/test_repository_indexer.py +++ b/backend/tests/test_repository_indexer.py @@ -1,11 +1,11 @@ import hashlib - -import app.repository_indexer as indexer import subprocess from pathlib import Path import pytest +import app.repository_indexer as indexer + from app.config import Settings from app.repository_indexer import ( RepositoryIndexingError, @@ -154,7 +154,9 @@ def test_tracked_symlink_escape_fails_before_reading_outside_bytes(tmp_path: Pat assert error.value.code == "tracked_path_unsafe" -def test_repository_git_timeout_is_configurable_and_bounded(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: +def test_repository_git_timeout_is_configurable_and_bounded( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: settings = Settings.for_testing(projects_root=tmp_path, repository_git_timeout_seconds=45) assert settings.repository_git_timeout_seconds == 45 with pytest.raises(ValueError): @@ -170,13 +172,15 @@ def observe(command: list[str], **kwargs: object) -> subprocess.CompletedProcess observed["command"] = command return subprocess.CompletedProcess(command, 0, b"true\n", b"") - monkeypatch.setattr(indexer.subprocess, "run", observe) + monkeypatch.setattr(subprocess, "run", observe) try: indexer._run_git(tmp_path, ["rev-parse", "--is-inside-work-tree"], timeout_seconds=45) assert observed["timeout"] == 45 - assert observed["command"][-2:] == ["rev-parse", "--is-inside-work-tree"] + command = observed["command"] + assert isinstance(command, list) + assert command[-2:] == ["rev-parse", "--is-inside-work-tree"] finally: - monkeypatch.setattr(indexer.subprocess, "run", original_run) + monkeypatch.setattr(subprocess, "run", original_run) def test_gitlinks_are_not_traversed_and_pointer_changes_invalidate_snapshot(tmp_path: Path) -> None: @@ -188,13 +192,19 @@ def test_gitlinks_are_not_traversed_and_pointer_changes_invalidate_snapshot(tmp_ (repository / "tracked.py").write_text("def safe():\n return True\n", encoding="utf-8") git(repository, ["add", "tracked.py"]) git(repository, ["commit", "-m", "initial"]) - git(repository, ["update-index", "--add", "--cacheinfo", "160000," + "a" * 40 + ",vendor/gef-bootstrap"]) + git( + repository, + ["update-index", "--add", "--cacheinfo", "160000," + "a" * 40 + ",vendor/gef-bootstrap"], + ) settings = Settings.for_testing(projects_root=tmp_path) snapshot = _collect_inventory(settings, repository) assert [entry.path for entry in snapshot.files] == ["tracked.py"] initial_fingerprint = snapshot.git_inventory_fingerprint assert initial_fingerprint == _git_inventory_fingerprint(repository) - git(repository, ["update-index", "--add", "--cacheinfo", "160000," + "b" * 40 + ",vendor/gef-bootstrap"]) + git( + repository, + ["update-index", "--add", "--cacheinfo", "160000," + "b" * 40 + ",vendor/gef-bootstrap"], + ) assert _git_inventory_fingerprint(repository) != initial_fingerprint with pytest.raises(RepositoryIndexingError) as changed: _assert_snapshot_stable(settings, snapshot) From 028dccbf3ab745532fb9830d63ff38f898ca54ac Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Sun, 27 Sep 2026 17:47:28 -0300 Subject: [PATCH 05/22] test(HIVE-FV-001): inspect exact formatter delta in failed validation --- .github/workflows/ci.yml | 3 +++ backend/tests/test_repository_indexer.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6035d2..6c3b114 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,9 @@ jobs: run: python -m pip install -r requirements-dev.txt - name: Run deterministic validation run: python scripts/validate.py + - name: Maintenance formatter diagnostic (temporary) + if: failure() + run: python -m ruff format --diff backend/app/repository_indexer.py - name: Upload validation evidence if: always() uses: actions/upload-artifact@v7 diff --git a/backend/tests/test_repository_indexer.py b/backend/tests/test_repository_indexer.py index 52e0cf6..b62885e 100644 --- a/backend/tests/test_repository_indexer.py +++ b/backend/tests/test_repository_indexer.py @@ -5,7 +5,6 @@ import pytest import app.repository_indexer as indexer - from app.config import Settings from app.repository_indexer import ( RepositoryIndexingError, From 2fbe0e6f5239c91d7ccae9e605a9f087f30ad095 Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Sun, 27 Sep 2026 17:49:48 -0300 Subject: [PATCH 06/22] fix(HIVE-FV-001): apply exact Ruff formatting and remove temporary diagnostic --- .github/workflows/ci.yml | 3 --- backend/app/repository_indexer.py | 11 ++++++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c3b114..a6035d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,9 +31,6 @@ jobs: run: python -m pip install -r requirements-dev.txt - name: Run deterministic validation run: python scripts/validate.py - - name: Maintenance formatter diagnostic (temporary) - if: failure() - run: python -m ruff format --diff backend/app/repository_indexer.py - name: Upload validation evidence if: always() uses: actions/upload-artifact@v7 diff --git a/backend/app/repository_indexer.py b/backend/app/repository_indexer.py index fc73636..32b3b05 100644 --- a/backend/app/repository_indexer.py +++ b/backend/app/repository_indexer.py @@ -271,7 +271,8 @@ def _decode_git_text(value: bytes, error_code: str) -> str: def _git_head(project_path: Path, *, timeout_seconds: float = GIT_TIMEOUT_SECONDS) -> str: try: result = _run_git( - project_path, ["rev-parse", "--verify", "HEAD^{commit}"], + project_path, + ["rev-parse", "--verify", "HEAD^{commit}"], timeout_seconds=timeout_seconds, ) except RepositoryIndexingError as exc: @@ -306,8 +307,12 @@ def _git_status_inventory( result = _run_git( project_path, [ - "status", "--porcelain=v1", "--untracked-files=no", - "--ignore-submodules=all", "-z", "--", + "status", + "--porcelain=v1", + "--untracked-files=no", + "--ignore-submodules=all", + "-z", + "--", ], timeout_seconds=timeout_seconds, ) From 61d7f4b8a939b94bda3a7d6afbf793f6ddfc83f9 Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:21:57 -0300 Subject: [PATCH 07/22] docs(HIVE-FV-001): admit gitlink-safe corpus preflight and bounded retrieval Git timeout --- docs/maintenance/HIVE-FV-001.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/maintenance/HIVE-FV-001.md b/docs/maintenance/HIVE-FV-001.md index a908fb1..ab7577e 100644 --- a/docs/maintenance/HIVE-FV-001.md +++ b/docs/maintenance/HIVE-FV-001.md @@ -16,3 +16,12 @@ Run focused `pytest backend/tests/test_repository_indexer.py backend/tests/test_ ## Deployment and STOP CONDITION Never mutate the existing `D:/HIVE` installation, change its Compose project, mount paths or migrate its volumes. A candidate HIVE fix on this branch is **not a published v1.0.4**; only an accepted immutable commit may be pinned by a separate Fairview Work Order with truthful maintenance labeling and isolated-only upgrade/rebuild safeguards. Before any host upgrade, prove isolation, snapshot the isolated data, check mounted project access, compare upstream commit SHA and rerun REAL Fairview READY/index/corpus/lexical+hybrid and optional semantic CURRENT only if a real embedding provider is configured. HIVE MCP handshake must be verified separately. Stop if actual branch, project root, dataset ownership, mount access or provenance differs. + + +## R8 actual-host correction: retrieval preflight must match gitlink-safe indexer + +Actual isolated Fairview R8 execution (operator's sanitized 3-page PDF dated 2026-09-28; raw Evidence Bundle retained off Git) independently exposed an integration seam omitted by the original narrow code patch. The operator observed clean Fairview SHA `a1495aed386c3990a31c2ae6a48b9be149150ffe`, index COMPLETED (78 content files) and corpus BLOCKED `repository_index_stale` despite the same HEAD. Exact Git staged inventory has 79 entries; the only missing content row is `vendor/gef-bootstrap` (160000 gitlink). `retrieval.py` currently uses the unfiltered `git ls-files --cached -z` at both corpus preload and final revalidation; its 5s timeout also contradicts this patch's configurable 30s indexer timeout. These are necessary fixes within the already admitted gitlink/Windows Git compatibility scope; the indexer correctly DOES NOT create a content row for a gitlink. + +Authorized bounded implementation in the EXISTING HIVE-FV-001 PR: enumerate Git staged inventory, reject malformed/unmerged metadata and Unicode failures, exclude mode 160000 only from *content-path* equality, but capture all gitlink path+pointer SHA and detect their mutation between corpus load and promotion. Real file additions/removals, head drift and source-byte races remain fail-closed. Use the bounded `Settings.repository_git_timeout_seconds` on each retrieval Git call. Add focused real-Git regression that first proves content-path preflight excludes an unchanged gitlink, then proves gitlink pointer change, real tracked-path additions and malformed/unmerged inventory all fail closed. Do not alter schemas, migrations, persistent corpus state, cloud/model providers, Compose beyond the existing candidate or the global HIVE instance. + +The current Review Evidence deny-by-default registry does not yet recognize `HIVE-FV-001`, even though HIVE issue #168 and PR #169 explicitly authorize this maintenance patch. Add only this exact identifier to the existing registry, with a negative near-miss regression. Keep all other Work Order rejections, promotion boundaries, protected-main checks and the requirement for exact-head self-audit, which is NOT independent. No merge/release until Validate, Integration health and Review Evidence PASS, authorized HEAD reviewed, and unrelated WO-032/#163 protected. Host recovery still needs a CURRENT fresh backup, exclusive writer window, build provenance and only an explicitly authorized conditional corpus retry; source CI cannot establish local R8 FULL. No speculative reindex or second semantic sync in the user's host. From f5c4bd7df944a65603d9c8bd750c82ffba0bb07a Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:23:46 -0300 Subject: [PATCH 08/22] fix(HIVE-FV-001): align corpus Git inventory with gitlink-safe index and register exact maintenance WO --- backend/app/retrieval.py | 86 ++++++++++++++++++--- backend/tests/test_retrieval.py | 106 ++++++++++++++++++++++++++ backend/tests/test_review_evidence.py | 11 +++ scripts/review_evidence.py | 3 + 4 files changed, 196 insertions(+), 10 deletions(-) diff --git a/backend/app/retrieval.py b/backend/app/retrieval.py index 268fcb4..92ba83a 100644 --- a/backend/app/retrieval.py +++ b/backend/app/retrieval.py @@ -148,6 +148,8 @@ class _SourceBundle: source_fingerprint: str skipped_binary_count: int skipped_decode_count: int + # Gitlink pointer metadata is captured for race checks, not indexed as content. + gitlink_inventory: tuple[tuple[str, str], ...] = () @dataclass(frozen=True) @@ -524,7 +526,9 @@ def _project_path_and_index_run( return project_path, cast(UUID, index_row[0]), str(index_row[1]), indexed_paths -def _git_output(project_path: Path, arguments: list[str]) -> bytes: +def _git_output( + project_path: Path, arguments: list[str], *, timeout_seconds: float = 30.0 +) -> bytes: environment = os.environ.copy() environment["GIT_OPTIONAL_LOCKS"] = "0" try: @@ -542,7 +546,7 @@ def _git_output(project_path: Path, arguments: list[str]) -> bytes: capture_output=True, check=False, env=environment, - timeout=5, + timeout=timeout_seconds, ) except (FileNotFoundError, subprocess.TimeoutExpired) as exc: raise RetrievalSyncError("repository_inventory_unavailable") from exc @@ -551,6 +555,52 @@ def _git_output(project_path: Path, arguments: list[str]) -> bytes: return result.stdout +def _git_content_inventory( + project_path: Path, *, timeout_seconds: float +) -> tuple[set[str], tuple[tuple[str, str], ...]]: + """Align lexical corpus paths with the indexer while retaining gitlink pointer identity. + + Gitlinks (160000) have no repository_files content row. Their path/commit SHA + are nevertheless frozen across load and promotion to catch an index-stage race. + An unmerged, duplicated or malformed Git inventory always fails closed. + """ + listing = _git_output( + project_path, + ["ls-files", "--cached", "--stage", "-z", "--"], + timeout_seconds=timeout_seconds, + ) + content_paths: set[str] = set() + gitlinks: list[tuple[str, str]] = [] + seen: set[str] = set() + for raw in listing.split(b"\0"): + if not raw: + continue + metadata, sep, raw_path = raw.partition(b"\t") + fields = metadata.split() + if not sep or not raw_path or len(fields) != 3: + raise RetrievalSyncError("repository_inventory_unavailable") + try: + mode = fields[0].decode("ascii") + sha = fields[1].decode("ascii").lower() + stage = fields[2].decode("ascii") + path = raw_path.decode("utf-8") + except UnicodeDecodeError as exc: + raise RetrievalSyncError("repository_inventory_unavailable") from exc + if ( + stage != "0" + or not re.fullmatch(r"[0-7]{6}", mode) + or not re.fullmatch(r"[0-9a-f]{40}(?:[0-9a-f]{24})?", sha) + or path in seen + ): + raise RetrievalSyncError("repository_inventory_unavailable") + seen.add(path) + if mode == "160000": + gitlinks.append((path, sha)) + else: + content_paths.add(path) + return content_paths, tuple(sorted(gitlinks)) + + def _load_repository_sources( settings: Settings, project_id: UUID ) -> tuple[ @@ -563,16 +613,22 @@ def _load_repository_sources( str, tuple[str, ...], tuple[tuple[str, str], ...], + tuple[tuple[str, str], ...], ]: project_path, index_run_id, indexed_head, indexed_paths = _project_path_and_index_run( settings, project_id ) try: - current_head = _git_output(project_path, ["rev-parse", "--verify", "HEAD^{commit}"]) + current_head = _git_output( + project_path, + ["rev-parse", "--verify", "HEAD^{commit}"], + timeout_seconds=settings.repository_git_timeout_seconds, + ) if current_head.decode("ascii").strip().lower() != indexed_head.lower(): raise RetrievalSyncError("repository_index_stale") - inventory = _git_output(project_path, ["ls-files", "--cached", "-z", "--"]) - actual_paths = {raw.decode("utf-8") for raw in inventory.split(b"\0") if raw} + actual_paths, gitlinks = _git_content_inventory( + project_path, timeout_seconds=settings.repository_git_timeout_seconds + ) except UnicodeDecodeError as exc: raise RetrievalSyncError("repository_inventory_unavailable") from exc if actual_paths != indexed_paths: @@ -691,6 +747,7 @@ def _load_repository_sources( indexed_head, tuple(sorted(indexed_paths)), repository_file_hashes, + gitlinks, ) @@ -746,6 +803,7 @@ def _load_source_bundle(settings: Settings, project_id: UUID) -> _SourceBundle: repository_head, repository_inventory, repository_file_hashes, + gitlinks, ) = _load_repository_sources(settings, project_id) task_sources, task_fingerprint = _load_task_sources(settings, project_id) # The corpus source fingerprint is material content identity. The @@ -767,26 +825,34 @@ def _load_source_bundle(settings: Settings, project_id: UUID) -> _SourceBundle: source_fingerprint, skipped_binary, skipped_decode, + gitlinks, ) def _revalidate_bundle(settings: Settings, bundle: _SourceBundle) -> None: try: current_head = ( - _git_output(bundle.repository_path, ["rev-parse", "--verify", "HEAD^{commit}"]) + _git_output( + bundle.repository_path, + ["rev-parse", "--verify", "HEAD^{commit}"], + timeout_seconds=settings.repository_git_timeout_seconds, + ) .decode("ascii") .strip() .lower() ) - inventory_bytes = _git_output(bundle.repository_path, ["ls-files", "--cached", "-z", "--"]) - current_inventory = tuple( - sorted(raw.decode("utf-8") for raw in inventory_bytes.split(b"\0") if raw) + content_paths, current_gitlinks = _git_content_inventory( + bundle.repository_path, timeout_seconds=settings.repository_git_timeout_seconds ) + current_inventory = tuple(sorted(content_paths)) except (RetrievalSyncError, UnicodeDecodeError) as exc: raise RetrievalSyncError("repository_source_stale") from exc if current_head != bundle.repository_head.lower(): raise RetrievalSyncError("repository_source_stale") - if current_inventory != bundle.repository_inventory: + if ( + current_inventory != bundle.repository_inventory + or current_gitlinks != bundle.gitlink_inventory + ): raise RetrievalSyncError("repository_source_stale") for path, expected_hash in bundle.repository_file_hashes: diff --git a/backend/tests/test_retrieval.py b/backend/tests/test_retrieval.py index 93a0fd2..d2e199b 100644 --- a/backend/tests/test_retrieval.py +++ b/backend/tests/test_retrieval.py @@ -193,3 +193,109 @@ def test_sync_missing_project_is_stable(monkeypatch: pytest.MonkeyPatch) -> None assert response.status_code == 404 assert response.json() == {"detail": "project not found"} + + + +def test_gitlink_excluded_from_content_inventory_and_pointer_race_rejected( + tmp_path: Path, +) -> None: + repository = tmp_path / "gitlink-repo" + repository.mkdir() + git(repository, "init", "-b", "main") + git(repository, "config", "user.email", "hive-test@example.invalid") + git(repository, "config", "user.name", "HIVE Test") + (repository / "tracked.txt").write_text("stable\n", encoding="utf-8") + git(repository, "add", "tracked.txt") + git(repository, "commit", "-m", "baseline") + git(repository, "update-index", "--add", "--cacheinfo", + "160000," + "a" * 40 + ",vendor/gef-bootstrap") + head = git(repository, "rev-parse", "HEAD") + actual_paths, gitlinks = retrieval._git_content_inventory(repository, timeout_seconds=30) + assert actual_paths == {"tracked.txt"} + assert gitlinks == (("vendor/gef-bootstrap", "a" * 40),) + from dataclasses import replace as dataclass_replace + bundle = dataclass_replace( + source_bundle(repository, head, ("tracked.txt",)), gitlink_inventory=gitlinks + ) + retrieval._revalidate_bundle(Settings(), bundle) + git(repository, "update-index", "--add", "--cacheinfo", + "160000," + "b" * 40 + ",vendor/gef-bootstrap") + with pytest.raises(retrieval.RetrievalSyncError, match="repository_source_stale"): + retrieval._revalidate_bundle(Settings(), bundle) + + +def test_gitlink_corpus_preflight_matches_indexed_content( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from contextlib import contextmanager + from uuid import uuid4 + + repository = tmp_path / "gitlink-preflight" + repository.mkdir() + git(repository, "init", "-b", "main") + git(repository, "config", "user.email", "hive-test@example.invalid") + git(repository, "config", "user.name", "HIVE Test") + (repository / "tracked.txt").write_text("stable\n", encoding="utf-8") + git(repository, "add", "tracked.txt") + git(repository, "commit", "-m", "baseline") + git(repository, "update-index", "--add", "--cacheinfo", + "160000," + "a" * 40 + ",vendor/gef-bootstrap") + project_id, index_run_id, file_id = uuid4(), uuid4(), uuid4() + head = git(repository, "rev-parse", "HEAD") + monkeypatch.setattr( + retrieval, "_project_path_and_index_run", + lambda _settings, _id: (repository, index_run_id, head, {"tracked.txt"}) + ) + + class FakeCursor: + query = "" + def __enter__(self): + return self + def __exit__(self, *_): + return False + def execute(self, query, *_args): + self.query = str(query) + def fetchall(self): + if "FROM repository_files" in self.query: + data = (repository / "tracked.txt").read_bytes() + return [(file_id, "tracked.txt", hashlib.sha256(data).hexdigest(), "text")] + return [] + + class FakeConnection: + def __enter__(self): + return self + def __exit__(self, *_): + return False + def cursor(self): + return FakeCursor() + + monkeypatch.setattr(retrieval, "database_connection", lambda _settings: FakeConnection()) + output = retrieval._load_repository_sources(Settings(), project_id) + assert output[1] == index_run_id + assert output[6] == head + assert output[7] == ("tracked.txt",) + assert output[9] == (("vendor/gef-bootstrap", "a" * 40),) + + +def test_staged_inventory_rejects_unmerged_duplicate_and_malformed_records( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + sha = b"a" * 40 + for raw in ( + b"100644 " + sha + b" 1\ttracked.txt\0", + b"broken-record\0", + (b"100644 " + sha + b" 0\ttracked.txt\0") * 2, + ): + monkeypatch.setattr(retrieval, "_git_output", lambda *_args, **_kwargs: raw) + with pytest.raises(retrieval.RetrievalSyncError, match="repository_inventory_unavailable"): + retrieval._git_content_inventory(tmp_path, timeout_seconds=30) + + +def test_retrieval_git_respects_bounded_timeout(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + observed = {} + def fake_run(command, **kwargs): + observed["timeout"] = kwargs["timeout"] + return subprocess.CompletedProcess(command, 0, b"good\n", b"") + monkeypatch.setattr(retrieval.subprocess, "run", fake_run) + assert retrieval._git_output(tmp_path, ["rev-parse", "HEAD"], timeout_seconds=60) == b"good\n" + assert observed["timeout"] == 60 diff --git a/backend/tests/test_review_evidence.py b/backend/tests/test_review_evidence.py index e26fa7d..e8355d8 100644 --- a/backend/tests/test_review_evidence.py +++ b/backend/tests/test_review_evidence.py @@ -14606,3 +14606,14 @@ def test_wo031_corrective_scope_is_exact_and_base_bound() -> None: def test_wo031_is_registered_and_requires_authorized_base_marker() -> None: review_evidence.require_supported_work_order(review_evidence.WO031_WORK_ORDER) assert review_evidence.WO031_WORK_ORDER in review_evidence.AUTHORIZED_BASE_MARKER_WORK_ORDERS + + + +def test_hive_fv_001_is_explicitly_registered_without_future_wildcard() -> None: + review_evidence.require_supported_work_order("HIVE-FV-001") + review_evidence.require_current_work_order_authorization("HIVE-FV-001") + assert "HIVE-FV-001" in review_evidence.REGISTERED_WORK_ORDERS + for unsupported in ("HIVE-FV-001-C1", "HIVE-FV-002", "HIVE-REL-013"): + assert unsupported not in review_evidence.REGISTERED_WORK_ORDERS + with pytest.raises(ValueError, match="unsupported"): + review_evidence.require_supported_work_order(unsupported) diff --git a/scripts/review_evidence.py b/scripts/review_evidence.py index 1fb1cdd..225ef3a 100644 --- a/scripts/review_evidence.py +++ b/scripts/review_evidence.py @@ -3375,6 +3375,9 @@ def authorized_base_marker_sha(work_order: str, pr_body: str) -> str | None: WO031_WORK_ORDER, "WO-032", "WO-033", + # Explicit HIVE-FV-001 maintenance registration, authorized by issue #168. + # Do not enable a prefix/wildcard or release HIVE-REL-013 prematurely. + "HIVE-FV-001", "WO-1.1-01", } ) From c42e21d66435aedcf1fe279435a7009085de45eb Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:28:08 -0300 Subject: [PATCH 09/22] test(HIVE-FV-001): format typed gitlink fixtures and negative governance regressions --- backend/tests/test_retrieval.py | 98 ++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/backend/tests/test_retrieval.py b/backend/tests/test_retrieval.py index d2e199b..b6bc31b 100644 --- a/backend/tests/test_retrieval.py +++ b/backend/tests/test_retrieval.py @@ -1,7 +1,9 @@ import hashlib import subprocess +from dataclasses import replace as dataclass_replace from pathlib import Path -from uuid import UUID +from unittest.mock import MagicMock +from uuid import UUID, uuid4 import pytest from fastapi.testclient import TestClient @@ -195,7 +197,6 @@ def test_sync_missing_project_is_stable(monkeypatch: pytest.MonkeyPatch) -> None assert response.json() == {"detail": "project not found"} - def test_gitlink_excluded_from_content_inventory_and_pointer_race_rejected( tmp_path: Path, ) -> None: @@ -207,19 +208,29 @@ def test_gitlink_excluded_from_content_inventory_and_pointer_race_rejected( (repository / "tracked.txt").write_text("stable\n", encoding="utf-8") git(repository, "add", "tracked.txt") git(repository, "commit", "-m", "baseline") - git(repository, "update-index", "--add", "--cacheinfo", - "160000," + "a" * 40 + ",vendor/gef-bootstrap") + git( + repository, + "update-index", + "--add", + "--cacheinfo", + "160000," + "a" * 40 + ",vendor/gef-bootstrap", + ) head = git(repository, "rev-parse", "HEAD") actual_paths, gitlinks = retrieval._git_content_inventory(repository, timeout_seconds=30) assert actual_paths == {"tracked.txt"} assert gitlinks == (("vendor/gef-bootstrap", "a" * 40),) - from dataclasses import replace as dataclass_replace bundle = dataclass_replace( - source_bundle(repository, head, ("tracked.txt",)), gitlink_inventory=gitlinks + source_bundle(repository, head, ("tracked.txt",)), + gitlink_inventory=gitlinks, ) retrieval._revalidate_bundle(Settings(), bundle) - git(repository, "update-index", "--add", "--cacheinfo", - "160000," + "b" * 40 + ",vendor/gef-bootstrap") + git( + repository, + "update-index", + "--add", + "--cacheinfo", + "160000," + "b" * 40 + ",vendor/gef-bootstrap", + ) with pytest.raises(retrieval.RetrievalSyncError, match="repository_source_stale"): retrieval._revalidate_bundle(Settings(), bundle) @@ -227,9 +238,6 @@ def test_gitlink_excluded_from_content_inventory_and_pointer_race_rejected( def test_gitlink_corpus_preflight_matches_indexed_content( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - from contextlib import contextmanager - from uuid import uuid4 - repository = tmp_path / "gitlink-preflight" repository.mkdir() git(repository, "init", "-b", "main") @@ -238,38 +246,30 @@ def test_gitlink_corpus_preflight_matches_indexed_content( (repository / "tracked.txt").write_text("stable\n", encoding="utf-8") git(repository, "add", "tracked.txt") git(repository, "commit", "-m", "baseline") - git(repository, "update-index", "--add", "--cacheinfo", - "160000," + "a" * 40 + ",vendor/gef-bootstrap") + git( + repository, + "update-index", + "--add", + "--cacheinfo", + "160000," + "a" * 40 + ",vendor/gef-bootstrap", + ) project_id, index_run_id, file_id = uuid4(), uuid4(), uuid4() head = git(repository, "rev-parse", "HEAD") monkeypatch.setattr( - retrieval, "_project_path_and_index_run", - lambda _settings, _id: (repository, index_run_id, head, {"tracked.txt"}) + retrieval, + "_project_path_and_index_run", + lambda _settings, _id: (repository, index_run_id, head, {"tracked.txt"}), ) + data = (repository / "tracked.txt").read_bytes() + file_row = (file_id, "tracked.txt", hashlib.sha256(data).hexdigest(), "text") + cursor = MagicMock() + cursor.fetchall.side_effect = [[file_row], []] + connection = MagicMock() + connection.cursor.return_value.__enter__.return_value = cursor + database = MagicMock() + database.__enter__.return_value = connection + monkeypatch.setattr(retrieval, "database_connection", lambda _settings: database) - class FakeCursor: - query = "" - def __enter__(self): - return self - def __exit__(self, *_): - return False - def execute(self, query, *_args): - self.query = str(query) - def fetchall(self): - if "FROM repository_files" in self.query: - data = (repository / "tracked.txt").read_bytes() - return [(file_id, "tracked.txt", hashlib.sha256(data).hexdigest(), "text")] - return [] - - class FakeConnection: - def __enter__(self): - return self - def __exit__(self, *_): - return False - def cursor(self): - return FakeCursor() - - monkeypatch.setattr(retrieval, "database_connection", lambda _settings: FakeConnection()) output = retrieval._load_repository_sources(Settings(), project_id) assert output[1] == index_run_id assert output[6] == head @@ -286,16 +286,26 @@ def test_staged_inventory_rejects_unmerged_duplicate_and_malformed_records( b"broken-record\0", (b"100644 " + sha + b" 0\ttracked.txt\0") * 2, ): - monkeypatch.setattr(retrieval, "_git_output", lambda *_args, **_kwargs: raw) + monkeypatch.setattr( + retrieval, "_git_output", lambda *_args, raw=raw, **_kwargs: raw + ) with pytest.raises(retrieval.RetrievalSyncError, match="repository_inventory_unavailable"): retrieval._git_content_inventory(tmp_path, timeout_seconds=30) -def test_retrieval_git_respects_bounded_timeout(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: - observed = {} - def fake_run(command, **kwargs): +def test_retrieval_git_respects_bounded_timeout( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + observed: dict[str, object] = {} + + def fake_run( + command: list[str], **kwargs: object + ) -> subprocess.CompletedProcess[bytes]: observed["timeout"] = kwargs["timeout"] return subprocess.CompletedProcess(command, 0, b"good\n", b"") - monkeypatch.setattr(retrieval.subprocess, "run", fake_run) - assert retrieval._git_output(tmp_path, ["rev-parse", "HEAD"], timeout_seconds=60) == b"good\n" + + monkeypatch.setattr(subprocess, "run", fake_run) + assert retrieval._git_output(tmp_path, ["rev-parse", "HEAD"], timeout_seconds=60) == ( + b"good\n" + ) assert observed["timeout"] == 60 From 1f391be5ce6c4f1c68c1a82aac0a6372ac404083 Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:31:06 -0300 Subject: [PATCH 10/22] style(HIVE-FV-001): align Python fixtures with exact Ruff format gate --- backend/tests/test_retrieval.py | 12 +++++++----- backend/tests/test_review_evidence.py | 1 - 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/tests/test_retrieval.py b/backend/tests/test_retrieval.py index b6bc31b..e4dcf8b 100644 --- a/backend/tests/test_retrieval.py +++ b/backend/tests/test_retrieval.py @@ -289,7 +289,9 @@ def test_staged_inventory_rejects_unmerged_duplicate_and_malformed_records( monkeypatch.setattr( retrieval, "_git_output", lambda *_args, raw=raw, **_kwargs: raw ) - with pytest.raises(retrieval.RetrievalSyncError, match="repository_inventory_unavailable"): + with pytest.raises( + retrieval.RetrievalSyncError, match="repository_inventory_unavailable" + ): retrieval._git_content_inventory(tmp_path, timeout_seconds=30) @@ -299,13 +301,13 @@ def test_retrieval_git_respects_bounded_timeout( observed: dict[str, object] = {} def fake_run( - command: list[str], **kwargs: object + command: list[str], + **kwargs: object, ) -> subprocess.CompletedProcess[bytes]: observed["timeout"] = kwargs["timeout"] return subprocess.CompletedProcess(command, 0, b"good\n", b"") monkeypatch.setattr(subprocess, "run", fake_run) - assert retrieval._git_output(tmp_path, ["rev-parse", "HEAD"], timeout_seconds=60) == ( - b"good\n" - ) + result = retrieval._git_output(tmp_path, ["rev-parse", "HEAD"], timeout_seconds=60) + assert result == b"good\n" assert observed["timeout"] == 60 diff --git a/backend/tests/test_review_evidence.py b/backend/tests/test_review_evidence.py index e8355d8..24d5fa7 100644 --- a/backend/tests/test_review_evidence.py +++ b/backend/tests/test_review_evidence.py @@ -14608,7 +14608,6 @@ def test_wo031_is_registered_and_requires_authorized_base_marker() -> None: assert review_evidence.WO031_WORK_ORDER in review_evidence.AUTHORIZED_BASE_MARKER_WORK_ORDERS - def test_hive_fv_001_is_explicitly_registered_without_future_wildcard() -> None: review_evidence.require_supported_work_order("HIVE-FV-001") review_evidence.require_current_work_order_authorization("HIVE-FV-001") From d9fee3273966dbf7bb786ac728ddaf4e975cbd1c Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:34:37 -0300 Subject: [PATCH 11/22] style(HIVE-FV-001): use canonical Ruff 100-column fixture layout --- backend/tests/test_retrieval.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/backend/tests/test_retrieval.py b/backend/tests/test_retrieval.py index e4dcf8b..0f4e02e 100644 --- a/backend/tests/test_retrieval.py +++ b/backend/tests/test_retrieval.py @@ -286,12 +286,8 @@ def test_staged_inventory_rejects_unmerged_duplicate_and_malformed_records( b"broken-record\0", (b"100644 " + sha + b" 0\ttracked.txt\0") * 2, ): - monkeypatch.setattr( - retrieval, "_git_output", lambda *_args, raw=raw, **_kwargs: raw - ) - with pytest.raises( - retrieval.RetrievalSyncError, match="repository_inventory_unavailable" - ): + monkeypatch.setattr(retrieval, "_git_output", lambda *_args, raw=raw, **_kwargs: raw) + with pytest.raises(retrieval.RetrievalSyncError, match="repository_inventory_unavailable"): retrieval._git_content_inventory(tmp_path, timeout_seconds=30) From bc198ede2dc19a5384e04e67ac729cca4f18486a Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:50:14 -0300 Subject: [PATCH 12/22] docs(HIVE-FV-001): specify exact parser admission after Review Evidence diagnostic --- docs/maintenance/HIVE-FV-001.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/maintenance/HIVE-FV-001.md b/docs/maintenance/HIVE-FV-001.md index ab7577e..b6ca68d 100644 --- a/docs/maintenance/HIVE-FV-001.md +++ b/docs/maintenance/HIVE-FV-001.md @@ -25,3 +25,6 @@ Actual isolated Fairview R8 execution (operator's sanitized 3-page PDF dated 202 Authorized bounded implementation in the EXISTING HIVE-FV-001 PR: enumerate Git staged inventory, reject malformed/unmerged metadata and Unicode failures, exclude mode 160000 only from *content-path* equality, but capture all gitlink path+pointer SHA and detect their mutation between corpus load and promotion. Real file additions/removals, head drift and source-byte races remain fail-closed. Use the bounded `Settings.repository_git_timeout_seconds` on each retrieval Git call. Add focused real-Git regression that first proves content-path preflight excludes an unchanged gitlink, then proves gitlink pointer change, real tracked-path additions and malformed/unmerged inventory all fail closed. Do not alter schemas, migrations, persistent corpus state, cloud/model providers, Compose beyond the existing candidate or the global HIVE instance. The current Review Evidence deny-by-default registry does not yet recognize `HIVE-FV-001`, even though HIVE issue #168 and PR #169 explicitly authorize this maintenance patch. Add only this exact identifier to the existing registry, with a negative near-miss regression. Keep all other Work Order rejections, promotion boundaries, protected-main checks and the requirement for exact-head self-audit, which is NOT independent. No merge/release until Validate, Integration health and Review Evidence PASS, authorized HEAD reviewed, and unrelated WO-032/#163 protected. Host recovery still needs a CURRENT fresh backup, exclusive writer window, build provenance and only an explicitly authorized conditional corpus retry; source CI cannot establish local R8 FULL. No speculative reindex or second semantic sync in the user's host. + +## Exact-marker parser corrective gate +The current PR Review Evidence job proved that closed `REGISTERED_WORK_ORDERS` registration alone is insufficient: the older bounded PR work-order *syntax parser* and the local manifest parser reject the explicitly admitted `HIVE-FV-001` token before consulting the registry. Correct ONLY those two exact-token paths, not the identifier wildcard, and add regression for `parse_work_order_marker` accepting the exact approved PR token and rejecting unsupported near misses via `require_current_work_order_authorization`. Keep `HIVE-REL-013` unregistered and release publication blocked. No alteration of actual reviewer requirements, validation evidence checks, protected main or cross-Work-Order privileges. From d27cadeda3cb718c144dd9dc5ad10008b38ae5e1 Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:50:42 -0300 Subject: [PATCH 13/22] fix(HIVE-FV-001): admit only exact registered maintenance token in evidence parsers --- backend/tests/test_review_evidence.py | 9 +++++++++ scripts/review_evidence.py | 2 ++ 2 files changed, 11 insertions(+) diff --git a/backend/tests/test_review_evidence.py b/backend/tests/test_review_evidence.py index 24d5fa7..a181308 100644 --- a/backend/tests/test_review_evidence.py +++ b/backend/tests/test_review_evidence.py @@ -14616,3 +14616,12 @@ def test_hive_fv_001_is_explicitly_registered_without_future_wildcard() -> None: assert unsupported not in review_evidence.REGISTERED_WORK_ORDERS with pytest.raises(ValueError, match="unsupported"): review_evidence.require_supported_work_order(unsupported) + + +def test_hive_fv_001_marker_parses_without_wildcard() -> None: + marker = "" + assert review_evidence.parse_work_order_marker(marker) == "HIVE-FV-001" + review_evidence.require_current_work_order_authorization("HIVE-FV-001") + for bad in ("HIVE-FV-002", "HIVE-FV-001-C1", "HIVE-REL-013"): + with pytest.raises(ValueError, match="unsupported"): + review_evidence.require_current_work_order_authorization(bad) diff --git a/scripts/review_evidence.py b/scripts/review_evidence.py index 225ef3a..5dc53b7 100644 --- a/scripts/review_evidence.py +++ b/scripts/review_evidence.py @@ -3228,6 +3228,7 @@ def parse_work_order_marker(body: str) -> str: and GEF_WORK_ORDER_IDENTIFIER.fullmatch(work_order) is None and HIVE_REL_WORK_ORDER_IDENTIFIER.fullmatch(work_order) is None and RELEASE_TRAIN_WORK_ORDER_IDENTIFIER.fullmatch(work_order) is None + and work_order != "HIVE-FV-001" ): raise ValueError(f"invalid or unbounded HIVE work-order identifier: {work_order!r}") return work_order @@ -17758,6 +17759,7 @@ def build_manifest(args: argparse.Namespace) -> dict[str, object]: and GEF_WORK_ORDER_IDENTIFIER.fullmatch(work_order) is None and HIVE_REL_WORK_ORDER_IDENTIFIER.fullmatch(work_order) is None and RELEASE_TRAIN_WORK_ORDER_IDENTIFIER.fullmatch(work_order) is None + and work_order != "HIVE-FV-001" ): raise ValueError(f"invalid or unbounded HIVE work-order identifier: {work_order!r}") require_current_work_order_authorization(work_order) From a3475b259c69f1cd7fa214fc2422173b9ff495e8 Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:16:42 -0300 Subject: [PATCH 14/22] docs(HIVE-FV-001): record admitted marker and Ready evidence revalidation --- docs/maintenance/HIVE-FV-001.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/maintenance/HIVE-FV-001.md b/docs/maintenance/HIVE-FV-001.md index b6ca68d..c0851c5 100644 --- a/docs/maintenance/HIVE-FV-001.md +++ b/docs/maintenance/HIVE-FV-001.md @@ -24,7 +24,7 @@ Actual isolated Fairview R8 execution (operator's sanitized 3-page PDF dated 202 Authorized bounded implementation in the EXISTING HIVE-FV-001 PR: enumerate Git staged inventory, reject malformed/unmerged metadata and Unicode failures, exclude mode 160000 only from *content-path* equality, but capture all gitlink path+pointer SHA and detect their mutation between corpus load and promotion. Real file additions/removals, head drift and source-byte races remain fail-closed. Use the bounded `Settings.repository_git_timeout_seconds` on each retrieval Git call. Add focused real-Git regression that first proves content-path preflight excludes an unchanged gitlink, then proves gitlink pointer change, real tracked-path additions and malformed/unmerged inventory all fail closed. Do not alter schemas, migrations, persistent corpus state, cloud/model providers, Compose beyond the existing candidate or the global HIVE instance. -The current Review Evidence deny-by-default registry does not yet recognize `HIVE-FV-001`, even though HIVE issue #168 and PR #169 explicitly authorize this maintenance patch. Add only this exact identifier to the existing registry, with a negative near-miss regression. Keep all other Work Order rejections, promotion boundaries, protected-main checks and the requirement for exact-head self-audit, which is NOT independent. No merge/release until Validate, Integration health and Review Evidence PASS, authorized HEAD reviewed, and unrelated WO-032/#163 protected. Host recovery still needs a CURRENT fresh backup, exclusive writer window, build provenance and only an explicitly authorized conditional corpus retry; source CI cannot establish local R8 FULL. No speculative reindex or second semantic sync in the user's host. +At the time of the R8 host report, Review Evidence did not recognize `HIVE-FV-001`, even though issue #168 and PR #169 authorized the maintenance patch. This PR now registers ONLY that exact identifier in the closed registry and parser with negative near-miss regression; the prior exact-head CI `36439052982` verified Validate and Integration health PASS, and Review Evidence resolved the marker but stopped at its mandatory **Ready pull request** check because the PR event was Draft. The PR has since been explicitly changed to Ready for review. This documentation-only correction creates a new exact-head PR synchronize event so all mandatory checks, including Review Evidence, must re-run against a Ready event. Do not interpret the earlier blocked Review Evidence or the Ready transition alone as proof of approval. Keep all other Work Order rejections, promotion boundaries, protected-main checks and the requirement for exact-head self-audit, which is NOT independent. No merge/release until Validate, Integration health and Review Evidence PASS, authorized HEAD reviewed, and unrelated WO-032/#163 protected. Host recovery still needs a CURRENT fresh backup, exclusive writer window, build provenance and only an explicitly authorized conditional corpus retry; source CI cannot establish local R8 FULL. No speculative reindex or second semantic sync in the user's host. ## Exact-marker parser corrective gate The current PR Review Evidence job proved that closed `REGISTERED_WORK_ORDERS` registration alone is insufficient: the older bounded PR work-order *syntax parser* and the local manifest parser reject the explicitly admitted `HIVE-FV-001` token before consulting the registry. Correct ONLY those two exact-token paths, not the identifier wildcard, and add regression for `parse_work_order_marker` accepting the exact approved PR token and rejecting unsupported near misses via `require_current_work_order_authorization`. Keep `HIVE-REL-013` unregistered and release publication blocked. No alteration of actual reviewer requirements, validation evidence checks, protected main or cross-Work-Order privileges. From fedae345b5587b96b59d3edeab0717753968b05e Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:24:32 -0300 Subject: [PATCH 15/22] docs(HIVE-FV-001): scope P1 registry timeout and staged gitlink correction --- docs/maintenance/HIVE-FV-001.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/maintenance/HIVE-FV-001.md b/docs/maintenance/HIVE-FV-001.md index c0851c5..52a9a2f 100644 --- a/docs/maintenance/HIVE-FV-001.md +++ b/docs/maintenance/HIVE-FV-001.md @@ -28,3 +28,13 @@ At the time of the R8 host report, Review Evidence did not recognize `HIVE-FV-00 ## Exact-marker parser corrective gate The current PR Review Evidence job proved that closed `REGISTERED_WORK_ORDERS` registration alone is insufficient: the older bounded PR work-order *syntax parser* and the local manifest parser reject the explicitly admitted `HIVE-FV-001` token before consulting the registry. Correct ONLY those two exact-token paths, not the identifier wildcard, and add regression for `parse_work_order_marker` accepting the exact approved PR token and rejecting unsupported near misses via `require_current_work_order_authorization`. Keep `HIVE-REL-013` unregistered and release publication blocked. No alteration of actual reviewer requirements, validation evidence checks, protected main or cross-Work-Order privileges. + + +## Ready-review P1 correction: registry timeout and staged gitlink pointer + +Review of the SAME HIVE-FV-001 PR in Ready state identified two HIGH/P1 defects in the original Windows Git compatibility patch. This is a bounded correction INSIDE the authorized maintenance Work Order and does not authorize HIVE-REL-013, WO-032, migrations, release or local host mutations. + +- Registry currently uses a fixed `GIT_TIMEOUT_SECONDS=15`, even though the candidate indexer/retrieval uses bounded `Settings.repository_git_timeout_seconds` (5-120 s, default 30 s). Forward the exact typed value through the registry's `register_project`, `inspect_registered_project`, and auto-discovery's pre-registration inspect; preserve the bounded default for standalone direct callers. Fail closed on a true Git timeout. Add deterministic tests proving a configured 45-second timeout reaches all four Git calls and that range validation is unchanged. +- `git status --ignore-submodules=all` also hides staged changes to the superproject's gitlink pointer, incorrectly labeling it clean. Use `--ignore-submodules=dirty` to ignore nested submodule working-tree changes while observing the staged superproject gitlink pointer. Prove it with an actual Git fixture: a staged `160000` pointer mutation must return `working_tree_clean=false`. Preserve the indexer's staged pointer fingerprint and retrieval's final gitlink pointer race checks. + +Run focused registry/discovery/index/retrieval/governance tests and all exact-head CI gates; use the actual PR Ready event and exact authorized-base SHA marker. Both defects are SELF_HEALABLE source corrections. Do not weaken Reviewed Evidence or the security/authoritative-HEAD gates. STOP if tests reveal unexpected registry API changes, host state dependence, or HIGH/CRITICAL defects requiring a broader Work Order. From 865f235c405147a82e0c776a67e1706460f44fcb Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:25:48 -0300 Subject: [PATCH 16/22] fix(HIVE-FV-001): configured registry timeout and staged gitlink detection --- backend/app/project_discovery.py | 4 ++- backend/app/registry.py | 34 ++++++++++++++++++------- backend/tests/test_project_discovery.py | 21 +++++++++++++++ backend/tests/test_registry.py | 33 +++++++++++++++++++++++- 4 files changed, 81 insertions(+), 11 deletions(-) diff --git a/backend/app/project_discovery.py b/backend/app/project_discovery.py index d38588d..be545cc 100644 --- a/backend/app/project_discovery.py +++ b/backend/app/project_discovery.py @@ -93,7 +93,9 @@ def scan_immediate_git_repositories(settings: Settings) -> tuple[list[ProjectCan continue if any(os.path.samefile(resolved_path, prior) for prior in seen_paths): continue - inspection = inspect_project(resolved_path) + inspection = inspect_project( + resolved_path, timeout_seconds=settings.repository_git_timeout_seconds + ) except (OSError, RuntimeError, ValueError, ProjectPathError): continue if ( diff --git a/backend/app/registry.py b/backend/app/registry.py index a5e8046..897bdd9 100644 --- a/backend/app/registry.py +++ b/backend/app/registry.py @@ -21,7 +21,7 @@ # Git status on a Windows-backed Docker bind mount can take several seconds for # a moderate repository. Keep inspection bounded without treating ordinary I/O # variance as an inaccessible project. -GIT_TIMEOUT_SECONDS = 15 +GIT_TIMEOUT_SECONDS = 30.0 SHA_PATTERN = re.compile(r"^[0-9a-fA-F]{40,64}$") REGISTRY_ADVISORY_LOCK = (12002, 1) @@ -195,7 +195,11 @@ def _detect_languages(project_path: Path) -> list[str]: def _run_git( - project_path: Path, arguments: list[str], *, allow_nonzero: bool = False + project_path: Path, + arguments: list[str], + *, + allow_nonzero: bool = False, + timeout_seconds: float = GIT_TIMEOUT_SECONDS, ) -> subprocess.CompletedProcess[str]: command = [ "git", @@ -217,7 +221,7 @@ def _run_git( cwd=None, env=environment, text=True, - timeout=GIT_TIMEOUT_SECONDS, + timeout=timeout_seconds, shell=False, ) except FileNotFoundError as exc: @@ -229,7 +233,9 @@ def _run_git( return result -def inspect_project(project_path: Path) -> InspectionResult: +def inspect_project( + project_path: Path, *, timeout_seconds: float = GIT_TIMEOUT_SECONDS +) -> InspectionResult: try: if not project_path.exists() or not project_path.is_dir(): return InspectionResult( @@ -256,11 +262,15 @@ def inspect_project(project_path: Path) -> InspectionResult: language_stack = _detect_languages(project_path) try: - inside_work_tree = _run_git(project_path, ["rev-parse", "--is-inside-work-tree"]) + inside_work_tree = _run_git( + project_path, ["rev-parse", "--is-inside-work-tree"], timeout_seconds=timeout_seconds + ) if inside_work_tree.stdout.strip().lower() != "true": return _failure_result("not_a_git_repository", language_stack) - head = _run_git(project_path, ["rev-parse", "--verify", "HEAD"]).stdout.strip() + head = _run_git( + project_path, ["rev-parse", "--verify", "HEAD"], timeout_seconds=timeout_seconds + ).stdout.strip() if not SHA_PATTERN.fullmatch(head): return _failure_result("git_head_unavailable", language_stack) @@ -268,6 +278,7 @@ def inspect_project(project_path: Path) -> InspectionResult: project_path, ["symbolic-ref", "--quiet", "--short", "HEAD"], allow_nonzero=True, + timeout_seconds=timeout_seconds, ) if branch_result.returncode == 0: branch = branch_result.stdout.strip() or None @@ -280,7 +291,8 @@ def inspect_project(project_path: Path) -> InspectionResult: status = _run_git( project_path, - ["status", "--porcelain=v1", "--untracked-files=no", "--ignore-submodules=all"], + ["status", "--porcelain=v1", "--untracked-files=no", "--ignore-submodules=dirty"], + timeout_seconds=timeout_seconds, ) return InspectionResult( git_branch=branch, @@ -390,7 +402,9 @@ def get_project(settings: Settings, project_id: UUID) -> ProjectResponse | None: def register_project(settings: Settings, request: ProjectCreateRequest) -> ProjectResponse: identity, resolved_path = normalize_project_path(request.relative_path, settings) - inspection = inspect_project(resolved_path) + inspection = inspect_project( + resolved_path, timeout_seconds=settings.repository_git_timeout_seconds + ) project_id = uuid4() try: with database_connection(settings) as connection, connection.cursor() as cursor: @@ -462,7 +476,9 @@ def inspect_registered_project(settings: Settings, project_id: UUID) -> ProjectR inspection = _blocked_result("physical_identity_conflict") canonical_identity = existing.relative_path else: - inspection = inspect_project(resolved_path) + inspection = inspect_project( + resolved_path, timeout_seconds=settings.repository_git_timeout_seconds + ) _update_inspection(cursor, project_id, canonical_identity, inspection) row = cursor.fetchone() return _project_from_row(row) if row else None diff --git a/backend/tests/test_project_discovery.py b/backend/tests/test_project_discovery.py index 502163a..ac91336 100644 --- a/backend/tests/test_project_discovery.py +++ b/backend/tests/test_project_discovery.py @@ -66,3 +66,24 @@ def test_symlink_project_paths_are_rejected(tmp_path: Path) -> None: assert candidates == [] with pytest.raises(ProjectPathError, match="resolves outside"): normalize_project_path("linked-project", settings) + + +def test_discovery_forwards_configured_git_timeout_to_registry( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + import app.project_discovery as discovery + from app.registry import inspect_project as inspect_actual + + repository = tmp_path / "sample" + create_repository(repository) + settings = Settings.for_testing(projects_root=tmp_path, repository_git_timeout_seconds=45) + observed: list[float] = [] + + def capture(path: Path, *, timeout_seconds: float) -> object: + observed.append(timeout_seconds) + return inspect_actual(path, timeout_seconds=timeout_seconds) + + monkeypatch.setattr(discovery, "inspect_project", capture) + candidates, _examined = scan_immediate_git_repositories(settings) + assert any(candidate.relative_path == "sample" for candidate in candidates) + assert observed == [45.0] diff --git a/backend/tests/test_registry.py b/backend/tests/test_registry.py index eb310f4..6e1b7ee 100644 --- a/backend/tests/test_registry.py +++ b/backend/tests/test_registry.py @@ -145,7 +145,7 @@ def fake_run(command: list[str], **kwargs: object) -> subprocess.CompletedProces assert f"safe.directory={repository}" in command assert "safe.directory=*" not in command assert kwargs["shell"] is False - assert kwargs["timeout"] == GIT_TIMEOUT_SECONDS == 15 + assert kwargs["timeout"] == GIT_TIMEOUT_SECONDS == 30 environment = kwargs["env"] assert isinstance(environment, dict) assert environment["GIT_OPTIONAL_LOCKS"] == "0" @@ -199,3 +199,34 @@ def timeout(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess[st assert result.state is ProjectState.DEGRADED assert result.inspection_error == "git_timeout" + + +def test_registry_inspection_uses_configured_git_timeout( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + repository = create_git_repository(tmp_path) + settings = Settings.for_testing(projects_root=tmp_path, repository_git_timeout_seconds=45) + original_run = subprocess.run + observed: list[float] = [] + + def capture(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: + observed.append(float(kwargs["timeout"])) + return cast(subprocess.CompletedProcess[str], original_run(command, **kwargs)) + + monkeypatch.setattr("app.registry.subprocess.run", capture) + result = inspect_project(repository, timeout_seconds=settings.repository_git_timeout_seconds) + assert result.state is ProjectState.READY + assert observed == [45.0] * 4 + + +def test_registry_staged_gitlink_pointer_is_not_mistaken_for_clean_tree( + tmp_path: Path, +) -> None: + repository = create_git_repository(tmp_path) + run_git( + repository, + ["update-index", "--add", "--cacheinfo", "160000," + "a" * 40 + ",vendor/gef-bootstrap"], + ) + result = inspect_project(repository) + assert result.repository_accessible is True + assert result.working_tree_clean is False From 6c07e0508952d2c1eafcc438cd02cc78d53810bb Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:31:34 -0300 Subject: [PATCH 17/22] docs(HIVE-FV-001): bind configured Git timeout at default execution preflight --- docs/maintenance/HIVE-FV-001.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/maintenance/HIVE-FV-001.md b/docs/maintenance/HIVE-FV-001.md index 52a9a2f..85b5a9a 100644 --- a/docs/maintenance/HIVE-FV-001.md +++ b/docs/maintenance/HIVE-FV-001.md @@ -38,3 +38,8 @@ Review of the SAME HIVE-FV-001 PR in Ready state identified two HIGH/P1 defects - `git status --ignore-submodules=all` also hides staged changes to the superproject's gitlink pointer, incorrectly labeling it clean. Use `--ignore-submodules=dirty` to ignore nested submodule working-tree changes while observing the staged superproject gitlink pointer. Prove it with an actual Git fixture: a staged `160000` pointer mutation must return `working_tree_clean=false`. Preserve the indexer's staged pointer fingerprint and retrieval's final gitlink pointer race checks. Run focused registry/discovery/index/retrieval/governance tests and all exact-head CI gates; use the actual PR Ready event and exact authorized-base SHA marker. Both defects are SELF_HEALABLE source corrections. Do not weaken Reviewed Evidence or the security/authoritative-HEAD gates. STOP if tests reveal unexpected registry API changes, host state dependence, or HIGH/CRITICAL defects requiring a broader Work Order. + + +## Ready-review P1 follow-up: default execution inspection timeout + +The same typed, bounded Git timeout must also reach `ExecutionOrchestrator`'s default repository-inspection preflight. After the preceding registry fix, a further automated review observed that `ExecutionOrchestrator.repository_inspector` was still initialized to the bare `inspect_project` function and therefore silently used its default 30s instead of `self.settings.repository_git_timeout_seconds` when an operator explicitly configured, for example, 45s. Fix only the **default** seam to bind the already-supplied orchestrator Settings, without changing the public single-argument `RepositoryInspector` injection contract or allowing arbitrary timeout input. Add a deterministic injected/default test showing the real configured value is forwarded and an explicitly injected inspector still receives only `Path`. Preserve the fail-closed inspection error behavior; no runtime host data or WO-032/WO-036 scope changes. Tests and exact HEAD Review Evidence remain mandatory. From 7777aa4436e5dc2b531a511bd1113db01dd58d34 Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:32:28 -0300 Subject: [PATCH 18/22] fix(HIVE-FV-001): use configured Git timeout in default execution inspector --- backend/app/execution_orchestrator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/app/execution_orchestrator.py b/backend/app/execution_orchestrator.py index 586283a..44db4a6 100644 --- a/backend/app/execution_orchestrator.py +++ b/backend/app/execution_orchestrator.py @@ -435,7 +435,11 @@ def __init__( self.project_loader = project_loader or get_project self.task_loader = task_loader or get_task self.context_builder = context_builder or context_manager.build_context - self.repository_inspector = repository_inspector or inspect_project + self.repository_inspector = repository_inspector or ( + lambda workspace: inspect_project( + workspace, timeout_seconds=self.settings.repository_git_timeout_seconds + ) + ) default_seams = all( seam is None for seam in ( From c525b16a57d10bc212b3b7204c4c22823eb6e0b8 Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:32:43 -0300 Subject: [PATCH 19/22] test(HIVE-FV-001): prove execution timeout and preserve injected inspector seam --- backend/tests/test_execution_orchestrator.py | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/backend/tests/test_execution_orchestrator.py b/backend/tests/test_execution_orchestrator.py index beeda99..2d4b67a 100644 --- a/backend/tests/test_execution_orchestrator.py +++ b/backend/tests/test_execution_orchestrator.py @@ -453,3 +453,31 @@ def counted(request_value: ExecutorRequest) -> tuple[ExecutionIdentity, ProjectR assert result.status == "STAGED" assert calls == 1 + + +def test_default_execution_inspector_uses_bounded_configured_git_timeout( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + observed: list[tuple[Path, float]] = [] + + def capture(path: Path, *, timeout_seconds: float) -> InspectionResult: + observed.append((path, timeout_seconds)) + return inspection() + + monkeypatch.setattr("app.execution_orchestrator.inspect_project", capture) + settings = Settings.for_testing(projects_root=tmp_path, repository_git_timeout_seconds=45) + orchestrator = ExecutionOrchestrator(settings=settings) + assert orchestrator.repository_inspector(tmp_path) == inspection() + assert observed == [(tmp_path, 45.0)] + + # A user-injected inspector still receives one Path argument. + injected_paths: list[Path] = [] + + def injected(path: Path) -> InspectionResult: + injected_paths.append(path) + return inspection() + + custom = ExecutionOrchestrator(settings=settings, repository_inspector=injected) + assert custom.repository_inspector(tmp_path) == inspection() + assert injected_paths == [tmp_path] + assert observed == [(tmp_path, 45.0)] From 526bc68d56abd60632575120664ca99129b08faf Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:49:15 -0300 Subject: [PATCH 20/22] docs(HIVE-FV-001): scope final delta-head configured timeout correction --- docs/maintenance/HIVE-FV-001.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/maintenance/HIVE-FV-001.md b/docs/maintenance/HIVE-FV-001.md index 85b5a9a..740b7f7 100644 --- a/docs/maintenance/HIVE-FV-001.md +++ b/docs/maintenance/HIVE-FV-001.md @@ -43,3 +43,7 @@ Run focused registry/discovery/index/retrieval/governance tests and all exact-he ## Ready-review P1 follow-up: default execution inspection timeout The same typed, bounded Git timeout must also reach `ExecutionOrchestrator`'s default repository-inspection preflight. After the preceding registry fix, a further automated review observed that `ExecutionOrchestrator.repository_inspector` was still initialized to the bare `inspect_project` function and therefore silently used its default 30s instead of `self.settings.repository_git_timeout_seconds` when an operator explicitly configured, for example, 45s. Fix only the **default** seam to bind the already-supplied orchestrator Settings, without changing the public single-argument `RepositoryInspector` injection contract or allowing arbitrary timeout input. Add a deterministic injected/default test showing the real configured value is forwarded and an explicitly injected inspector still receives only `Path`. Preserve the fail-closed inspection error behavior; no runtime host data or WO-032/WO-036 scope changes. Tests and exact HEAD Review Evidence remain mandatory. + +## Final Ready review: Context Manager delta HEAD recheck + +CodeRabbit's minor functional finding on PR #169 identifies one remaining source-identity caller within this Work Order's configured Git timeout scope: `context_manager._assert_delta_target_stable` rechecks the HEAD with `_git_head(project_path)` and therefore uses the default 30s instead of the bounded `settings.repository_git_timeout_seconds` during an otherwise valid slow Windows bind mount. Forward the existing typed bounded Settings value in this final fail-closed race recheck. Extend its existing precise-race-reason test to assert an explicit 45-second configured timeout is forwarded while preserving the `project_head_stale` and `repository_source_changed` distinctions. This is not authorization for other Context Manager changes, unrelated cleanup, HIVE release or host mutation. Final acceptance remains exact-head Validate, Integration health, Review Evidence, CodeQL, Dependency Review, resolved review threads and transparent OWNER_SELF_AUDIT (not independent), then governed squash and exact-main postmerge CI. From 6afa727e7dd1229ed7aa0be36f98519e4c771869 Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:49:33 -0300 Subject: [PATCH 21/22] fix(HIVE-FV-001): forward Git timeout to Context Manager delta HEAD recheck --- backend/app/context_manager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/app/context_manager.py b/backend/app/context_manager.py index 303055e..9c8c8a8 100644 --- a/backend/app/context_manager.py +++ b/backend/app/context_manager.py @@ -2347,7 +2347,10 @@ def _assert_delta_target_stable( except ContextStaleError as exc: if exc.code == "repository_source_changed": try: - current_head = _git_head(state.snapshot.project_path) + current_head = _git_head( + state.snapshot.project_path, + timeout_seconds=settings.repository_git_timeout_seconds, + ) except (OSError, RepositoryIndexingError, RuntimeError): raise if current_head.lower() != state.snapshot.repository_head_sha.lower(): From c06d37d6d07cbb44c846509f78cb91655e178f59 Mon Sep 17 00:00:00 2001 From: Clayton nunes <114633702+KayzenRoot@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:49:52 -0300 Subject: [PATCH 22/22] test(HIVE-FV-001): verify delta HEAD honors configured timeout and preserves race reasons --- backend/tests/test_context_manager.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_context_manager.py b/backend/tests/test_context_manager.py index 50f41cb..90b4968 100644 --- a/backend/tests/test_context_manager.py +++ b/backend/tests/test_context_manager.py @@ -1094,7 +1094,13 @@ def test_delta_final_stability_preserves_precise_race_reason( ) -> None: patch_build_dependencies(monkeypatch) target = context_manager.build_context(Settings(), PROJECT_ID, TASK_ID) - monkeypatch.setattr(context_manager, "_git_head", lambda _path: current_head) + observed_timeouts: list[float] = [] + + def capture_head(_path: Path, *, timeout_seconds: float) -> str: + observed_timeouts.append(timeout_seconds) + return current_head + + monkeypatch.setattr(context_manager, "_git_head", capture_head) monkeypatch.setattr( context_manager, "_assert_state_stable", @@ -1104,9 +1110,12 @@ def test_delta_final_stability_preserves_precise_race_reason( ) with pytest.raises(context_manager.ContextStaleError) as caught: - context_manager._assert_delta_target_stable(Settings(), PROJECT_ID, TASK_ID, target) + context_manager._assert_delta_target_stable( + Settings.for_testing(repository_git_timeout_seconds=45), PROJECT_ID, TASK_ID, target + ) assert caught.value.code == expected_code + assert observed_timeouts == [45.0] def test_context_api_accepts_project_and_task_ids_without_query_assembly(