From 763acfbce018b734fe026fb22fc4cae194f7be99 Mon Sep 17 00:00:00 2001 From: Yann Roberto <1922498827@qq.com> Date: Sat, 10 Oct 2026 11:57:53 +0800 Subject: [PATCH] fix(context): require scoped evidence for automatic skill loading Keep follow-up questions and reported instructions on the proposal path by requiring recognized scopes and a direct request prefix for exact admission. Preserve grouped rejections, personal negation, and genuine directives around indirect discussion. Invalidate v7 receipts with routing policy v8. Cover both Greptile reproductions and neighboring resolver/launcher cases, including unlisted wording, nested reports, and grouped negative lists. --- scripts/lib/context-selection.js | 34 +++++++++++++------- tests/lib/context-profile-launch.test.js | 15 +++++++++ tests/lib/context-selection.test.js | 40 ++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/scripts/lib/context-selection.js b/scripts/lib/context-selection.js index 6a312f78a..402ffcf9d 100644 --- a/scripts/lib/context-selection.js +++ b/scripts/lib/context-selection.js @@ -31,13 +31,20 @@ const FALLBACK_MARGIN = 1.1; // v5: indirect name citations cannot bypass review through exact or scored admission. // v6: ordered directives and singular named references share the same admission guard. // v7: independent directives scope negation and imperatives allow question punctuation. -const ROUTING_POLICY_VERSION = 7; +// v8: only recognized request/rejection scopes can authorize coordinated directives. +const ROUTING_POLICY_VERSION = 8; const TASK_KEYS = new Set(['sessionId', 'taskId', 'revision', 'phase', 'query', 'explicitIds', 'proposedIds', 'noWorkflow']); const DIRECTIVE_VERB = /\b(use|apply|invoke|run|follow|load)\s+(the\s+)?/i; const DIRECTIVE_REQUEST = /^(?:please )?(?:(?:can|could|would|will) you (?:please )?|do )?(?:use|apply|invoke|run|follow|load)\b/; -const QUESTION_START = /^(?!do not\b)(can|could|would|should|may|might|do|does|did|is|are|why|how|what|when|where|which)\b/; -const INSTRUCTION_BOUNDARY = /(?:,\s*(?:(?:and|but)\s+)?|\s+(?:and|but)\s+)(?=(?:please\s+)?(?:do\s+(?:not\s+)?|not\s+|never\s+)?(?:use|apply|invoke|run|follow|load)\b|(?:can|could|would|should|may|might|do|does|did|is|are|why|how|what|when|where|which)\b)/i; +const DIRECTIVE_REJECTION = /^(?:please )?(?:(?:(?:do )?not|never) (?:use|apply|invoke|run|follow|load)\b|avoid\b)/; +const DIRECTIVE_NAME_PREFIX = new RegExp(DIRECTIVE_REQUEST.source + '\\s+(?:the\\s+)?(?:skill\\s*)?$'); +const QUESTION_START = /^(?!do not\b)(can|could|would|should|shall|will|may|might|do|does|did|is|are|why|how|what|when|where|which)\b/; +const REPORTED_INSTRUCTION = /\b(say|says|said|reads|told|mentions?|quoted?|document|states?|stated|recommends?|recommended|asserts?|asserted)\b/; +// Rejections keep their whole list until another instruction/question begins. +const INSTRUCTION_BOUNDARY = /(?:,\s*(?:(?:and|but)\s+)?|\s+(?:and|but)\s+)(?=(?:please\s+)?(?:do\s+(?:not\s+)?|not\s+|never\s+)?(?:use|apply|invoke|run|follow|load)\b|(?:can|could|would|should|shall|will|may|might|do|does|did|is|are|why|how|what|when|where|which)\b)/i; +// Admission additionally checks intervening scopes, including unknown wording. +const ADMISSION_BOUNDARY = /(?:,\s*(?:(?:and|but)\s+)?|\s+(?:and|but)\s+)(?=\S)/i; /** Normalize citation names while preserving contracted negation as a separate word. */ const normalizedQueryName = text => text.replace(/n['\u2019]t\b/gi, ' not') .replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/_/g, ' ') @@ -76,22 +83,25 @@ function citationFor(candidate, query, nativeName) { const text = clause.split(',').map(normalizedQueryName).join(', '); const mentions = aliases.flatMap(({ name, exact }) => [...text.matchAll(new RegExp('\\b' + name + '\\b', 'g'))] .map(mention => ({ index: mention.index, exact }))).sort((a, b) => a.index - b.index); - const request = DIRECTIVE_REQUEST.test(text); - const interrogative = !request && QUESTION_START.test(text); for (const mention of mentions) { const prefix = text.slice(0, mention.index); - // A directive or question after a connector starts its own scope. - // Lists without another verb and reported speech keep their guards. + const scopes = prefix.split(ADMISSION_BOUNDARY); const instructionPrefix = prefix.split(INSTRUCTION_BOUNDARY).at(-1); - const question = interrogative || (!DIRECTIVE_REQUEST.test(instructionPrefix) - && (QUESTION_START.test(instructionPrefix) || (!request && clause.includes('?')))); - if (question || /^\s*["'\u201c\u2018\x60]/.test(clause) - || /\b(say|says|said|reads|told|mentions?|quoted?|document)\b/.test(prefix)) { + // Unknown scopes cannot authorize a later "and use ...". Positive scope + // evidence is required for admission, while existing rejection forms remain + // usable and indirect discussion cannot withdraw an earlier directive. + const instruction = scopes.every(scope => DIRECTIVE_REQUEST.test(scope) || DIRECTIVE_REJECTION.test(scope)); + const question = (!DIRECTIVE_REQUEST.test(text) && QUESTION_START.test(text)) + || (!DIRECTIVE_REQUEST.test(instructionPrefix) + && (QUESTION_START.test(instructionPrefix) || clause.includes('?'))); + if (question || REPORTED_INSTRUCTION.test(prefix) || /^\s*["'\u201c\u2018\x60]/.test(clause)) { if (citation === 'none') citation = 'indirect'; } else if (/\b(do not|never|no|not|avoid)\b/.test(instructionPrefix)) { citation = 'indirect'; + } else if (!instruction) { + if (citation === 'none') citation = 'indirect'; } else if (mention.exact && candidate.exact - && new RegExp(DIRECTIVE_VERB.source + '(skill\\s*:?\\s*)?$', 'i').test(prefix)) { + && DIRECTIVE_NAME_PREFIX.test(scopes.at(-1))) { citation = 'directive'; } } diff --git a/tests/lib/context-profile-launch.test.js b/tests/lib/context-profile-launch.test.js index 681da8fd0..7193bf83e 100644 --- a/tests/lib/context-profile-launch.test.js +++ b/tests/lib/context-profile-launch.test.js @@ -160,9 +160,17 @@ for (const query of ["Don't use the feature skill.", 'Don\u2019t use the feature 'Can I use the feature skill?', 'The README says use the feature skill.', 'The docs say use feature.', 'The docs say do not use feature and use shared guidance.', 'The docs say do not use feature, use shared guidance, okay?', + 'The spec states do not use feature and use shared guidance.', + 'The spec stated do not use feature, use shared guidance.', + 'The spec recommends we do not use feature and use shared guidance.', + 'The spec asserts we should not use feature and use shared guidance.', + 'Use standard tools, the spec stipulates do not use feature and use shared guidance.', 'Should we avoid feature and use shared guidance?', 'Should we avoid feature, use shared guidance?', 'Use standard tools, should we use feature?', + 'Use standard tools, shall we use feature?', + 'Use standard tools, will we use feature?', + 'Use standard tools, ought we to use feature?', 'Please use the existing code, can I use feature?', 'Use standard tools and should we use feature?', 'Use standard tools but should we use feature?', @@ -182,7 +190,9 @@ for (const query of ["Don't use the feature skill.", 'Don\u2019t use the feature } }); assert.deepEqual(phases, ['selection', 'task']); assert.equal(result.routingCalls, 1); + assert.deepEqual(result.selection.selectedIds, []); assert.deepEqual(result.selection.loadedIds, []); + assert.equal(result.selection.fallback, null); assert.equal(result.selection.reason, 'agent-declined-selection'); })); } @@ -201,6 +211,11 @@ for (const [query, skill] of [ ['Use feature?', 'feature'], ['Please use feature?', 'feature'], ['Use feature to fix the bug, okay?', 'feature'], + ['Use feature. Shall we use feature?', 'feature'], + ['Use feature, shall we use shared guidance?', 'feature'], + ['Shall we use shared guidance? Use feature.', 'feature'], + ['Use feature. The spec states do not use feature and use shared guidance.', 'feature'], + ['The spec states do not use feature and use shared guidance. Use feature.', 'feature'], ]) { test('an unambiguous final directive injects context without a proposal: ' + query, () => withFixture(repoRoot => { const phases = []; diff --git a/tests/lib/context-selection.test.js b/tests/lib/context-selection.test.js index d55b50868..ef6e2c4af 100644 --- a/tests/lib/context-selection.test.js +++ b/tests/lib/context-selection.test.js @@ -371,6 +371,13 @@ for (const query of [ 'The docs say do not use feature and use shared guidance.', 'The docs say do not use feature, use shared guidance.', 'The docs say do not use feature, use shared guidance, okay?', + 'Use standard tools, the spec states do not use feature and use shared guidance.', + 'The spec states do not use feature and use shared guidance.', + 'The spec stated do not use feature, use shared guidance.', + 'The spec recommends we do not use feature and use shared guidance.', + 'The spec asserts we should not use feature and use shared guidance.', + 'Use standard tools, the spec stipulates do not use feature and use shared guidance.', + 'Use the documentation that says do not use feature and use shared guidance.', 'Should we avoid feature and use shared guidance?', 'Should we avoid feature, use shared guidance?', 'Do not use feature and shared guidance.', @@ -388,6 +395,9 @@ for (const query of [ for (const query of [ 'Use standard tools, should we use feature?', + 'Use standard tools, shall we use feature?', + 'Use standard tools, will we use feature?', + 'Use standard tools, ought we to use feature?', 'Please use the existing code, can I use feature?', 'Use standard tools and should we use feature?', 'Use standard tools but should we use feature?', @@ -437,13 +447,13 @@ for (const prefix of ["Don't use database-migrations.", 'Do not use database-mig } test('routing policy changes invalidate receipts created by the old citation policy', () => withFixture(repoRoot => { - const query = "Don't use feature."; + const query = 'Use standard tools, shall we use feature?'; const first = resolve(repoRoot, { query, explicitIds: ['skill:feature'] }); const { digestObject } = require('../../scripts/lib/context-profile-support'); const { compileContextProfile } = require('../../scripts/lib/context-profiles'); const plan = compileContextProfile({ repoRoot, selectionMode: 'auto' }); const bindingDigest = digestObject({ sessionId: 'session-1', taskId: 'task-1', revision: 1, - phase: 'implement', planDigest: plan.planDigest, routingPolicyVersion: 6, + phase: 'implement', planDigest: plan.planDigest, routingPolicyVersion: 7, triggersDigest: digestObject({}), queryDigest: digestObject(query) }); const { receiptDigest: _receiptDigest, ...receipt } = first.receipt; const oldReceipt = { ...receipt, bindingDigest, explicitIds: [], @@ -451,7 +461,10 @@ test('routing policy changes invalidate receipts created by the old citation pol const previous = { ...oldReceipt, receiptDigest: digestObject(oldReceipt) }; const result = resolve(repoRoot, { query }, { previous, load: true }); assert.equal(result.reused, false); + assert.notEqual(result.receipt.bindingDigest, previous.bindingDigest); + assert.deepEqual(result.selectedIds, []); assert.deepEqual(result.loadedIds, []); + assert.equal(result.fallback, null); })); test('an old empty selection is reconsidered when directive policy changes', () => withFixture(repoRoot => { @@ -478,8 +491,14 @@ for (const query of [ 'Should we use feature? Use feature.', 'Use feature. Should we use feature?', 'Use feature, should we use feature?', + 'Use feature. Shall we use feature?', + 'Use feature, shall we use shared guidance?', + 'Shall we use shared guidance? Use feature.', 'The docs say use feature. Use feature.', 'Use feature. The docs say use feature.', + 'Use feature. The spec states do not use feature and use shared guidance.', + 'Use feature. Use the warning that says never use feature.', + 'The spec states do not use feature and use shared guidance. Use feature.', 'The phrase "Use feature." is an example. Use feature.', 'The docs say "Use feature." Use feature.', 'The docs say "Should we use feature?" Use feature.', @@ -503,6 +522,11 @@ for (const query of [ for (const query of [ "Use feature. Don't use feature.", + 'Use feature. Avoid feature.', + 'Use feature. You should not use feature.', + 'Use feature. You shouldn\u2019t use feature.', + 'Use feature, no feature.', + 'Use feature. For this task, do not use feature.', "Use feature, don't use feature.", "Use feature and don't use feature.", "Use feature but don't use feature.", @@ -516,6 +540,18 @@ for (const query of [ })); } +test('a later grouped rejection withdraws a previously requested skill', () => withFixture(repoRoot => { + for (const query of ["Use shared. Don't use feature and shared guidance.", + 'Use shared. Do not use feature and our shared guidance.', + 'Use shared. Do not use feature and the existing shared guidance.', + 'Use shared. Do not use feature, manual, and shared guidance.', + "Use shared. Don't use feature and the shared skill."]) { + const result = resolve(repoRoot, { query }, { load: true }); + assert.deepEqual(result.loadedIds, [], query); + assert.equal(result.fallback, null); + } +})); + for (const prefix of [ "Don't use the database migration skill.", 'Don\u2019t use the database migration skill.',