Skip to content

context: load project skills from git root#2164

Merged
enyst merged 7 commits intomainfrom
fix/load-project-skills-git-root
Feb 21, 2026
Merged

context: load project skills from git root#2164
enyst merged 7 commits intomainfrom
fix/load-project-skills-git-root

Conversation

@enyst
Copy link
Collaborator

@enyst enyst commented Feb 21, 2026

What

When load_project_skills() is called from a subdirectory inside a git repo, it now also loads third-party files (e.g. AGENTS.md) and skills directories from the git repo root.

Why

OpenHands CLI passes the current working directory to load_project_skills(). If the user runs openhands from a subdirectory, repo-level guidance in the repo root (AGENTS.md, .agents/skills/, etc.) was previously missed.

How

  • Detect nearest ancestor containing a .git entry (dir or file).
  • Load skills from [work_dir, git_root] with work_dir precedence for duplicates.

Tests

  • uv run pytest tests/sdk/context/skill/test_load_project_skills.py -q

Manual proof (CLI)

Ran OpenHands CLI headless from a git subdirectory with only root AGENTS.md present and confirmed the system prompt event included the root content.

Commands used (high level):

  • Create temp repo: .git/ + root AGENTS.md containing SENTINEL_ROOT_123
  • Run: openhands --override-with-envs --headless ... from subdir/
  • Verify: SENTINEL_ROOT_123 appears under [BEGIN context from [agents]] in the saved SystemPromptEvent JSON

@enyst can click here to continue refining the PR


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.12-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:62438a2-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-62438a2-python \
  ghcr.io/openhands/agent-server:62438a2-python

All tags pushed for this build

ghcr.io/openhands/agent-server:62438a2-golang-amd64
ghcr.io/openhands/agent-server:62438a2-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:62438a2-golang-arm64
ghcr.io/openhands/agent-server:62438a2-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:62438a2-java-amd64
ghcr.io/openhands/agent-server:62438a2-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:62438a2-java-arm64
ghcr.io/openhands/agent-server:62438a2-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:62438a2-python-amd64
ghcr.io/openhands/agent-server:62438a2-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-amd64
ghcr.io/openhands/agent-server:62438a2-python-arm64
ghcr.io/openhands/agent-server:62438a2-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-arm64
ghcr.io/openhands/agent-server:62438a2-golang
ghcr.io/openhands/agent-server:62438a2-java
ghcr.io/openhands/agent-server:62438a2-python

About Multi-Architecture Support

  • Each variant tag (e.g., 62438a2-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 62438a2-python-amd64) are also available if needed

Copy link
Collaborator Author

enyst commented Feb 21, 2026

Proofs:

  1. Unit test:
uv run pytest tests/sdk/context/skill/test_load_project_skills.py -q
# => 18 passed
  1. Manual (OpenHands CLI) check from a git subdirectory:
TMP=/workspace/tmp_agents_repo
mkdir -p "$TMP/subdir" "$TMP/.git"
printf "%s\n" "# Project Guidelines" "" "SENTINEL_ROOT_123" > "$TMP/AGENTS.md"

cd "$TMP/subdir"
PYTHONPATH=/path/to/this/branch/openhands-sdk \
  LLM_API_KEY=$OPENAI_API_KEY LLM_MODEL=openai/gpt-5-nano \
  openhands --override-with-envs --headless --task "Reply with exactly: OK"
# (no AGENTS.md in subdir; only in repo root)

# verify saved SystemPromptEvent includes root AGENTS.md content:
python - <<"PY"
import json
from pathlib import Path
needle = "SENTINEL_ROOT_123"
conv = Path.home() / ".openhands" / "conversations"
# pick the newest conversation dir
latest = max(conv.iterdir(), key=lambda p: p.stat().st_mtime)
event0 = sorted((latest / "events").glob("event-00000-*.json"))[0]
blob = event0.read_text()
assert needle in blob
print("...", blob[blob.index(needle)-80:blob.index(needle)+len(needle)+80], "...", sep="")
PY

# output contains:
# [BEGIN context from [agents]]
# # Project Guidelines
#
# SENTINEL_ROOT_123
#
# [END Context]

@github-actions
Copy link
Contributor

