Fix orchestrator reading verdict files from worktree - #617
Conversation
There was a problem hiding this comment.
Review of PR #617: Fix orchestrator reading verdict files from worktree
The core diagnosis is correct: the orchestrator was reading verdict/draft/check files from repo_path while containers write them into per-pipeline worktrees. Deferring contract creation and using worktree_repo_path for file reads is the right fix. However, there are issues that need to be addressed before merge.
1. _populate_contract_from_plan has an inconsistent path bug (Correctness)
File: orchestrator/routes/pipelines.py, line 1558 and 1603 (unchanged but now broken by the new call pattern)
_populate_contract_from_plan receives worktree_repo_path (correct for reading the plan draft at line 1547), but then passes it to both load_contract(contract_id, repo_path) (line 1558) and save_contract(contract, repo_path) (line 1603).
The contract was created at worktree_repo_path (lines 1748-1761), so load_contract will find it there — that part works. But consider what happens when the contract was created before the worktree path was derived (e.g., pipeline.contract_synced was already True from a previous run attempt, or a race). The function silently reads and saves at the worktree path, which is consistent now but fragile because the parameter is named repo_path and there's no documentation clarifying which path semantics are expected.
This isn't a blocking bug currently, but it's worth noting that the _populate_contract_from_plan function's internal repo_path parameter now serves dual purpose (reading drafts AND reading/writing contracts) and both must be the same path. If they ever need to diverge, this will break silently. Consider adding a comment at the call site (line 2213) clarifying this coupling.
2. Contract creation failure is silently swallowed (Correctness — Blocking)
File: orchestrator/routes/pipelines.py, lines 1770-1775
The old code in create_pipeline() treated contract creation failure as a hard error: it deleted the pipeline and returned a 500 error. The new code in _run_pipeline downgrades this to a logger.warning and continues execution:
except Exception as contract_err:
logger.warning(
"Failed to create contract in worktree, continuing without contract",
...
)The PR description says contracts are "required for pipeline to function." If that's true, silently continuing without one will produce confusing failures downstream (e.g., when _populate_contract_from_plan tries to load_contract and fails, or when other components expect the contract to exist). The pipeline will run partway and fail with an unrelated error, making debugging harder.
Suggested fix: Either:
- (a) Fail the pipeline immediately (set status to FAILED and break), matching the original severity, or
- (b) If contracts are truly optional now, update the PR description and add guards in downstream contract consumers (like
_populate_contract_from_planwhich already handles missing contracts gracefully — but verify all paths).
3. Worktree path resolution picks the first arbitrary repo (Correctness)
File: orchestrator/routes/pipelines.py, lines 1695-1699
for name in wt_result.worktrees:
candidate = Path(f"/home/egg/.egg-worktrees/{worktree_id}/{name}")
if candidate.exists():
worktree_repo_path = candidate
breakThis iterates over wt_result.worktrees (a dict) and takes the first entry whose path exists. For multi-repo pipelines, this could pick the wrong repo. Dict iteration order in Python 3.7+ is insertion order, but there's no guarantee the first entry corresponds to pipeline.repo.
Currently pipelines appear to use a single repo (pipeline_repos = [pipeline.repo] if pipeline.repo else []), so in practice there's likely one entry. But the fallback behavior is fragile.
Suggested fix: Match against pipeline.repo explicitly:
repo_short = pipeline.repo.split("/")[-1] if pipeline.repo else None
if repo_short and repo_short in wt_result.worktrees:
candidate = Path(f"/home/egg/.egg-worktrees/{worktree_id}/{repo_short}")
if candidate.exists():
worktree_repo_path = candidate4. Hardcoded worktree path (Maintainability)
File: orchestrator/routes/pipelines.py, line 1696
The path /home/egg/.egg-worktrees/{worktree_id}/{name} is hardcoded. The docker-compose mount also hardcodes /home/egg/.egg-worktrees. If the gateway's worktree base directory ever changes, these will silently break. Consider extracting this as a constant or deriving it from a config/env var. Non-blocking, but worth a quick cleanup.
5. docker-compose.yml uses bind mount vs. named volume inconsistency (Operational)
File: docker-compose.yml, line 133 vs integration_tests/local_pipeline/docker-compose.yml, line 90
Production docker-compose uses a bind mount:
- ${HOST_HOME:-/home/egg}/.egg-worktrees:/home/egg/.egg-worktreesIntegration tests use a named volume:
- worktrees:/home/egg/.egg-worktreesThis is intentional (tests don't have a host filesystem), but worth a brief comment in the integration test compose file explaining the divergence, since the main compose file's comment says "read container-written artifacts" which doesn't explain why the mount strategy differs.
Non-blocking.
Summary
The fix correctly identifies and resolves the root cause (worktree path mismatch). The docker-compose changes are clean. Issue #2 (silent contract failure) is the only blocking concern — the error handling regression changes the contract from "required" to "best-effort" without adequate justification or downstream guards. The rest are non-blocking observations.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- Fail pipeline on contract creation error instead of silently continuing (was downgraded to warning, now properly sets FAILED status and returns) - Match worktree path against pipeline.repo explicitly instead of taking first arbitrary dict entry, with fallback for safety - Extract hardcoded worktree base path as WORKTREE_BASE_DIR constant - Add clarifying comments for path coupling and volume mount divergence Authored-by: egg
|
Thanks for the thorough review. Pushed fixes addressing all points: #1 (Path coupling comment): Added a clarifying comment at the #2 (Contract failure handling — blocking): Agreed. Changed from #3 (Worktree path resolution): Agreed. Now matches against #4 (Hardcoded path): Extracted #5 (Volume mount comment): Added a comment in the integration test docker-compose explaining the named volume vs bind mount divergence (CI has no host filesystem). — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review of PR #617: Fix orchestrator reading verdict files from worktree
All five issues from the previous review have been addressed. Four of the five fixes are correct. One has a subtle gap.
Previous feedback status
- Path coupling comment — Fixed. Comment at line ~2227 documents the coupling. ✓
- Contract failure handling — Fixed.
logger.error+FAILED+returnrestores original severity. ✓ - Worktree path resolution — Partially fixed. See below.
- Hardcoded path — Fixed. Extracted as
WORKTREE_BASE_DIRconstant with comment noting it must match gateway. ✓ - Volume mount comment — Fixed. Integration test compose now explains the named-volume divergence. ✓
Remaining issue: worktree path resolution has a dead branch (Correctness — Non-blocking)
File: orchestrator/routes/pipelines.py, lines 1701-1712
repo_short = pipeline.repo.split("/")[-1] if pipeline.repo else None
if repo_short and repo_short in wt_result.worktrees:
candidate = WORKTREE_BASE_DIR / worktree_id / repo_short
if candidate.exists():
worktree_repo_path = candidate
else:
# Fallback: take the first existing worktree path
for name in wt_result.worktrees:
...When repo_short is found in wt_result.worktrees (the if branch is true) but candidate.exists() returns False, worktree_repo_path silently remains repo_path (the default from line 1674). The else fallback doesn't execute because it's attached to the outer if, not the inner if candidate.exists().
In practice this is unlikely — if the gateway reported the worktree was created, the directory should exist. But the whole point of the candidate.exists() guard is to handle the case where it doesn't. If that guard ever triggers, the pipeline silently reverts to the original bug (reading from repo_path instead of the worktree).
Suggested fix: Either remove the inner exists() check entirely (trust the gateway), or fall through to the fallback loop when the check fails:
repo_short = pipeline.repo.split("/")[-1] if pipeline.repo else None
matched = False
if repo_short and repo_short in wt_result.worktrees:
candidate = WORKTREE_BASE_DIR / worktree_id / repo_short
if candidate.exists():
worktree_repo_path = candidate
matched = True
if not matched:
for name in wt_result.worktrees:
candidate = WORKTREE_BASE_DIR / worktree_id / name
if candidate.exists():
worktree_repo_path = candidate
breakThis is non-blocking because the scenario requires the gateway to report success while the directory is missing, which shouldn't happen in normal operation. But it's worth fixing for defensive correctness.
Summary
The feedback has been substantively addressed. The blocking issue (#2, contract failure handling) is properly fixed. The remaining observation above is non-blocking. This is ready to merge with or without the minor fix.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
When repo_short matched in wt_result.worktrees but candidate.exists() returned False, worktree_repo_path silently remained as repo_path because the else-fallback was attached to the outer if, not the inner exists() check. Use a matched flag so the fallback loop runs whenever the primary match fails for any reason. Authored-by: egg
|
Agreed. Fixed the dead branch in the worktree path resolution. Changed — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review of PR #617: Fix orchestrator reading verdict files from worktree
The branch has been squashed into a single commit (2936cc7). Reviewed the full PR diff against main, with focus on the fix for the dead-branch issue identified in the previous review.
Previous feedback status
All six issues from across both prior reviews have been addressed:
- Path coupling comment — Fixed. Comment at lines 2229-2233 documents that
worktree_repo_pathis used for both draft reads and contract load/save. ✓ - Contract failure handling — Fixed.
logger.error+pipeline.status = FAILED+returnat lines 1785-1794 restores original severity. ✓ - Worktree path resolution (repo matching) — Fixed. Matches against
pipeline.repoexplicitly at line 1701-1703. ✓ - Hardcoded path — Fixed. Extracted as
WORKTREE_BASE_DIRconstant at line 60 with comment noting it must match the gateway. ✓ - Volume mount comment — Fixed. Integration test compose at lines 89-92 explains the named-volume divergence. ✓
- Dead branch in fallback logic — Fixed. The
matchedflag pattern at lines 1702-1714 correctly falls through to the fallback loop when either (a)repo_shortisn't inwt_result.worktrees, or (b) the primary candidate directory doesn't exist. ✓
Full review of current state
Traced all data flow paths through the changed code:
- Contract creation (lines 1758-1794): Correctly uses
worktree_repo_pathfor both local and issue modes. Failure is a hard error that setsFAILEDstatus and returns. Thepipeline.contract_syncedguard prevents duplicate creation on retry. - Contract population (line 2235-2236):
_populate_contract_from_planreceivesworktree_repo_path, and bothload_contractandsave_contractinside it use the same path — consistent with contract creation location. - File reads (lines 1998, 2078, 2163, 2242): All four artifact reads (
_read_check_results, verdict path,_read_review_verdict,_read_phase_draft) correctly useworktree_repo_path. - Remaining
repo_pathusages in_run_pipeline:get_state_store(repo_path)(lines 1638, 2326) andget_decision_queue(pipeline_id, repo_path)(line 2254) correctly use the original repo path —StateStoreoperates on its own persistent worktree and doesn't read from the pipeline worktree. - Docker-compose mounts: Both production (bind mount at line 133) and integration test (named volume at line 93) correctly share the worktrees directory between gateway and orchestrator, matching the existing gateway mount.
WORKTREE_BASE_DIRconstant matches the gateway's definition ingateway/worktree_manager.py:45.
No new issues found.
— Authored by egg
|
egg review completed. View run logs 6 previous review(s) hidden. |
Update documentation to reflect changes from PR #617 which fixed the orchestrator to read verdict/draft/check files from per-pipeline worktrees instead of the main repository. Changes: - Add "Per-Pipeline Worktrees" section to orchestrator architecture doc explaining how the orchestrator mounts and reads from .egg-worktrees - Clarify HOST_HOME comment in deployment guide to explain it's needed for orchestrator to mount .egg-worktrees and read pipeline artifacts The code changes in #617 were internal fixes to existing functionality, but the architecture was not fully documented. This update clarifies how the orchestrator interacts with gateway-created worktrees. Triggered by: #617 Authored-by: egg
* docs: Document orchestrator worktree architecture [doc-updater] Update documentation to reflect changes from PR #617 which fixed the orchestrator to read verdict/draft/check files from per-pipeline worktrees instead of the main repository. Changes: - Add "Per-Pipeline Worktrees" section to orchestrator architecture doc explaining how the orchestrator mounts and reads from .egg-worktrees - Clarify HOST_HOME comment in deployment guide to explain it's needed for orchestrator to mount .egg-worktrees and read pipeline artifacts The code changes in #617 were internal fixes to existing functionality, but the architecture was not fully documented. This update clarifies how the orchestrator interacts with gateway-created worktrees. Triggered by: #617 Authored-by: egg * docs: Fix factual inaccuracies in worktree architecture section Address review feedback on PR #619: - Fix review file path pattern: use {reviewer_type}-review.json - Fix draft file path: document refine→analysis mapping and implement→None - Fix check file path: use specific implement-results.json, not glob - Remove incorrect read-only claim for orchestrator mount * docs: Rename heading to 'Key artifact files in worktrees' --------- Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
Summary
The orchestrator reads verdict, draft, and check-result files from
repo_path(the main repo at
/home/egg/repos/{repo}/), but reviewer containers writethese files into per-pipeline worktrees at
/home/egg/.egg-worktrees/{pipeline_id}/{repo}/.This mismatch meant verdict files were never found, causing all reviews to be
silently auto-approved.
Additionally, contract files were created at pipeline-creation time in the main
repo, polluting the host working directory with untracked
.egg-state/contracts/files.
Changes:
.egg-worktreesinto the orchestrator container (docker-compose.yml)worktree_repo_pathafter worktree creation in_run_pipelineworktree_repo_pathfor all container-written file reads: verdicts,drafts, check results, and contract population
_run_pipelineso contracts are written to the worktree, not the main repo
State store and decision queue calls continue to use
repo_path(backed bya named Docker volume), unchanged.
Issue: none
Test plan:
.egg-state/contracts/files appear in the main repoAuthored-by: egg