Fix: honor --target for nested products in non-monorepo repos (#377)

* Fix: honor --target for nested products in non-monorepo repos

Closes #376. Resolve projectRoot from the target path when no monorepo
marker is present, and inherit missing context files from the repo root
when the active project is nested below it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Recognize nested-product context in .agents/context/ and docs/ fallback dirs

Addresses PR #377 review: nearestTargetContextRoot only matched canonical
PRODUCT.md/DESIGN.md directly in a directory, so nested products keeping
context in the documented fallback locations were never selected. Reuse
resolveLocalContextDir so the walk honors the same lookup order.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Abdul Wahab
2026-07-17 12:13:12 -07:00
committed by GitHub
co-authored by Cursor
parent 8259c28209
commit 79d5294765
2 changed files with 109 additions and 6 deletions
+36 -4
View File
@@ -7,9 +7,13 @@
* using the existing code as context.
*
* Path resolution (first match wins):
* 1. Active project root, if PRODUCT.md or DESIGN.md is there
* 1. Active project root, if PRODUCT.md or DESIGN.md is there. An explicit
* --target selects the active project: the workspace child in a
* monorepo, or the nearest directory around the target carrying
* canonical context files in an ordinary repo (issue #376).
* 2. Active project .agents/context/ then docs/
* 3. Monorepo root context, using the same order, as a per-file fallback
* 3. Repo root context, using the same order, as a per-file fallback
* whenever the active project is nested below it
* 4. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user
* escape hatch, only consulted when defaults are empty
* 5. Active project root as a "nothing found" default
@@ -88,7 +92,10 @@ function resolveContext(cwd = process.cwd(), options = {}) {
const absCwd = path.resolve(cwd);
const project = resolveProject(absCwd, options);
const projectContextDir = resolveLocalContextDir(project.projectRoot);
const rootContextDir = project.isMonorepo && project.repoRoot !== project.projectRoot
// Per-file inheritance from the repo root whenever the active project is
// nested below it: monorepo workspace children and explicit-target nested
// products in ordinary repos behave the same way.
const rootContextDir = project.repoRoot !== project.projectRoot
? resolveLocalContextDir(project.repoRoot)
: null;
@@ -165,7 +172,7 @@ function resolveProject(cwd = process.cwd(), options = {}) {
if (!repoRoot) {
return {
targetDir,
projectRoot: absCwd,
projectRoot: nearestTargetContextRoot(absCwd, targetDir) || absCwd,
repoRoot: absCwd,
isMonorepo: false,
};
@@ -471,6 +478,31 @@ function isExcludedByWorkspacePattern(relSegments, patterns) {
});
}
// An explicit --target in an ordinary (non-monorepo) repository must still
// select a nested product's own context (issue #376). Walk from the target up
// to — but not including — the invocation root and return the nearest
// directory carrying context files, in the canonical spot or a fallback dir
// (resolveLocalContextDir covers both). Context files only, not package.json:
// without the monorepo root-context fallback, a package.json marker would
// strand targets inside plain subpackages away from the root PRODUCT.md. The
// cwd's own fallback context dirs (.agents/context, docs) hold the root
// project's context, not a nested product, so they never count.
// Returns null when nothing nested is found, keeping the cwd default.
function nearestTargetContextRoot(absCwd, targetDir) {
if (!isPathInside(targetDir, absCwd)) return null;
const rootFallbackDirs = FALLBACK_DIRS.map((rel) => path.resolve(absCwd, rel));
let dir = path.resolve(targetDir);
while (dir && dir !== absCwd) {
if (!rootFallbackDirs.includes(dir) && resolveLocalContextDir(dir)) {
return dir;
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return null;
}
function nearestProjectLikeRoot(repoRoot, targetDir) {
let dir = path.resolve(targetDir);
const stop = path.resolve(repoRoot);
+73 -2
View File
@@ -278,11 +278,12 @@ describe('loadContext (monorepo project context)', () => {
it('does not reuse stale project resolution after workspace markers change', () => {
write('PRODUCT.md', '# Root product\n');
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
write('apps/dashboard/package.json', JSON.stringify({ name: 'dashboard' }, null, 2));
write('apps/dashboard/src/App.jsx', 'export default null;\n');
const before = loadContext(scratch, { targetPath: 'apps/dashboard/src/App.jsx' });
assert.equal(before.projectRoot, scratch);
assert.equal(before.isMonorepo, false);
assert.match(before.product, /Root product/);
write('package.json', JSON.stringify({
@@ -292,7 +293,77 @@ describe('loadContext (monorepo project context)', () => {
const after = loadContext(scratch, { targetPath: 'apps/dashboard/src/App.jsx' });
assert.equal(after.projectRoot, path.join(scratch, 'apps', 'dashboard'));
assert.match(after.product, /Dashboard product/);
assert.equal(after.isMonorepo, true);
assert.match(after.product, /Root product/);
});
it('resolves an explicit target onto a nested product context in a non-monorepo repo', () => {
write('nested/product/PRODUCT.md', '# Nested product\n');
write('nested/product/DESIGN.md', '# Nested design\n');
write('nested/product/file.ts', 'export const x = 1;\n');
const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
assert.equal(ctx.isMonorepo, false);
assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
assert.equal(ctx.repoRoot, scratch);
assert.match(ctx.product, /Nested product/);
assert.match(ctx.design, /Nested design/);
assert.equal(ctx.productPath, path.join('nested', 'product', 'PRODUCT.md'));
assert.equal(ctx.designPath, path.join('nested', 'product', 'DESIGN.md'));
});
it('resolves a nested target whose context lives in .agents/context/', () => {
write('nested/product/.agents/context/PRODUCT.md', '# Nested product\n');
write('nested/product/file.ts', 'export const x = 1;\n');
const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
assert.equal(ctx.isMonorepo, false);
assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
assert.match(ctx.product, /Nested product/);
assert.equal(ctx.productPath, path.join('nested', 'product', '.agents', 'context', 'PRODUCT.md'));
});
it('resolves a nested target whose context lives in docs/', () => {
write('nested/product/docs/DESIGN.md', '# Nested design\n');
write('nested/product/file.ts', 'export const x = 1;\n');
const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
assert.equal(ctx.isMonorepo, false);
assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
assert.match(ctx.design, /Nested design/);
assert.equal(ctx.designPath, path.join('nested', 'product', 'docs', 'DESIGN.md'));
});
it('does not treat the root fallback context dirs as a nested product', () => {
write('.agents/context/PRODUCT.md', '# Root product\n');
write('.agents/context/notes/file.md', '# Notes\n');
const ctx = loadContext(scratch, { targetPath: '.agents/context/notes/file.md' });
assert.equal(ctx.projectRoot, scratch);
assert.match(ctx.product, /Root product/);
});
it('inherits missing root context per-file for a nested target in a non-monorepo repo', () => {
write('DESIGN.md', '# Root design\n');
write('nested/product/PRODUCT.md', '# Nested product\n');
write('nested/product/file.ts', 'export const x = 1;\n');
const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
assert.match(ctx.product, /Nested product/);
assert.match(ctx.design, /Root design/);
assert.equal(ctx.designPath, 'DESIGN.md');
});
it('keeps the repo root project for targets without nearby context files', () => {
write('PRODUCT.md', '# Root product\n');
write('src/App.jsx', 'export default null;\n');
const ctx = loadContext(scratch, { targetPath: 'src/App.jsx' });
assert.equal(ctx.isMonorepo, false);
assert.equal(ctx.projectRoot, scratch);
assert.match(ctx.product, /Root product/);
assert.equal(ctx.productPath, 'PRODUCT.md');
});
it('does not escape a nested git repo to an ancestor workspace', () => {