github-actions bot commented Feb 21, 2026

API breakage checks (Griffe)

Result: Passed

Action log

@github-actions
Copy link
Contributor

github-actions bot commented Feb 21, 2026

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/context/skills
   skill.py3692593%90–91, 246–247, 433–439, 442, 531–534, 709–710, 804–805, 836–837, 925, 932–933
TOTAL18677563169% 

Copy link
Collaborator Author

enyst commented Feb 21, 2026

Follow-up: fixed flaky xdist teardown assertion in CI.

CI was failing with:

  • AssertionError: previous item was not torn down properly

Root cause: @pytest.mark.forked (pytest-forked) on test_conversation_large_event_handling does not reliably compose with xdist. Under xdist we already have per-worker process isolation, so we now apply the forked marker only when not running under xdist (PYTEST_XDIST_WORKER unset).

Local proof (matches CI style):

uv run pytest -n 4 --maxfail=1 tests/sdk -q
# => 2376 passed

CI for this PR is green now.

@enyst enyst marked this pull request as ready for review February 21, 2026 04:57
Copy link
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Acceptable - Solves Real Problem, Minor Complexity Concern

Taste Rating: 🟡 Acceptable

Solid pragmatic fix. You're solving an actual user pain point (CLI users in subdirs missing repo-root AGENTS.md). The git root detection is clean - no shelling out, handles worktrees. Tests exercise real behavior.

One concern: Approaching the complexity threshold with nested loops. See inline comment.

Verdict: ✅ Worth merging after considering the simplification suggestion.

enyst and others added 3 commits February 21, 2026 05:23
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
@enyst enyst force-pushed the fix/load-project-skills-git-root branch from 6b6ec21 to 59c1018 Compare February 21, 2026 05:26
Co-authored-by: openhands <openhands@all-hands.dev>
Copy link
Collaborator Author

enyst commented Feb 21, 2026

Thanks for the review + the simplification suggestion.

Addressed: I refactored the inner merging logic into a small helper (_merge_loaded_skills) so load_project_skills() no longer has the 4-level loop nesting. This keeps the main flow (search roots + candidate dirs) readable while preserving the same precedence semantics (work_dir wins over git_root).

Also added a more integration-style test (closer to the CLI “live test” proof):

  • tests/sdk/conversation/test_repo_root_project_skills.py

It sets up a temp repo like:

  • repo root contains .git/ + AGENTS.md with sentinel SENTINEL_ROOT_123
  • workspace is a subdir (no AGENTS.md there)

Then it builds skills = load_project_skills(subdir)AgentContext(skills=skills) → starts a LocalConversation and sends a message. That path emits a SystemPromptEvent via Agent.init_state(). The assertion checks that SystemPromptEvent.dynamic_context.text includes the sentinel, which is exactly what the CLI ultimately relies on (system prompt context injection), but without needing to run the CLI binary or a real LLM.

Relevant commands run locally:

uv run pytest -q tests/sdk/context/skill/test_load_project_skills.py \
  tests/sdk/conversation/test_repo_root_project_skills.py

@enyst enyst requested a review from all-hands-bot February 21, 2026 05:36
Copy link
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Acceptable - Solid Pragmatic Fix

Taste Rating: 🟡 Acceptable

You're solving a real problem (CLI users in subdirs missing repo-root AGENTS.md). The git root detection is clean - no shelling out, handles worktrees. Tests exercise real behavior, not mocks.

The complexity concern from the previous review was addressed by extracting _merge_loaded_skills(). Still 3-4 nesting levels, but justified by the requirements (multiple roots × multiple dirs × multiple skill types).

Key Insight: Simple data flow eliminates special cases - search both roots, merge with precedence. No edge case handling needed.

Verdict: ✅ Worth merging. Backward compatible, well-tested, solves actual user pain point.

enyst and others added 3 commits February 21, 2026 23:00
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Copy link
Collaborator Author

@enyst enyst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂 TestLLM was merged too recently, finally it found it after updating

@enyst enyst merged commit 40e5c52 into main Feb 21, 2026
27 checks passed
@enyst enyst deleted the fix/load-project-skills-git-root branch February 21, 2026 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants