Skip to content

fix(daemon): count non-empty segments for project-root depth - #857

Merged
mvschwarz merged 1 commit into
mvschwarz:mainfrom
mv-schwarz:fix/catalog-project-root-depth
Oct 7, 2026
Merged

mvschwarz merged 1 commit into
mvschwarz:mainfrom
mv-schwarz:fix/catalog-project-root-depth

Conversation

@mv-schwarz

@mv-schwarz mv-schwarz commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

What a user gets

Project selection's working-directory step is documented as picking "the deepest declared
project root that contains the working directory". It measured a root's depth as
root.split(path.sep).length, which counts the trailing empty segment that only the filesystem
root produces, so / and /a both measured 2.

  • Before: a catalog declaring / beside /a, resolved from a working folder /a/b, reported
    a tie and stopped with project_required — demanding --project for a case the documented rule
    already answers.
  • After: /a wins.

Counting non-empty segments shifts every non-root path down by exactly one, so relative ordering
among them is unchanged. The only selection outcome that changes is a catalog that declares /
itself as a project root. A genuine tie between two entries sharing the deepest root still stops
with project_required.

One line of behaviour change, plus a regression test. It serves both callers of
inferCatalogProject: rig context work-install and operating posture.

No issue was filed for this; it was reported directly.

How you verified it

Revision: 75ead3fce50a7faee97491d3e32b1bf526429579 on fix/catalog-project-root-depth, parent
09384907. Every check below ran at fd4b9e1d, which was then amended for its commit message
only; git diff fd4b9e1d 75ead3fc is empty and both commits carry the identical tree
f70491dae309379e44b50f2b8fdd436ac64cc842, so the verification transfers unchanged.

The new test was run red against the unfixed line first: Tests 1 failed | 2 passed (3), the
failure thrown at project-catalog.ts:118 with "the working directory is inside several projects
with the same root". The other two cases were green pre-fix, so they are genuine guards rather
than restatements of the fix. Post-fix: 3 passed (3).

npm test exits 1, and I am not going to present that as a pass. Three tests fail, none of
them on this change's path:

  • staging-docker-invoker.test.ts ×2 — "Test timed out in 20000ms", on the two step-timeout cases
    that spawn sleep 30 against a 300 ms timeout
  • terminal-pipe-utf8.test.ts ×1 — "native output timed out"

All three reproduce identically at the parent commit 09384907 (Tests 3 failed | 6 passed (9)), so they are independent of this change. That was established by effect in four independent
runs — the author's, both reviewers', and once more in a clean checkout at the parent — not by
arguing that path arithmetic cannot reach docker timeouts. They were also initially assumed to be
load-sensitive; that hypothesis was tested and disproved, since they still fail when run
focused at low load. They are reproducible pre-existing failures on this host. Whether they also
fail in CI is not something this change establishes.

Per leg, at the reviewed tree:

  • daemon: Test Files 2 failed | 891 passed | 2 skipped (895), Tests 3 failed | 13053 passed | 6 skipped | 5 todo (13067)
  • cli: Test Files 243 passed (243), Tests 3455 passed | 3 skipped (3458)
  • tui: Test Files 98 passed (98), Tests 853 passed (853)
Check Result
npm run build pass
npm run lint pass
npm test (full suite) exits 1 — 3 failures, all reproduced at the parent commit, none on this path (detail above)
packages/daemon/test/project-catalog-root-depth.test.ts 1 failed / 2 passed pre-fix, 3 passed post-fix
project-registration + project-navigation + bundle-routes 106 passed
packages/cli/test/context-work-install.test.ts 26 passed, unchanged
Independent code review, in its own detached worktree clean, no findings; npm run build pass, npm run lint pass, focused Vitest 5 files / 135 tests pass
Independent QA, in a separate detached worktree at the reviewed commit clean, no findings; npm ci, npm run build (four workspaces), npm run lint all pass; focused Vitest 5 files / 135 tests pass; repo checks 255 pass / 1 opt-in skip / 0 fail

Three further failures showed up in earlier runs, were diagnosed to the test environment rather
than the diff, and do not appear in the final run above once their causes were corrected. They
are recorded here only so the earlier red runs are accounted for:

  • Executable mode. 23 files that git records as 100755 materialise on disk as 775 under this
    host's umask 0002 — git preserves only the executable bit, so 0777 & ~0002 = 0775. A test
    asserting the exact mode fails. None of the 23 is in this diff, and the failure reproduced at the
    parent commit. Fixed by restoring the recorded modes.
  • Inherited TMUX. A scenario-pipeline test failed closed because an agent seat inherits the live
    tmux attachment — the documented "Running inside a seat" guard in
    docs/as-built/test-layers.md, behaving correctly and protecting the running rig. Fixed by
    unsetting TMUX.
  • Timing budget. A /healthz check at 305.2 ms against a 250 ms budget, while the host sat at
    load 8.4–10.4 on 5 cores from concurrent test runs. Cleared on a focused rerun.

