diff --git a/AGENTS.md b/AGENTS.md index 2f59e8c..ee41476 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,7 +110,12 @@ If you need to add new environment variables: 3. Add a deterministic helper in `triage/classifier.py` or `worker/analyzer.py` if applicable. 4. Add the reasoner to `triage/agent.py` or `worker/agent.py`. Tag with `entry` only if it's a polling/claiming entrypoint. 5. Unit-test the helper and the reasoner wiring under `tests/unit/`. -6. **Lint and format before committing** — run `ruff check .` and `ruff format .` to fix all linting and formatting issues. +6. **Lint and format before committing**: + ```bash + ruff check . # Check for linting issues + ruff format . # Auto-format code + ``` + All linting must pass before committing. CI enforces this. 7. **Run tests** — `pytest tests/unit` must pass before committing. 8. **Update both `CLAUDE.md` and `AGENTS.md`** with any relevant changes. @@ -125,6 +130,60 @@ If you need to add new environment variables: ## Troubleshooting Common Issues +### Working in Git Worktrees + +**This repository is often used with git worktrees.** Docker containers must be started from the worktree directory you're currently working in. + +**Common Issue:** `/var/nd/work` directory not found + +**Symptoms:** +- Agent execution fails with `[Errno 2] No such file or directory: '/var/nd/work'` +- WorkspaceClient.prepare() fails + +**Root cause:** Docker containers were started from a different worktree. The volume mount in docker-compose.yml uses: +```yaml +volumes: + - ${ND_WORKSPACE_ROOT:-./.nd-workspace}:/var/nd +``` + +The `./.nd-workspace` path is relative to where `docker-compose up` was run, NOT the current directory. + +**How to identify:** +```bash +# Check where containers are running from +docker inspect -worker-1-1 --format '{{.Config.Labels}}' | grep working_dir + +# Check volume mounts +docker inspect -worker-1-1 --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' +``` + +**Solution:** +```bash +# 1. Stop containers from old worktree +docker-compose -p down + +# 2. Create workspace directory in current worktree +mkdir -p .nd-workspace/work + +# 3. Start containers from current worktree +docker-compose up -d +``` + +**Alternative: Shared workspace across worktrees** + +Set `ND_WORKSPACE_ROOT` to an absolute path in `.env.local`: +```bash +ND_WORKSPACE_ROOT=/Users/andy/.nd-workspace-shared +``` + +Then create the directory and restart: +```bash +mkdir -p /Users/andy/.nd-workspace-shared/work +docker-compose down && docker-compose up -d +``` + +This allows all worktrees to share the same workspace, making tasks visible across worktrees. + ### AgentField Connectivity If agents show "running in degraded mode" or "Could not resolve host: agentfield": - Check for port conflicts on 8081 diff --git a/CLAUDE.md b/CLAUDE.md index 8472579..688c0f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,6 +72,40 @@ ruff format --check . CI (`.github/workflows/ci.yml`) runs `ruff check`, `ruff format --check`, unit tests, and enforces ≥50% coverage. Match this locally before pushing. +## Working in Git Worktrees + +This repository is often used with git worktrees. **Important:** Docker containers must be started from the worktree directory you're currently working in. + +### Common Issue: `/var/nd/work` not found + +**Symptom:** Agent execution fails with `[Errno 2] No such file or directory: '/var/nd/work'` + +**Root cause:** Docker containers were started from a different worktree, and the volume mount `${ND_WORKSPACE_ROOT:-./.nd-workspace}:/var/nd` is relative to where `docker-compose up` was run. + +**Solution:** +```bash +# Stop containers from the old worktree +docker-compose -p down + +# Create workspace directory in current worktree +mkdir -p .nd-workspace/work + +# Start containers from current worktree +docker-compose up -d +``` + +The `.nd-workspace/` directory is gitignored and local to each worktree. + +### Alternative: Shared Workspace Across Worktrees + +If you want all worktrees to share the same workspace: +```bash +# Set in .env.local +ND_WORKSPACE_ROOT=/Users/andy/.nd-workspace-shared +``` + +Then `mkdir -p /Users/andy/.nd-workspace-shared/work` and restart containers. This allows tasks created in one worktree to be visible in others. + ## Refreshing AWS Credentials When AWS credentials expire, follow this workflow to update the docker-compose environment: @@ -91,7 +125,9 @@ docker-compose down docker-compose up -d # 4. Verify credentials loaded and have Bedrock access -docker exec hyper-furniture-worker-1-1 sh -c ' +# Note: container name is -worker-1-1 (based on current directory name) +docker ps --filter "name=worker-1" --format "{{.Names}}" # Find exact container name +docker exec -worker-1-1 sh -c ' python3 -c "import boto3; sts = boto3.client(\"sts\", region_name=\"us-east-1\"); print(\"Identity:\", sts.get_caller_identity()[\"Arn\"])" ' # Should show: arn:aws:sts::657062785455:assumed-role/horizon-okta/andy @@ -132,7 +168,12 @@ Full table is in `README.md`. 3. Add a deterministic helper in `triage/classifier.py` or `worker/analyzer.py` if applicable. 4. Add the reasoner to `triage/agent.py` or `worker/agent.py`. Tag with `entry` only if it's a polling/claiming entrypoint. 5. Unit-test the helper and the reasoner wiring under `tests/unit/`. -6. **Lint and format before committing** — run `ruff check .` and `ruff format .` to fix all linting and formatting issues. +6. **Lint and format before committing**: + ```bash + ruff check . # Check for linting issues + ruff format . # Auto-format code + ``` + All linting must pass before committing. CI enforces this. 7. **Run tests** — `pytest tests/unit` must pass before committing. ## Things to avoid diff --git a/nd/worker/agent.py b/nd/worker/agent.py index 758f195..c203f89 100644 --- a/nd/worker/agent.py +++ b/nd/worker/agent.py @@ -55,7 +55,9 @@ def _extract_source_metadata( else: # Fallback: construct from available data if mr_number: - source_url = f"https://{platform_host}/{repo_owner}/{repo_name}/-/merge_requests/{mr_number}" + source_url = ( + f"https://{platform_host}/{repo_owner}/{repo_name}/-/merge_requests/{mr_number}" + ) source_type = "mr" source_identifier = f"{platform}:{repo_owner}/{repo_name}#{mr_number}" elif issue_number: @@ -421,7 +423,7 @@ async def _maybe_cleanup_on_failure() -> None: repo_name=context.get("repo_name", project), base_branch=ws.base_branch or context.get("base_branch", "main"), title=context.get("mr_title", title), - source_url=context.get("mr_url", ""), + source_url=context.get("issue_url") or context.get("mr_url", ""), is_issue=is_issue, ) publish = PublishResult(**publish_result) @@ -950,8 +952,8 @@ def _parse_task_body(body: str) -> dict | None: context["category"] = "issue" context["repo_owner"] = issue_match.group(1) context["repo_name"] = issue_match.group(2) - context["mr_number"] = int(issue_match.group(3)) - context["mr_url"] = issue_match.group(4) + context["issue_number"] = int(issue_match.group(3)) + context["issue_url"] = issue_match.group(4) title_match = re.search(r"\*\*Title:\*\* (.+)", body) if title_match: diff --git a/tests/e2e/test_suite_structure.py b/tests/e2e/test_suite_structure.py index 6c07f64..2ae2508 100644 --- a/tests/e2e/test_suite_structure.py +++ b/tests/e2e/test_suite_structure.py @@ -50,4 +50,6 @@ def test_no_skipped_agent_integration_tests_in_ci(): if "test_simple_request_flow" in line and "SKIPPED" in line: found_skipped = True break - assert found_skipped, "test_simple_request_flow should be SKIPPED when SKIP_AGENT_INTEGRATION=true" + assert found_skipped, ( + "test_simple_request_flow should be SKIPPED when SKIP_AGENT_INTEGRATION=true" + )