Isolate explicit targets at Git boundaries

Keep nested repositories and external targets out of caller and home-level context or hook discovery.

AI assistance disclosure: Codex helped implement and test this fix under maintainer direction.
This commit is contained in:
Paul Bakaus
2026-09-02 11:00:33 -07:00
parent f54f7ce3e5
commit 5712d78255
2 changed files with 94 additions and 8 deletions
+17 -3
View File
@@ -200,7 +200,20 @@ export function resolveTargetSelection(cwd = process.cwd(), options = {}) {
function resolveProject(cwd = process.cwd(), options = {}) {
const absCwd = path.resolve(cwd);
const targetDir = resolveTargetDir(absCwd, options);
const hasExplicitTarget = hasTargetOption(options) && targetDir !== absCwd;
const targetGitRoot = hasExplicitTarget ? findGitBoundaryRoot(targetDir) : null;
let repoRoot = findMonorepoRoot(targetDir);
if (!repoRoot && targetGitRoot) {
const cwdGitRoot = findGitBoundaryRoot(absCwd);
if (targetGitRoot !== cwdGitRoot) {
return {
targetDir,
projectRoot: nearestTargetContextRoot(targetGitRoot, targetDir) || targetGitRoot,
repoRoot: targetGitRoot,
isMonorepo: false,
};
}
}
if (!repoRoot && targetDir !== absCwd) {
const cwdRepoRoot = findMonorepoRoot(absCwd);
if (cwdRepoRoot && isPathInside(targetDir, cwdRepoRoot)) {
@@ -212,7 +225,7 @@ function resolveProject(cwd = process.cwd(), options = {}) {
&& targetDir !== absCwd
&& !isPathInside(targetDir, absCwd);
if (targetIsExternal) {
const targetRepoRoot = findGitBoundaryRoot(targetDir) || targetDir;
const targetRepoRoot = targetGitRoot || targetDir;
return {
targetDir,
projectRoot: nearestTargetContextRoot(targetRepoRoot, targetDir) || targetRepoRoot,
@@ -239,8 +252,8 @@ function findGitBoundaryRoot(startDir) {
let dir = path.resolve(startDir);
const homeDir = path.resolve(os.homedir());
while (true) {
if (hasGitBoundary(dir)) return dir;
if (dir === homeDir) return null;
if (hasGitBoundary(dir)) return dir;
const parent = path.dirname(dir);
if (parent === dir) return null;
dir = parent;
@@ -1359,8 +1372,9 @@ function hookManifestSearchRoots(ctx) {
let current = path.resolve(ctx.projectRoot || process.cwd());
const home = path.resolve(os.homedir());
while (true) {
if (current === home) break;
add(current);
if (current === home || hasGitBoundary(current)) break;
if (hasGitBoundary(current)) break;
const parent = path.dirname(current);
if (parent === current) break;
current = parent;
+77 -5
View File
@@ -408,7 +408,7 @@ describe('loadContext (monorepo project context)', () => {
assert.equal(ctx.designPath, null);
});
it('resolves an explicit root target into a nested-git workspace child', () => {
it('keeps an explicit root target inside its nested Git repository', () => {
write('package.json', JSON.stringify({
private: true,
workspaces: ['repos/*'],
@@ -421,13 +421,13 @@ describe('loadContext (monorepo project context)', () => {
const project = path.join(scratch, 'repos', 'standalone');
const ctx = loadContext(scratch, { targetPath: 'repos/standalone/src/App.jsx' });
assert.equal(ctx.isMonorepo, true);
assert.equal(ctx.isMonorepo, false);
assert.equal(ctx.projectRoot, project);
assert.equal(ctx.repoRoot, scratch);
assert.equal(ctx.repoRoot, project);
assert.match(ctx.product, /Standalone product/);
assert.match(ctx.design, /Outer design/);
assert.equal(ctx.design, null);
assert.equal(ctx.productPath, path.join('repos', 'standalone', 'PRODUCT.md'));
assert.equal(ctx.designPath, 'DESIGN.md');
assert.equal(ctx.designPath, null);
});
it('supports double-star workspace patterns by resolving the shallow child project', () => {
@@ -1298,6 +1298,39 @@ describe('context.mjs CLI', () => {
assert.match(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
});
it('treats a markerless nested Git target as an independent repository', () => {
const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts');
stageContextBundle(scripts, { providerId: 'claude-code' });
const repo = path.join(scratch, 'repo');
const target = path.join(repo, 'repos', 'standalone');
fs.mkdirSync(path.join(repo, '.git'), { recursive: true });
fs.mkdirSync(path.join(repo, '.claude'), { recursive: true });
fs.mkdirSync(path.join(target, '.git'), { recursive: true });
fs.mkdirSync(path.join(target, 'src'), { recursive: true });
fs.writeFileSync(path.join(repo, 'package.json'), JSON.stringify({ private: true, workspaces: ['repos/*'] }));
fs.writeFileSync(path.join(repo, 'PRODUCT.md'), '# Outer product\n');
fs.writeFileSync(path.join(repo, '.claude', 'settings.local.json'), JSON.stringify({
hooks: { Stop: [{ hooks: [{ command: 'node .claude/skills/impeccable/scripts/hook.mjs' }] }] },
}));
fs.writeFileSync(path.join(target, 'src', 'App.jsx'), 'export default function App() { return "standalone"; }\n');
const res = spawnSync(process.execPath, [
path.join(scripts, 'context.mjs'),
'--target',
path.join('repos', 'standalone', 'src', 'App.jsx'),
], {
cwd: repo,
encoding: 'utf8',
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
});
assert.equal(res.status, 0, res.stderr);
assert.match(res.stdout, /"projectRoot": ".*\/repos\/standalone"/);
assert.match(res.stdout, /"repoRoot": ".*\/repos\/standalone"/);
assert.doesNotMatch(res.stdout, /# Outer product/);
assert.match(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
});
it('does not borrow the caller hook for a target in an independent sibling repository', () => {
const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts');
stageContextBundle(scripts, { providerId: 'claude-code' });
@@ -1332,6 +1365,45 @@ describe('context.mjs CLI', () => {
assert.match(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
});
it('does not treat a home-directory Git checkout as an external target repository', () => {
const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts');
stageContextBundle(scripts, { providerId: 'claude-code' });
const fakeHome = path.join(scratch, 'home');
const caller = path.join(fakeHome, 'caller');
const target = path.join(fakeHome, 'target');
fs.mkdirSync(path.join(fakeHome, '.git'), { recursive: true });
fs.mkdirSync(path.join(fakeHome, '.claude'), { recursive: true });
fs.mkdirSync(caller, { recursive: true });
fs.mkdirSync(target, { recursive: true });
fs.writeFileSync(path.join(fakeHome, 'PRODUCT.md'), '# Home product\n');
fs.writeFileSync(path.join(fakeHome, '.claude', 'settings.local.json'), JSON.stringify({
hooks: { Stop: [{ hooks: [{ command: 'node .claude/skills/impeccable/scripts/hook.mjs' }] }] },
}));
fs.writeFileSync(path.join(target, 'PRODUCT.md'), '# Target product\n');
const res = spawnSync(process.execPath, [
path.join(scripts, 'context.mjs'),
'--target',
target,
], {
cwd: caller,
encoding: 'utf8',
env: {
...process.env,
HOME: fakeHome,
IMPECCABLE_NO_UPDATE_CHECK: '1',
IMPECCABLE_NO_STALENESS_CHECK: '1',
},
});
assert.equal(res.status, 0, res.stderr);
assert.match(res.stdout, /"projectRoot": ".*\/target"/);
assert.match(res.stdout, /"repoRoot": ".*\/target"/);
assert.match(res.stdout, /# Target product/);
assert.doesNotMatch(res.stdout, /# Home product/);
assert.match(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
});
it('adds no detector directive when a per-edit-only hook is active', () => {
const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts');
stageContextBundle(scripts, { providerId: 'cursor' });