Skip to content

fix(task): resolve monorepo task names from subdirectories - #11941

Merged
jdx merged 3 commits into
jdx:mainfrom
pikeas:fix/monorepo-deep-cwd-task-resolution
Aug 13, 2026
Merged

jdx merged 3 commits into
jdx:mainfrom
pikeas:fix/monorepo-deep-cwd-task-resolution

Conversation

@pikeas

@pikeas pikeas commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Problem

In monorepo mode, running a task by its bare or :-prefixed name only works when the cwd is exactly a config root. From any directory below one it fails:

$ cd mono/proj/deep/deeper
$ mise run hello-mono
Error: no task //proj/deep/deeper:hello-mono found
      Did you mean one of these?
        - //proj:hello-mono

This contradicts non-monorepo mode (which has always walked up to the nearest mise.toml), monorepo config/env discovery (which walks up correctly — mise env in the same directory shows the project's [env]), and the docs ("The leading : is optional when running tasks from subdirectories"). Absolute forms (//proj:task, //.../proj:task, //...:task) already work from any depth.

Root cause

expand_colon_task_syntax (src/task/task_load_context.rs) converted the cwd to a task-path prefix verbatim via monorepo_scope(&monorepo_root, cwd) — a pure strip_prefix, no upward walk. The loader names tasks on a different basis: load_local_tasks_with_context walks all_dirs() upward from the cwd and prefixes each config's tasks with the defining directory's scope. The two only agree when the cwd is itself the defining directory — the one case that worked. The error message is self-diagnosing: mise had already loaded //proj:hello-mono while asking for //proj/deep/deeper:hello-mono.

Fix

Scope bare and :-prefixed names to the nearest project root at or above the cwd, instead of to the cwd itself:

let scope_dir: &Path = if cwd.starts_with(&monorepo_root) {
    let roots: Vec<PathBuf> = config
        .config_files
        .values()
        .filter_map(|cf| cf.project_root())
        .chain(config.monorepo_config_root_dirs(None).unwrap_or_default())
        .collect();
    cwd.ancestors()
        .find(|a| *a == monorepo_root || roots.iter().any(|r| r == a))
        .unwrap_or(cwd)
} else {
    cwd
};

A "project root" here is the union of two sources, and both are load-bearing:

  • Directories of configs in scope (nearest-first ConfigMap, the same upward walk the loader uses to name tasks). Needed because a mise.toml in a subdirectory not listed in [monorepo].config_roots still gets its own namespace from the loader (//proj/nested:nested-task) — a name typed in proj/nested/inner must resolve to //proj/nested, not the outer project.
  • Declared [monorepo].config_roots (resolved via monorepo_config_root_dirs(None), which matches any existing directory). Needed because a declared project may contain only file tasks (mise-tasks/) and no config file at all — the loader still names its tasks //projects/frontend:build (covered by e2e/tasks/test_task_non_executable_error).

The walk is bounded by the monorepo root, which counts as a root itself, and a cwd outside the monorepo is left as-is, preserving every existing error path.

Alternatives considered

  • Reuse Config::project_root alone: one line, and correct for every config-file case — but it misses declared config roots that hold only file tasks (caught by test_task_non_executable_error in CI).
  • Expand [monorepo].config_roots alone: wrong in the other direction — unlisted nested configs resolve to the outer project, which lacks the task.
  • Retry the matcher with successively shorter path prefixes: changes matching semantics globally; would let //a/b:t silently resolve to //a:t, weakening the absolute forms, which must stay exact.
  • Reset the cwd to the config root before expansion: cwd is load-bearing elsewhere ({{cwd}}, relative script paths); far wider blast radius.

Risk and compatibility

  • Only expand_colon_task_syntax changed; all its callers (mise run, bare shorthand, tasks info, tasks deps, cache task) gain the same resolution consistently.
  • depends (incl. ./...) is structurally unaffected: dependency patterns resolve via resolve_task_pattern from the declaring task's name, never the cwd. Its e2e coverage passes.
  • Absolute // forms return early before this code; non-monorepo mode returns early on monorepo_root.is_none().
  • Cwd exactly at a config root or the monorepo root: project_root == cwd, so the scope is byte-identical to before (verified — identical task names and cwds in all control cases).
  • One intentional change beyond the bug: a cwd inside the monorepo but below no config root previously expanded to a guaranteed-miss literal name; it now resolves to the monorepo root's // scope. An error case starts working; no previously-working command changes meaning.

Tests

  • e2e/tasks/test_task_shorthand_monorepo: deep-subdir bare + :name; a directory outside all configured projects resolving to root tasks.
  • e2e/tasks/test_task_monorepo_optional_colon: deep-subdir bare + :name including dependency execution; absolute //app:build and //...:build from a deep cwd; a monorepo-root subdir below no config root; nearest-root-wins for an unlisted nested config, including precedence over an ancestor config defining the same task name. These fail on the pre-fix binary (at the first new assertion, with no task //app/deep/deeper:build found), so they are regression tests rather than tautologies.
  • The file-task-only declared-root case is covered by the existing e2e/tasks/test_task_non_executable_error, which runs :build from a project that has mise-tasks/ but no config file.
  • Docs: docs/tasks/monorepo.md now states :task works from any directory below a config_root and resolves to the nearest enclosing one.

Verified additionally: 13-case repro matrix (classic-mode control, config-root/monorepo-root controls, deep-cwd cases, absolute forms from deep cwd, below-no-config-root, unlisted-nested-config) — all pass; cargo clippy --locked --bin mise -- -D warnings clean; cargo fmt --check clean; targeted monorepo e2e suite incl. the ./... depends-scoping tests passes.

🤖 Generated with Claude Code

Bare and `:`-prefixed task names only resolved when the cwd was exactly a
config root: expand_colon_task_syntax turned the cwd into a task-path prefix
verbatim, while the loader names tasks after the defining config's directory.
Scope the name to Config::project_root (the nearest config root above the
cwd) instead, falling back to the cwd when it lies outside the selected
monorepo. Absolute //-forms, depends resolution, and non-monorepo mode are
unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 72bfd423-f419-449c-8ad5-c8e3af384476

📥 Commits

Reviewing files that changed from the base of the PR and between 00ba658 and d54362d.

📒 Files selected for processing (2)
  • src/config/mod.rs
  • src/task/task_load_context.rs

📝 Walkthrough

Walkthrough

Task expansion now resolves names from nested directories against the nearest enclosing configuration root. It falls back to the current directory when no matching root exists. Documentation and end-to-end tests cover both task syntaxes and resolution cases.

Changes

Monorepo task scope

Layer / File(s) Summary
Configuration-root scope resolution
src/config/mod.rs, src/task/task_load_context.rs, docs/tasks/monorepo.md
expand_colon_task_syntax now selects the nearest applicable project or monorepo configuration root. The configuration-root method is available within the crate. Documentation describes the resolution behavior.
Nested task resolution coverage
e2e/tasks/test_task_monorepo_optional_colon, e2e/tasks/test_task_shorthand_monorepo
End-to-end tests cover nested project tasks, monorepo-root fallback, absolute references, and precedence from nested mise.toml files.

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

Mergeability Score: ⚪ Minimal · up to d5436

This change fixes task-name resolution from nested monorepo directories while preserving existing absolute and non-monorepo behavior, with targeted regression coverage and validation reported. No actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant WorkingDirectory
  participant TaskExpansion
  participant ConfigRoots
  participant TaskLoader
  WorkingDirectory->>TaskExpansion: request bare or colon-prefixed task
  TaskExpansion->>ConfigRoots: collect enclosing project and monorepo roots
  ConfigRoots-->>TaskExpansion: return nearest matching root
  TaskExpansion->>TaskLoader: expand task within selected root
  TaskLoader-->>TaskExpansion: return resolved task
Loading

Possibly related PRs

  • jdx/mise#11112: Both changes modify monorepo task discovery and configuration-root precedence.
  • jdx/mise#11180: Both changes modify expand_colon_task_syntax and monorepo task-root scoping.
  • jdx/mise#11283: Both changes modify monorepo task-resolution behavior and related tests.

Suggested reviewers: jdx, risu729

Poem

A rabbit hops through nested roots,
And finds the task where config suits.
Bare or colon, the paths align,
The nearest root becomes the sign.
Outside, the monorepo calls—
Tasks echo through its halls.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: resolving monorepo task names from subdirectories.

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.

@greptile-apps

greptile-apps Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR updates monorepo task-name expansion so bare and colon-prefixed names use the nearest enclosing task namespace rather than the current directory itself.

  • Adds namespace selection across discovered project roots and declared monorepo config roots.
  • Adds end-to-end coverage for deep directories, root fallback, absolute task names, dependencies, and nested configurations.
  • Documents task resolution below configured project roots.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/task/task_load_context.rs Changes bare and colon-prefixed task expansion to select the nearest enclosing project or declared monorepo root.
src/config/mod.rs Exposes monorepo config-root discovery within the crate for task namespace selection.
e2e/tasks/test_task_monorepo_optional_colon Adds coverage for deep-directory resolution, absolute forms, root fallback, dependencies, and nearest nested configuration precedence.
e2e/tasks/test_task_shorthand_monorepo Adds shorthand task coverage below configured and unconfigured monorepo directories.
docs/tasks/monorepo.md Documents nearest-enclosing-root resolution and monorepo-root fallback.

Reviews (3): Last reviewed commit: "fix(task): include declared config_roots..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
e2e/tasks/test_task_monorepo_optional_colon (1)

178-188: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add an ancestor conflict to prove nearest-root precedence.

The fixture defines nested-only only in services/nested/mise.toml. This proves nested-config discovery, but it does not prove that the nested task wins over a same-named task in services/mise.toml.

Add a conflicting ancestor task and assert that its output is absent. This makes the “nearest enclosing config root wins” claim executable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/tasks/test_task_monorepo_optional_colon` around lines 178 - 188, Add a
same-named conflicting task to the ancestor services/mise.toml fixture, then
extend Test 12’s assertions from services/nested/inner to verify both task
invocations execute the nested task output and exclude the ancestor task output,
preserving nearest-root precedence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@e2e/tasks/test_task_monorepo_optional_colon`:
- Around line 178-188: Add a same-named conflicting task to the ancestor
services/mise.toml fixture, then extend Test 12’s assertions from
services/nested/inner to verify both task invocations execute the nested task
output and exclude the ancestor task output, preserving nearest-root precedence.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: ddf2cc11-dc8f-49f8-bc07-836a389d2d74

📥 Commits

Reviewing files that changed from the base of the PR and between 477daf9 and f6aa004.

📒 Files selected for processing (4)
  • docs/tasks/monorepo.md
  • e2e/tasks/test_task_monorepo_optional_colon
  • e2e/tasks/test_task_shorthand_monorepo
  • src/task/task_load_context.rs

pikeas and others added 2 commits August 12, 2026 21:26
Addresses review: the nested-config case now defines the same task in an
ancestor config and asserts the nearest one runs and the ancestor's does not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A project listed in [monorepo].config_roots may hold only file tasks and no
config file (e2e: test_task_non_executable_error), so Config::project_root
alone misses it. Resolve bare and `:`-prefixed names against the nearest cwd
ancestor that is either a config-file root or a declared config root, bounded
by the monorepo root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jdx
jdx merged commit 84579c0 into jdx:main Aug 13, 2026
31 checks passed
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.

2 participants