Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
908c4f8
docs(HIVE-FV-001): bind Windows indexer maintenance to scoped work order
KayzenRoot Sep 27, 2026
e050b9c
fix(HIVE-FV-001): bounded Windows Git timeout and safe GEF gitlink in…
KayzenRoot Sep 27, 2026
2636d0c
fix(HIVE-FV-001): correct multiline Gitlink fixture and subprocess fi…
KayzenRoot Sep 27, 2026
22d6c5c
fix(HIVE-FV-001): preserve registry contract and correct formatting/t…
KayzenRoot Sep 27, 2026
028dccb
test(HIVE-FV-001): inspect exact formatter delta in failed validation
KayzenRoot Sep 27, 2026
2fbe0e6
fix(HIVE-FV-001): apply exact Ruff formatting and remove temporary di…
KayzenRoot Sep 27, 2026
61d7f4b
docs(HIVE-FV-001): admit gitlink-safe corpus preflight and bounded re…
KayzenRoot Sep 28, 2026
f5c4bd7
fix(HIVE-FV-001): align corpus Git inventory with gitlink-safe index …
KayzenRoot Sep 28, 2026
c42e21d
test(HIVE-FV-001): format typed gitlink fixtures and negative governa…
KayzenRoot Sep 28, 2026
1f391be
style(HIVE-FV-001): align Python fixtures with exact Ruff format gate
KayzenRoot Sep 28, 2026
d9fee32
style(HIVE-FV-001): use canonical Ruff 100-column fixture layout
KayzenRoot Sep 28, 2026
bc198ed
docs(HIVE-FV-001): specify exact parser admission after Review Eviden…
KayzenRoot Sep 28, 2026
d27cade
fix(HIVE-FV-001): admit only exact registered maintenance token in ev…
KayzenRoot Sep 28, 2026
a3475b2
docs(HIVE-FV-001): record admitted marker and Ready evidence revalida…
KayzenRoot Sep 28, 2026
fedae34
docs(HIVE-FV-001): scope P1 registry timeout and staged gitlink corre…
KayzenRoot Sep 28, 2026
865f235
fix(HIVE-FV-001): configured registry timeout and staged gitlink dete…
KayzenRoot Sep 28, 2026
6c07e05
docs(HIVE-FV-001): bind configured Git timeout at default execution p…
KayzenRoot Sep 28, 2026
7777aa4
fix(HIVE-FV-001): use configured Git timeout in default execution ins…
KayzenRoot Sep 28, 2026
c525b16
test(HIVE-FV-001): prove execution timeout and preserve injected insp…
KayzenRoot Sep 28, 2026
526bc68
docs(HIVE-FV-001): scope final delta-head configured timeout correction
KayzenRoot Sep 28, 2026
6afa727
fix(HIVE-FV-001): forward Git timeout to Context Manager delta HEAD r…
KayzenRoot Sep 28, 2026
c06d37d
test(HIVE-FV-001): verify delta HEAD honors configured timeout and pr…
KayzenRoot Sep 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
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"
Expand Down
5 changes: 4 additions & 1 deletion backend/app/context_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
6 changes: 5 additions & 1 deletion backend/app/execution_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
4 changes: 3 additions & 1 deletion backend/app/project_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
37 changes: 28 additions & 9 deletions backend/app/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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",
Expand All @@ -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:
Expand All @@ -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
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
) -> InspectionResult:
try:
if not project_path.exists() or not project_path.is_dir():
return InspectionResult(
Expand All @@ -256,18 +262,23 @@ 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)

branch_result = _run_git(
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
Expand All @@ -278,7 +289,11 @@ 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=dirty"],
timeout_seconds=timeout_seconds,
)
return InspectionResult(
git_branch=branch,
git_head_sha=head.lower(),
Expand Down Expand Up @@ -387,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:
Expand Down Expand Up @@ -459,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
Expand Down
85 changes: 65 additions & 20 deletions backend/app/repository_indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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",
Expand All @@ -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:
Expand All @@ -267,9 +268,13 @@ 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:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
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()
Expand All @@ -278,12 +283,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
Expand All @@ -294,11 +300,21 @@ 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
Expand All @@ -322,12 +338,18 @@ 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)


Expand Down Expand Up @@ -415,24 +437,34 @@ 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
Expand All @@ -450,10 +482,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 mode == "160000":
raise RepositoryIndexingError("tracked_submodule_unsupported")
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
try:
resolved_path = _resolve_tracked_path(project_root, path)
except ProjectPathError as exc:
Expand Down Expand Up @@ -493,11 +527,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")


Expand Down
Loading
Loading