diff --git a/skill/scripts/context.mjs b/skill/scripts/context.mjs index 096c400cf..6509b00f9 100644 --- a/skill/scripts/context.mjs +++ b/skill/scripts/context.mjs @@ -1299,8 +1299,15 @@ function readBuildPathAt(root) { return value ? { value, source } : null; } +// Roots in precedence order, nearest first: the active workspace decides, and +// the repo root is the fallback a monorepo commits once for every app in it. +// `checkBuildPathUnset` already reads both, so leaving repoRoot out here made +// the two disagree: the finding stayed silent because a value existed while the +// directive never named it, which is the one combination nobody can debug. function appendBuildPathDirective(parts, ctx) { - const roots = [...new Set([ctx?.projectRoot, process.cwd()].filter(Boolean).map((root) => path.resolve(root)))]; + const roots = [...new Set( + [ctx?.projectRoot, process.cwd(), ctx?.repoRoot].filter(Boolean).map((root) => path.resolve(root)), + )]; for (const root of roots) { const found = readBuildPathAt(root); if (!found) continue; diff --git a/skill/scripts/serve-question.mjs b/skill/scripts/serve-question.mjs index 4d972e957..b54d9e483 100644 --- a/skill/scripts/serve-question.mjs +++ b/skill/scripts/serve-question.mjs @@ -161,8 +161,12 @@ function printAnswer(raw) { console.log('FOLLOWUP OPEN: the table stays open and the page is showing a loading hand. Deliver the next round now with --update --key --payload , then collect it with --wait; never leave the page waiting on a round you have not sent.'); } if (a.buildPath === 'comp' || a.buildPath === 'code') { + // The page never writes the flip itself, but "never write it" overstated + // that into a rule the agent then applied to new-work's one-time offer, + // which exists for exactly this case: a flip on a project that had no + // recorded default is the only moment the preference is ever asked for. const origin = a.buildPathFlipped - ? 'flipped on the page, so it binds this session only; never write it to settings' + ? 'flipped on the page, so it binds this session only, and the page never writes it back; the sole exception is new-work’s one-time offer, on a project that had no recorded default at all, which asks after the round closes and writes the answer to .impeccable/config.json' : 'the round’s recorded default'; console.log(`BUILD PATH: ${a.buildPath} (${origin}). ${a.buildPath === 'comp' ? 'Comp-led: the chosen card’s comp is law; generate it before building when it does not exist yet, and the finish review audits the build against it.' diff --git a/tests/context.test.mjs b/tests/context.test.mjs index ea2b55dbf..f6f09d648 100644 --- a/tests/context.test.mjs +++ b/tests/context.test.mjs @@ -1094,6 +1094,36 @@ describe('context.mjs CLI', () => { write('.impeccable/config.json', JSON.stringify({ buildPath: 'code-first' })); assert.equal(run().stdout.includes('BUILD_PATH_DEFAULT'), false); }); + + // A monorepo commits the preference once at the repo root for every app in + // it. Reading only projectRoot left the staleness finding (which does read + // both) silent while the directive never named the value. + describe('in a monorepo', () => { + const writeWorkspace = () => { + write('package.json', JSON.stringify({ private: true, workspaces: ['apps/*'] })); + write('turbo.json', JSON.stringify({ tasks: {} })); + write('PRODUCT.md', '# Root product\n'); + write('apps/dashboard/src/App.jsx', 'export default function App() { return "d"; }\n'); + }; + const runFromWorkspace = () => spawnSync(process.execPath, [SCRIPT_PATH, '--target', 'apps/dashboard/src/App.jsx'], { + cwd: scratch, + encoding: 'utf8', + env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' }, + }); + + it('falls back to the repo-root value for a workspace that sets none', () => { + writeWorkspace(); + write('.impeccable/config.json', JSON.stringify({ buildPath: 'code' })); + assert.match(runFromWorkspace().stdout, /BUILD_PATH_DEFAULT: code/); + }); + + it('lets the workspace override the repo root, nearest root first', () => { + writeWorkspace(); + write('.impeccable/config.json', JSON.stringify({ buildPath: 'code' })); + write('apps/dashboard/.impeccable/config.json', JSON.stringify({ buildPath: 'comp' })); + assert.match(runFromWorkspace().stdout, /BUILD_PATH_DEFAULT: comp/); + }); + }); }); it('keeps the manual-detector directive out of early context when the current provider hook is active', () => {