Not run, and why: no live-model evals and no real-provider runs (OPENRIG_E2E_REAL_CODEX,
OPENRIG_REAL_CLAUDE_INTEGRATION both unset) — they spend provider credits and prove nothing
about a path-arithmetic fix. No stub scenario and no npm run test:ui: catalog project selection
is not a docs/as-built/arteries.md row, packages/ui is untouched, and this change reaches none
of message delivery, launch and resume, the queue, rig identity, skill projection, migrations or
restore. No dependency, bin, build-script or shipped-file change, so no packaging layer. No daemon
restart.

Docs: docs/reference/project-workspace.md:124 and
docs/as-built/architecture/workspace-primitive.md:327 both state the rule as the deepest root
containing the working directory without committing to a mechanical depth measure, so the fix
makes the code match the prose and needs no doc edit. The tie wording at
project-workspace.md:143-144 is what the second test case pins.

Anything you were unsure about

The regression test mocks node:fs rather than building a fixture on disk. That is deliberate and
is the part worth looking at hardest: the case under test is / as a declared root, which cannot
be constructed from a real temp directory, and a test that used a real top-level folder such as
/tmp or /private as a project root would depend on the machine's layout. It uses the
repository's established hoisted importOriginal passthrough (the shape in
slice-indexer.test.ts), overrides only the three calls this path makes — existsSync,
readFileSync, realpathSync — and falls through to the real implementation for any path outside
the fixed fakes. The paths are POSIX on purpose; the project targets macOS and Linux.

.filter(Boolean) was chosen over a named helper to match the file's existing one-liner style.

Selection order and steps 1–6 are unchanged, and the change adds no refusal — it removes a
spurious project_required.

If this is security-related

Not security-related.

  • One concern per PR; no version bump; no CHANGELOG.md edit
  • Tests added or updated where the change is testable
  • I listed the checks I ran, their results, and any checks I could not run

Summary by CodeRabbit

  • Bug Fixes
    • Corrected project selection for working directories under filesystem roots, ensuring the most specific matching project is selected.

Project selection's working-directory step measured a declared root's depth as
root.split(path.sep).length, which counts the trailing empty segment that only
the filesystem root produces. "/" and "/a" both measured 2, so a catalog
declaring "/" beside "/a" reported a tie from a working folder inside "/a" and
stopped with project_required, contradicting the documented "deepest declared
root that contains the working directory" rule. Counting non-empty segments
makes the code match the rule. Relative ordering among non-root paths is
unchanged, since every one shifts down by exactly one; the only selection
outcome that changes is a catalog that declares "/" itself as a project root.
A genuine tie between two entries sharing the deepest root still stops with
project_required. The fix serves rig context work-install and operating
posture, which both call inferCatalogProject.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c387c94b-3f35-4e19-991c-eb9ff247e769
📥 Commits

Reviewing files that changed from the base of the PR and between 7a61054 and 75ead3f.

📒 Files selected for processing (2)
  • packages/daemon/src/domain/workspace/project-catalog.ts
  • packages/daemon/test/project-catalog-root-depth.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The project catalog now measures working-directory containment depth by counting nonempty path components. Tests cover root selection, tied deepest roots, and nested roots.

Changes

Project catalog selection

Layer / File(s) Summary
Containment depth and selection tests
packages/daemon/src/domain/workspace/project-catalog.ts, packages/daemon/test/project-catalog-root-depth.test.ts
Depth calculation excludes root separators. Tests check selection of the deepest root and preserve the project_required error when roots tie.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 75ead

Project selection appears ready to merge after normal checks; no unresolved issue is identified in this change.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: counting non-empty path segments to measure project-root depth.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mvschwarz

Copy link
Copy Markdown
Owner

Thanks, @mv-schwarz, for counting only non-empty segments for project-root depth, so a catalog that declares / beside a deeper root picks the deeper one instead of stopping with project_required as if they tied. We've got it, and it's queued with other PRs until the current release is cut. We'll reply here with the outcome.

@openrig-review openrig-review left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved at 75ead3f. A one-line fix: project-root depth now counts non-empty path segments. Before, the filesystem root / measured the same depth as /a, so a catalog declaring both reported a tie from inside /a and asked for --project, although the documented rule (the deepest declared root that contains the working directory) already answers it. Ordering among all other roots is unchanged, a genuine tie still returns project_required, and the change adds no refusal while removing a spurious one. Mocking node:fs in the regression test is the right call, since / cannot be built as a real temp root, and its two other cases guard the tie and nested ordering. Thank you for the clear verification notes, including the pre-existing test failures on your machine.

— dev60-planner@v-openrig-build

@mvschwarz
mvschwarz merged commit 6e8a6d9 into mvschwarz:main Oct 7, 2026
10 checks passed
@mvschwarz

Copy link
Copy Markdown
Owner

Merged. Thanks, @mv-schwarz, for counting only non-empty segments for project-root depth. It's on main now, not in a release yet.

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