diff --git a/bin/gstack-learnings-search b/bin/gstack-learnings-search index 665be6fc1..51c461f7c 100755 --- a/bin/gstack-learnings-search +++ b/bin/gstack-learnings-search @@ -90,10 +90,13 @@ for (const taggedLine of lines) { const isCrossProject = sourceTag === 'cross'; e._crossProject = isCrossProject; - // Trust gate: cross-project learnings only loaded if trusted (user-stated) + // Trust gate: cross-project learnings only loaded if trusted (user-stated). // This prevents prompt injection from one project's AI-generated learnings // silently influencing reviews in another project. - if (isCrossProject && e.trusted === false) continue; + // #1745: this is an ALLOWLIST, not a denylist. `=== false` admitted any row + // where `trusted` is missing/undefined (legacy rows written before the field + // existed, hand-edited rows, rows from other tools). Require trusted === true. + if (isCrossProject && e.trusted !== true) continue; entries.push(e); } catch {} diff --git a/test/gstack-learnings-search.test.ts b/test/gstack-learnings-search.test.ts index bef562598..489e52607 100644 --- a/test/gstack-learnings-search.test.ts +++ b/test/gstack-learnings-search.test.ts @@ -33,6 +33,9 @@ beforeAll(() => { const otherEntries = [ { ts: '2026-05-04T00:00:00Z', skill: 'test', type: 'pattern', key: 'foreign-observed', insight: 'A foreign observed insight', confidence: 8, source: 'observed', trusted: false, files: [] }, { ts: '2026-05-05T00:00:00Z', skill: 'test', type: 'pattern', key: 'foreign-user', insight: 'A foreign user-stated insight', confidence: 8, source: 'user-stated', trusted: true, files: [] }, + // #1745: legacy row with NO `trusted` field at all (written before the field + // existed). The old `=== false` denylist admitted these; the allowlist must exclude. + { ts: '2026-05-06T00:00:00Z', skill: 'test', type: 'pattern', key: 'foreign-legacy', insight: 'A foreign legacy insight with no trusted field', confidence: 8, source: 'observed', files: [] }, ]; fs.writeFileSync(path.join(projDir, 'learnings.jsonl'), entries.map(e => JSON.stringify(e)).join('\n') + '\n'); fs.writeFileSync(path.join(otherProjDir, 'learnings.jsonl'), otherEntries.map(e => JSON.stringify(e)).join('\n') + '\n'); @@ -79,4 +82,11 @@ describe('gstack-learnings-search cross-project trust gating', () => { expect(out).toContain('[cross-project]'); expect(out).not.toContain('foreign-observed'); }); + + // #1745: the gate is an allowlist, not a denylist. A cross-project row with no + // `trusted` field (legacy / hand-edited / other-tool) must NOT be imported. + test('cross-project mode excludes foreign rows missing the trusted field (#1745)', () => { + const out = run(['--cross-project', '--query', 'foreign']); + expect(out).not.toContain('foreign-legacy'); + }); });