Repository navigation
fix(scheduler): enforce user hold directives by blocking mutating tools at scheduler layer - #29394
dylanyunlon wants to merge 1 commit into
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
|
📊 PR Size: size/XL
|
|
@gemini-code-assist Please review this PR. This addresses issue #26390 (action-bias overriding explicit user hold directives). The core change adds programmatic enforcement at the scheduler layer so that when a user says "don't apply yet" or "explain first", mutating tool calls are blocked before they reach the execution pipeline. Key areas for review:
|
🛑 Action Required: Evaluation ApprovalSteering changes have been detected in this PR. To prevent regressions, a maintainer must approve the evaluation run before this PR can be merged. Maintainers:
Once approved, the evaluation results will be posted here automatically. |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a programmatic mechanism to enforce user hold directives, preventing the agent from executing mutating tool calls when a user explicitly requests to wait, explain first, or review before applying changes. By intercepting these calls at the scheduler layer, the system ensures that negative constraints are respected even when the model's internal bias might otherwise lead to premature actions. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a programmatic Hold Directive Guard mechanism to enforce user hold directives (such as "don't apply yet" or "explain first") by blocking mutating tool calls at the scheduler layer while keeping read-only tools available. Key feedback highlights opportunities to improve robustness and security: supporting standalone wait/hold words, refining negation patterns to prevent false negatives on phrases like "just do the analysis", adopting a secure-by-default approach in isMutatingTool by blocking any tool not explicitly in the read-only allowlist, safely handling and trimming the optional errorMessage in the scheduler, and ensuring consistent state clearing in the non-interactive CLI.
| pattern: | ||
| /\b(?:wait|hold|pause|stop|halt)\b(?:\s+(?:for|until)\s+(?:my|me|user|i\s|further)|\s+(?:before|on|here|there|now|first)|\s*[,.]?\s*(?:don'?t|do\s+not|let me))/i, | ||
| type: HoldDirectiveType.EXPLICIT_WAIT, | ||
| confidence: 0.95, | ||
| }, |
There was a problem hiding this comment.
Standalone directives like "wait", "hold", or "stop" are the most direct way a user might instruct the agent to pause. However, the current regex pattern for EXPLICIT_WAIT requires trailing context words (e.g., "wait for me", "hold on"), meaning a single-word message like "wait" or "hold" will be completely ignored.
Adding a pattern specifically for standalone wait/hold/pause/stop/halt words ensures these direct user commands are programmatically enforced.
{
pattern:
/\b(?:wait|hold|pause|stop|halt)\b(?:\s+(?:for|until)\s+(?:my|me|user|i\s|further)|\s+(?:before|on|here|there|now|first)|\s*[,.]?\s*(?:don'?t|do\s+not|let me))/i,
type: HoldDirectiveType.EXPLICIT_WAIT,
confidence: 0.95,
},
{
pattern: /^(?:wait|hold|pause|stop|halt)[.,!?]*$/i,
type: HoldDirectiveType.EXPLICIT_WAIT,
confidence: 0.95,
},| const NEGATION_PATTERNS: RegExp[] = [ | ||
| /\b(?:don'?t|do\s+not)\s+(?:wait|hold|pause|stop)\b/i, | ||
| /\bjust\s+(?:do|apply|fix|implement|make|go ahead|proceed)\b/i, | ||
| /\bgo\s+ahead\b/i, | ||
| /\bproceed\b/i, | ||
| ]; |
There was a problem hiding this comment.
The negation pattern \bjust\s+(?:do|apply|fix|implement|make|go ahead|proceed)\b is overly broad because of the do keyword. If a user says "just do the analysis, don't apply yet", the phrase "just do" will trigger the negation pattern and completely disable the hold directive detection. This allows the agent to bypass the hold and aggressively mutate the codebase.
Refining the pattern to require "it" or other explicit action targets for "do" (e.g., just do it) prevents these false negatives.
| const NEGATION_PATTERNS: RegExp[] = [ | |
| /\b(?:don'?t|do\s+not)\s+(?:wait|hold|pause|stop)\b/i, | |
| /\bjust\s+(?:do|apply|fix|implement|make|go ahead|proceed)\b/i, | |
| /\bgo\s+ahead\b/i, | |
| /\bproceed\b/i, | |
| ]; | |
| const NEGATION_PATTERNS: RegExp[] = [ | |
| /\b(?:don'?t|do\s+not)\s+(?:wait|hold|pause|stop)\b/i, | |
| /\bjust\s+(?:apply|fix|implement|make|go ahead|proceed)\b|\bjust\s+do\s+it\b/i, | |
| /\bgo\s+ahead\b/i, | |
| /\bproceed\b/i, | |
| ]; |
| export function isMutatingTool(toolName: string): boolean { | ||
| // Also treat invoke_agent as mutating since agents can perform writes. | ||
| return MUTATING_TOOL_NAMES.has(toolName) || toolName === 'invoke_agent'; | ||
| } |
There was a problem hiding this comment.
The current implementation of isMutatingTool relies on checking a hardcoded set of mutating tools (MUTATING_TOOL_NAMES). This approach is fragile and insecure by default:
- Any custom or third-party MCP tools that perform mutations (e.g.,
mcp_filesystem_write_file) will bypass the hold directive guard entirely. - Sibling tools like
tracker_create_taskortracker_update_task(which modify files in the workspace) are not blocked.
To enforce a "secure by default" methodology, we should treat any tool not explicitly listed in the read-only allowlist (READ_ONLY_TOOL_NAMES) as mutating.
| export function isMutatingTool(toolName: string): boolean { | |
| // Also treat invoke_agent as mutating since agents can perform writes. | |
| return MUTATING_TOOL_NAMES.has(toolName) || toolName === 'invoke_agent'; | |
| } | |
| export function isMutatingTool(toolName: string): boolean { | |
| // Secure by default: any tool not explicitly in the read-only allowlist is treated as mutating. | |
| return !READ_ONLY_TOOL_NAMES.has(toolName); | |
| } |
References
- When a core tools allowlist is configured, the default policy for non-allowlisted tools should be
DENYto enforce a 'secure by default' methodology.
| if (holdCheck?.blocked) { | ||
| return { | ||
| status: CoreToolCallStatus.Error, | ||
| request: enrichedRequest, | ||
| response: createErrorResponse( | ||
| enrichedRequest, | ||
| new Error(holdCheck.errorMessage), | ||
| holdCheck.errorType, | ||
| ), |
There was a problem hiding this comment.
Since errorMessage is defined as optional (string | undefined) in the HoldDirectiveCheckResult interface, callers must handle the undefined case. Additionally, when using an optional string with a fallback value, we should trim the optional string and use the fallback if the result is empty to avoid uninformative messages from whitespace-only strings.
| if (holdCheck?.blocked) { | |
| return { | |
| status: CoreToolCallStatus.Error, | |
| request: enrichedRequest, | |
| response: createErrorResponse( | |
| enrichedRequest, | |
| new Error(holdCheck.errorMessage), | |
| holdCheck.errorType, | |
| ), | |
| const holdCheck = holdBlockedCalls.get(request.callId); | |
| if (holdCheck?.blocked) { | |
| return { | |
| status: CoreToolCallStatus.Error, | |
| request: enrichedRequest, | |
| response: createErrorResponse( | |
| enrichedRequest, | |
| new Error(holdCheck.errorMessage?.trim() || 'Tool call blocked by active hold directive'), | |
| holdCheck.errorType, | |
| ), | |
| durationMs: 0, | |
| schedulerId: this.schedulerId, | |
| approvalMode: currentApprovalMode, | |
| } as ErroredToolCall; | |
| } |
References
- When consuming an object, if a property is optional in its type definition (interface), callers must handle the
undefinedcase (e.g., by providing a default with??). Do not rely on the implementation details of the function that creates the object to always provide a value, as this can change. Code against the interface contract. - When using an optional string with a fallback value, trim the optional string and use the fallback if the result is empty to avoid uninformative messages from whitespace-only strings.
| const holdDirective = detectHoldDirective(input); | ||
| if (holdDirective) { | ||
| scheduler.setActiveHoldDirective(holdDirective); | ||
| debugLogger.log( | ||
| `[HoldDirective] Detected "${holdDirective.type}" directive ` + | ||
| `(matched: "${holdDirective.matchedPhrase}"). ` + | ||
| `Mutating tools will be blocked for this turn.`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
For consistency with nonInteractiveCliAgentSession.ts and to prevent any potential stale state if the scheduler instance is ever reused or kept alive across multiple turns/sessions in other contexts, we should explicitly clear the active hold directive when none is detected.
| const holdDirective = detectHoldDirective(input); | |
| if (holdDirective) { | |
| scheduler.setActiveHoldDirective(holdDirective); | |
| debugLogger.log( | |
| `[HoldDirective] Detected "${holdDirective.type}" directive ` + | |
| `(matched: "${holdDirective.matchedPhrase}"). ` + | |
| `Mutating tools will be blocked for this turn.`, | |
| ); | |
| } | |
| const holdDirective = detectHoldDirective(input); | |
| if (holdDirective) { | |
| scheduler.setActiveHoldDirective(holdDirective); | |
| debugLogger.log( | |
| `[HoldDirective] Detected "${holdDirective.type}" directive ` + | |
| `(matched: "${holdDirective.matchedPhrase}"). ` + | |
| `Mutating tools will be blocked for this turn.`, | |
| ); | |
| } else { | |
| scheduler.setActiveHoldDirective(null); | |
| } |
There was a problem hiding this comment.
Code Review
This pull request introduces a programmatic enforcement mechanism for user hold directives (such as "don't apply yet" or "explain first") to prevent the agent from executing mutating tool calls when a hold is active. It adds detection logic in userDirectiveService, integrates a guard at the scheduler layer, updates system prompts to inform the model of this constraint, and includes comprehensive unit and integration tests. The review feedback correctly identifies a critical regex false positive in the NO_CHANGES_YET pattern that could unintentionally block mutating tools for general negative commands, and provides a robust regex suggestion to make the temporal suffix mandatory.
| { | ||
| pattern: | ||
| /\b(?:don'?t|do\s+not|never)\s+(?:apply|make|execute|run|implement|modify|change|write|edit|replace|update|fix|rewrite)\b(?:\s+(?:any|the|those|these|it|that|them))?\b(?:\s+(?:yet|now|until|before|first))?/i, | ||
| type: HoldDirectiveType.NO_CHANGES_YET, | ||
| confidence: 0.95, | ||
| }, |
There was a problem hiding this comment.
Critical Regex False Positive Issue
The current regex pattern for NO_CHANGES_YET makes the temporal/conditional suffix (yet, now, until, before, first) completely optional due to the trailing ?.
This causes the regex to match any negative command containing these verbs (e.g., "do not write duplicate tests", "never run commands as root", "do not change the port"). Because the scheduler programmatically blocks all mutating tools when a hold directive is active, these false positives will completely break the agent's ability to write files or run commands, failing the user's request.
Solution
Make the temporal/conditional suffix mandatory and allow optional intermediate words using [^.]*? (to keep it within the same sentence). This ensures we only match actual hold directives while ignoring general negative constraints.
| { | |
| pattern: | |
| /\b(?:don'?t|do\s+not|never)\s+(?:apply|make|execute|run|implement|modify|change|write|edit|replace|update|fix|rewrite)\b(?:\s+(?:any|the|those|these|it|that|them))?\b(?:\s+(?:yet|now|until|before|first))?/i, | |
| type: HoldDirectiveType.NO_CHANGES_YET, | |
| confidence: 0.95, | |
| }, | |
| { | |
| pattern: | |
| /\b(?:don'?t|do\s+not|never)\s+(?:apply|make|execute|run|implement|modify|change|write|edit|replace|update|fix|rewrite)\b[^.]*?\b(?:yet|now|until|before|first)\b/i, | |
| type: HoldDirectiveType.NO_CHANGES_YET, | |
| confidence: 0.95, | |
| }, |
f843155 to
1d2bc29
Compare
|
All 6 review findings from @gemini-code-assist have been addressed in the force-pushed update: Review #1 (high) - Standalone wait/hold directives: Added dedicated pattern Review #2 (high) - Overly broad "just do" negation: Narrowed from Review #3 (high) - Secure by default for unknown tools: Flipped Review #4 (high) - Optional errorMessage handling: Added Review #5 (high) - Stale hold directive state: Added Review #6 (CRITICAL) - Regex false positives: Made temporal suffix mandatory in All 78 tests pass (77 existing + 1 new for Review #6). E2E CLI run confirmed: |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request implements a programmatic hold directive mechanism that blocks mutating tool calls when a user requests the agent to wait, explain, or review findings first. The implementation includes regex-based detection in userDirectiveService.ts, scheduler-level enforcement via holdDirectiveGuard.ts, and updates to CLI sessions and prompt snippets. The review feedback identifies several critical issues with the regex patterns, specifically pointing out potential false positives from crossing newlines, clause boundaries, or matching negative constraints without temporal suffixes. Additionally, the feedback notes that classifying all shell commands as mutating could incorrectly block non-destructive commands like test execution.
| { | ||
| pattern: | ||
| /\b(?:don'?t|do\s+not)\s+(?:apply|make|execute|run|implement|modify|change|write|edit|replace|update|fix|rewrite)\b[^.]*?\b(?:yet|now|until|before|first|for now)\b/i, | ||
| type: HoldDirectiveType.NO_CHANGES_YET, | ||
| confidence: 0.95, | ||
| }, |
There was a problem hiding this comment.
The regex pattern uses [^.]*? to find a temporal suffix within the same sentence. However, [^.] matches newlines, meaning it can cross line breaks in multi-line chat messages. In chat interfaces, users often use newlines instead of periods to separate thoughts (e.g., "Do not write duplicate tests\nNow, let's implement the feature"). This will cause a false positive match on "Now" on the second line, incorrectly triggering a hold directive and blocking all mutating tools for the entire turn.
To prevent crossing newlines or other sentence boundaries, exclude newlines and other common sentence delimiters from the character class.
| { | |
| pattern: | |
| /\b(?:don'?t|do\s+not)\s+(?:apply|make|execute|run|implement|modify|change|write|edit|replace|update|fix|rewrite)\b[^.]*?\b(?:yet|now|until|before|first|for now)\b/i, | |
| type: HoldDirectiveType.NO_CHANGES_YET, | |
| confidence: 0.95, | |
| }, | |
| { | |
| pattern: | |
| /\b(?:don'?t|do\s+not)\s+(?:apply|make|execute|run|implement|modify|change|write|edit|replace|update|fix|rewrite)\b[^\n.!?]*?\b(?:yet|now|until|before|first|for now)\b/i, | |
| type: HoldDirectiveType.NO_CHANGES_YET, | |
| confidence: 0.95, | |
| }, |
| { | ||
| pattern: | ||
| /\bbut\s+(?:not\s+now|wait|first|hold|don'?t\s+(?:apply|make|change|fix|modify))/i, | ||
| type: HoldDirectiveType.NO_CHANGES_YET, | ||
| confidence: 0.9, | ||
| }, | ||
|
|
There was a problem hiding this comment.
The pattern matches but don't change or but don't make without requiring any temporal suffix (like "yet" or "now"). This causes it to trigger false positives on common negative constraints in user prompts, such as "Fix the bug in auth.ts, but don't change the existing tests" or "Implement the feature, but don't make duplicate files". When triggered, this incorrectly activates a hold directive and programmatically blocks all mutating tools, completely preventing the agent from executing the requested task.
Since the main NO_CHANGES_YET pattern already handles "don't change ... yet" with proper temporal suffixes, we should remove the suffix-less don'?t sub-pattern from this "but" pattern to avoid these false positives.
| { | |
| pattern: | |
| /\bbut\s+(?:not\s+now|wait|first|hold|don'?t\s+(?:apply|make|change|fix|modify))/i, | |
| type: HoldDirectiveType.NO_CHANGES_YET, | |
| confidence: 0.9, | |
| }, | |
| // "but not now", "but wait", "but first explain" | |
| { | |
| pattern: | |
| /\bbut\s+(?:not\s+now|wait|first|hold)/i, | |
| type: HoldDirectiveType.NO_CHANGES_YET, | |
| confidence: 0.9, | |
| }, |
| { | ||
| pattern: | ||
| /\b(?:show|present|give)\s+(?:me\s+)?(?:the\s+)?(?:full\s+)?(?:review|report|analysis|findings|results)\b.*\b(?:before)\b/i, | ||
| type: HoldDirectiveType.REVIEW_FIRST, | ||
| confidence: 0.85, | ||
| }, |
There was a problem hiding this comment.
Using the greedy wildcard .* allows matching across clause or sentence boundaries on the same line. This can lead to false positives on compound sentences where the user actually wants the agent to proceed with an action (e.g., "Show me the report. Before that, let's write the file."). In this case, the agent will match "Show me the report... before" and block the write_file tool, which is the exact action the user requested.
To prevent matching across clause or sentence boundaries, restrict the wildcard to not cross punctuation marks like periods, commas, semicolons, or question marks.
| { | |
| pattern: | |
| /\b(?:show|present|give)\s+(?:me\s+)?(?:the\s+)?(?:full\s+)?(?:review|report|analysis|findings|results)\b.*\b(?:before)\b/i, | |
| type: HoldDirectiveType.REVIEW_FIRST, | |
| confidence: 0.85, | |
| }, | |
| { | |
| pattern: | |
| /\b(?:show|present|give)\s+(?:me\s+)?(?:the\s+)?(?:full\s+)?(?:review|report|analysis|findings|results)\b[^,.;!?\n]*?\b(?:before)\b/i, | |
| type: HoldDirectiveType.REVIEW_FIRST, | |
| confidence: 0.85, | |
| }, |
| { | ||
| pattern: | ||
| /\bi\s+will\s+(?:run|do|apply|check|review|test)\b.*\b(?:first|myself|later|after)\b/i, | ||
| type: HoldDirectiveType.REVIEW_FIRST, | ||
| confidence: 0.85, | ||
| }, |
There was a problem hiding this comment.
Using the greedy wildcard .* allows matching across clause or sentence boundaries on the same line. This can lead to false positives on compound sentences (e.g., "I will do it later, for now just implement the basic structure."). Here, "I will do... later" matches, triggering a hold directive and blocking all mutating tools, which prevents the agent from implementing the requested structure.
To prevent matching across clause or sentence boundaries, restrict the wildcard to not cross punctuation marks like periods, commas, semicolons, or question marks.
| { | |
| pattern: | |
| /\bi\s+will\s+(?:run|do|apply|check|review|test)\b.*\b(?:first|myself|later|after)\b/i, | |
| type: HoldDirectiveType.REVIEW_FIRST, | |
| confidence: 0.85, | |
| }, | |
| { | |
| pattern: | |
| /\bi\s+will\s+(?:run|do|apply|check|review|test)\b[^,.;!?\n]*?\b(?:first|myself|later|after)\b/i, | |
| type: HoldDirectiveType.REVIEW_FIRST, | |
| confidence: 0.85, | |
| }, |
| { | ||
| pattern: | ||
| /\b(?:before|prior\s+to)\s+(?:making|applying|implementing|executing|running)\s+(?:any\s+)?(?:changes|fixes|modifications|updates)\b/i, | ||
| type: HoldDirectiveType.REVIEW_FIRST, | ||
| confidence: 0.9, | ||
| }, |
There was a problem hiding this comment.
This pattern matches phrases like "before making any changes". If a user instructs the agent to "run the tests before making any changes", this pattern will trigger a hold directive. Because run_shell_command is classified as a mutating tool, the scheduler will block the agent from running the tests, completely defeating the user's request.
Consider refining the tool classification or the hold directive guard to allow non-destructive shell commands (like test execution or linting) to run even when a hold directive is active, or document this as a known limitation.
1d2bc29 to
f464440
Compare
|
Round 2 review findings from @gemini-code-assist addressed: Review #7 (high) - Newline crossing: Newlines are now converted to Review #8 (high) - "but don't change" without temporal suffix: Split into two patterns: Review #9 (high) - Greedy Review #10 (high) - Greedy Review #11 (high) - "run tests before making changes" false positive: Pattern now requires Added 6 new tests specifically for these false-positive scenarios. All 84 tests pass. CLI run confirmed: /gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a programmatic 'Hold Directive' mechanism to enforce user constraints (such as 'explain first' or 'don't apply yet') by blocking mutating tool calls at the scheduler layer while keeping read-only tools available. It adds detection logic, scheduler integration, prompt updates, and comprehensive unit and integration tests. The review feedback highlights two critical issues in userDirectiveService.ts: first, the 'i will ...' pattern matches 'later' and 'after', causing false positives that incorrectly block the agent; second, converting newlines to periods during whitespace normalization breaks multi-line directive phrases and triggers false positives on sentence-start patterns. Code suggestions are provided to restrict the pattern matching and preserve newlines during normalization.
| { | ||
| pattern: | ||
| /\bi\s+will\s+(?:run|do|apply|check|review|test)\b[^.\n]*\b(?:first|myself|later|after)\b/i, | ||
| type: HoldDirectiveType.REVIEW_FIRST, | ||
| confidence: 0.85, | ||
| }, |
There was a problem hiding this comment.
The pattern matches later and after in i will ..., which causes false positives when the user says they will do something "later" or "after" (e.g., "I will review the changes later, for now implement the fix"). This incorrectly blocks the agent from acting. We should restrict this pattern to first|myself to only match immediate hold directives.
| { | |
| pattern: | |
| /\bi\s+will\s+(?:run|do|apply|check|review|test)\b[^.\n]*\b(?:first|myself|later|after)\b/i, | |
| type: HoldDirectiveType.REVIEW_FIRST, | |
| confidence: 0.85, | |
| }, | |
| { | |
| pattern: | |
| /\bi\s+will\s+(?:run|do|apply|check|review|test)\b[^.\n]*\b(?:first|myself)\b/i, | |
| type: HoldDirectiveType.REVIEW_FIRST, | |
| confidence: 0.85, | |
| }, |
| // Normalize whitespace for matching. Newlines are treated as sentence | ||
| // boundaries (converted to periods) to prevent cross-line false positives | ||
| // (Round 2 Review #7). | ||
| const normalized = userMessage | ||
| .replace(/\r\n|\r/g, '\n') | ||
| .replace(/\n/g, '. ') | ||
| .replace(/\s+/g, ' ') | ||
| .trim(); |
There was a problem hiding this comment.
Converting newlines (\n) to periods (. ) during normalization breaks multi-line messages where a newline falls inside a directive phrase (e.g., wait\nfor me or explain\nfirst), and causes false positives for sentence-start patterns like (?:^|[.!?]\s+) (e.g., run tests\nbefore making changes). Since the regex patterns already use [^.\n] to prevent crossing sentence and newline boundaries, and \s+ naturally matches newlines for intra-sentence wrapping, we should preserve newlines and only normalize horizontal whitespace.
| // Normalize whitespace for matching. Newlines are treated as sentence | |
| // boundaries (converted to periods) to prevent cross-line false positives | |
| // (Round 2 Review #7). | |
| const normalized = userMessage | |
| .replace(/\r\n|\r/g, '\n') | |
| .replace(/\n/g, '. ') | |
| .replace(/\s+/g, ' ') | |
| .trim(); | |
| // Normalize horizontal whitespace while preserving newlines. | |
| // The regex patterns use [^.\n] to prevent crossing sentence and newline | |
| // boundaries, while \s+ naturally matches newlines for intra-sentence wrapping. | |
| const normalized = userMessage | |
| .replace(/\r\n|\r/g, '\n') | |
| .replace(/[ \t]+/g, ' ') | |
| .replace(/ \n/g, '\n') | |
| .replace(/\n /g, '\n') | |
| .trim(); |
f464440 to
fb4a3cb
Compare
|
Round 3 review findings addressed: Review #12 (high) - "I will ... later/after" false positive: Restricted pattern to only match Review #13 (high) - Newline-to-period normalization breaks multi-line directives: Switched to preserving newlines and only normalizing horizontal whitespace ( All 84 tests pass. CLI run verified. /gemini review |
There was a problem hiding this comment.
Code Review
This pull request implements a programmatic hold directive mechanism to block mutating tool calls (such as file modifications and shell commands) when a user requests the agent to wait or explain first, while keeping read-only tools available. The implementation includes a new userDirectiveService for detection and a holdDirectiveGuard integrated into the scheduler. The review feedback suggests improving regex patterns to use [^.!?\n] instead of [^.\n] to prevent matching across sentence boundaries, addressing a potential false negative issue with global negation patterns in compound sentences, and using imported tool name constants in prompt snippets instead of hardcoding them.
Note: Security Review did not run due to the size of the PR.
| // [^.\n]*? prevents crossing sentence AND newline boundaries. | ||
| { | ||
| pattern: | ||
| /\b(?:don'?t|do\s+not)\s+(?:apply|make|execute|run|implement|modify|change|write|edit|replace|update|fix|rewrite)\b[^.\n]*?\b(?:yet|now|until|before|first|for now)\b/i, |
There was a problem hiding this comment.
Using [^.\n] only excludes literal periods and newlines, meaning other sentence/clause boundaries like ! and ? are not excluded. This allows the regex to match across sentence boundaries, leading to false positives where a hold directive in one sentence is incorrectly combined with an action directive in the next sentence (e.g., 'do not apply! Now, let's write the file.'). Excluding ! and ? by using [^.!?\n] instead of [^.\n] prevents these false positives.
| /\b(?:don'?t|do\s+not)\s+(?:apply|make|execute|run|implement|modify|change|write|edit|replace|update|fix|rewrite)\b[^.\n]*?\b(?:yet|now|until|before|first|for now)\b/i, | |
| /\b(?:don'?t|do\s+not)\s+(?:apply|make|execute|run|implement|modify|change|write|edit|replace|update|fix|rewrite)\b[^.!?\n]*?\b(?:yet|now|until|before|first|for now)\b/i, |
| }, | ||
| { | ||
| pattern: | ||
| /\bbut\s+don'?t\s+(?:apply|make|change|fix|modify)\b[^.\n]*?\b(?:yet|now|first|for now)\b/i, |
There was a problem hiding this comment.
Similar to the previous pattern, using [^.!?\n] instead of [^.\n] ensures that sentence boundaries like ! and ? are respected, preventing false positives on compound sentences.
| /\bbut\s+don'?t\s+(?:apply|make|change|fix|modify)\b[^.\n]*?\b(?:yet|now|first|for now)\b/i, | |
| /\bbut\s+don'?t\s+(?:apply|make|change|fix|modify)\b[^.!?\n]*?\b(?:yet|now|first|for now)\b/i, |
| // Round 2 Review #9: replaced greedy .* with [^.\n]* to stay within one clause. | ||
| { | ||
| pattern: | ||
| /\b(?:show|present|give)\s+(?:me\s+)?(?:the\s+)?(?:full\s+)?(?:review|report|analysis|findings|results)\b[^.\n]*\b(?:before)\b/i, |
There was a problem hiding this comment.
Using [^.!?\n] instead of [^.\n] prevents matching across sentence boundaries marked by ! or ?.
| /\b(?:show|present|give)\s+(?:me\s+)?(?:the\s+)?(?:full\s+)?(?:review|report|analysis|findings|results)\b[^.\n]*\b(?:before)\b/i, | |
| /\b(?:show|present|give)\s+(?:me\s+)?(?:the\s+)?(?:full\s+)?(?:review|report|analysis|findings|results)\b[^.!?\n]*\b(?:before)\b/i, |
| // caused false positives on "I will review later, for now implement". | ||
| { | ||
| pattern: | ||
| /\bi\s+will\s+(?:run|do|apply|check|review|test)\b[^.\n]*\b(?:first|myself)\b/i, |
There was a problem hiding this comment.
| for (const negPattern of NEGATION_PATTERNS) { | ||
| if (negPattern.test(normalized)) { | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Checking negation patterns globally across the entire user message causes false negatives (failing to block mutating tools) when a user combines a permissive phrase with a negative constraint in a compound sentence (e.g., 'go ahead with the research, but do not modify files yet' or 'proceed with the investigation, but hold on before fixing'). Because 'go ahead' or 'proceed' matches globally, the entire hold directive is bypassed. Consider evaluating negation patterns on a per-clause basis or ensuring that explicit hold patterns take precedence over global negations.
| - **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it. | ||
| - **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix. | ||
| - **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations, e.g., "Can you tell me how to"). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, or whenever the user explicitly instructs you NOT to make changes just yet (e.g., "Don't make changes just yet", "Without changing anything"), your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a subsequent Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. ${options.interactive ? 'For Directives, only clarify if critically underspecified; otherwise, work autonomously.' : 'For Directives, you must work autonomously as no further user input is available.'} You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction. | ||
| - **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations, e.g., "Can you tell me how to"). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, or whenever the user explicitly instructs you NOT to make changes just yet (e.g., "Don't make changes just yet", "Without changing anything", "wait", "hold", "explain first", "but not now"), your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a subsequent Directive is issued. **IMPORTANT:** This constraint is programmatically enforced. If you attempt to call mutating tools (write_file, edit, shell) while a hold directive is active, they will be blocked at the system level and return a [HOLD DIRECTIVE ACTIVE] error. Only read-only tools (read_file, grep, glob, ls, web_search) remain available. Present your findings to the user and explicitly wait for authorization (e.g., "apply the fix", "go ahead", "proceed") before calling any mutating tools. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. ${options.interactive ? 'For Directives, only clarify if critically underspecified; otherwise, work autonomously.' : 'For Directives, you must work autonomously as no further user input is available.'} You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction. |
There was a problem hiding this comment.
Avoid hardcoding tool names like write_file, edit, shell, read_file, grep, and glob in the prompt text. Since these tool name constants are already imported at the top of the file, using the imported constants (e.g., ${WRITE_FILE_TOOL_NAME}, ${EDIT_TOOL_NAME}, ${SHELL_TOOL_NAME}) ensures consistency and prevents potential mismatches if tool names are renamed in the future.
| - **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations, e.g., "Can you tell me how to"). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, or whenever the user explicitly instructs you NOT to make changes just yet (e.g., "Don't make changes just yet", "Without changing anything", "wait", "hold", "explain first", "but not now"), your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a subsequent Directive is issued. **IMPORTANT:** This constraint is programmatically enforced. If you attempt to call mutating tools (write_file, edit, shell) while a hold directive is active, they will be blocked at the system level and return a [HOLD DIRECTIVE ACTIVE] error. Only read-only tools (read_file, grep, glob, ls, web_search) remain available. Present your findings to the user and explicitly wait for authorization (e.g., "apply the fix", "go ahead", "proceed") before calling any mutating tools. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. ${options.interactive ? 'For Directives, only clarify if critically underspecified; otherwise, work autonomously.' : 'For Directives, you must work autonomously as no further user input is available.'} You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction. | |
| - **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations, e.g., "Can you tell me how to"). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, or whenever the user explicitly instructs you NOT to make changes just yet (e.g., "Don't make changes just yet", "Without changing anything", "wait", "hold", "explain first", "but not now"), your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a subsequent Directive is issued. **IMPORTANT:** This constraint is programmatically enforced. If you attempt to call mutating tools (${WRITE_FILE_TOOL_NAME}, ${EDIT_TOOL_NAME}, ${SHELL_TOOL_NAME}) while a hold directive is active, they will be blocked at the system level and return a [HOLD DIRECTIVE ACTIVE] error. Only read-only tools (${READ_FILE_TOOL_NAME}, ${GREP_TOOL_NAME}, ${GLOB_TOOL_NAME}, ls, web_search) remain available. Present your findings to the user and explicitly wait for authorization (e.g., "apply the fix", "go ahead", "proceed") before calling any mutating tools. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. ${options.interactive ? 'For Directives, only clarify if critically underspecified; otherwise, work autonomously.' : 'For Directives, you must work autonomously as no further user input is available.'} You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction. |
…ls at scheduler layer Resolves google-gemini#26390 ## Summary The Gemini CLI agent exhibits an aggressive action-bias that overrides explicit user hold directives. When users say 'wait', 'explain first', or 'don't apply fixes yet', the agent still fires destructive tool calls (replace, write_file, run_shell_command). Prompt-level instructions alone are insufficient — the model's RLHF-driven completion bias overwhelms the attention weights on negative constraints. This fix adds programmatic enforcement at the scheduler layer: ## Details 1. **User Directive Detection Service** (userDirectiveService.ts): Detects hold/wait/explain directives from user messages using 14 regex patterns across 4 directive types (EXPLICIT_WAIT, EXPLAIN_ONLY, NO_CHANGES_YET, REVIEW_FIRST). Includes negation guards to avoid false positives on 'don't wait' / 'go ahead' / 'proceed'. 2. **Hold Directive Guard** (holdDirectiveGuard.ts): Checks tool calls against the active hold directive at the scheduler layer, before they reach the policy/confirmation/execution pipeline. Only mutating tools (write_file, replace, run_shell_command, write_todos, invoke_agent) are blocked. Read-only tools (read_file, grep, glob, ls, web_search) remain available for continued investigation. 3. **Scheduler Integration** (scheduler.ts): The hold directive guard runs in _startBatch() before tool validation. Blocked calls receive an immediate error with a structured [HOLD DIRECTIVE ACTIVE] message that instructs the model to present findings and wait for explicit authorization. 4. **Strengthened System Prompt** (snippets.ts): The Expertise and Intent Alignment section now documents the programmatic enforcement, telling the model that mutating tools will be system-blocked during hold directives. 5. **New Error Type** (tool-error.ts): Added HOLD_DIRECTIVE_VIOLATION for precise error categorization in telemetry and logging. 6. **AgentLoopContext Extension** (agent-loop-context.ts): Added optional activeHoldDirective field for context-wide directive state. ## Test Coverage - 48 unit tests for directive detection (including all 3 scenarios from issue google-gemini#26390, negation patterns, edge cases) - 20 unit tests for the hold directive guard (mutating/read-only/batch) - 9 end-to-end integration tests simulating the exact issue scenarios ## How to Validate npm test -w @google/gemini-cli-core -- src/services/userDirectiveService.test.ts --run npm test -w @google/gemini-cli-core -- src/scheduler/holdDirectiveGuard.test.ts --run npm test -w @google/gemini-cli-core -- src/services/holdDirectiveIntegration.test.ts --run ## Related Issues Resolves google-gemini#26390
fb4a3cb to
c9b9bf5
Compare
|
Round 4 review findings addressed: Reviews #14-17 (high) - Review #18 (high) - Global negation bypasses compound sentences: Rewrote detection to split input into clauses (by Review #19 (high) - Hardcoded tool names in prompt: Replaced All 86 tests pass. CLI run: |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a programmatic 'Hold Directive Guard' mechanism to enforce user constraints (such as 'don't apply yet' or 'explain first') at the scheduler layer. It detects hold directives from user input using regex patterns, blocks mutating tool calls (like write_file, replace, and run_shell_command) while allowing read-only tools (like read_file and grep), and returns descriptive error messages to guide the model. It also updates prompt guidelines and adds comprehensive unit and integration tests. There are no review comments provided, so I have no feedback to offer on the review itself.
gemini-code-assist Review: PASSEDRound 5 review returned 0 inline comments, all previous findings resolved.
Review History
Summary
|
|
Hi there! Thank you for your interest in contributing to Gemini CLI. To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'. This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding. |
|
This pull request is being closed as it has been open for 14 days without a 'help wanted' designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding. |
Summary
Resolves #26390
The agent exhibits an aggressive action-bias that overrides explicit user hold directives. When users say "wait", "explain first", or "do not apply fixes yet", the agent still fires destructive tool calls (
replace,write_file,run_shell_command). Prompt-level instructions alone are insufficient, the model RLHF-driven completion bias overwhelms the attention weights on negative constraints.This fix adds programmatic enforcement at the scheduler layer, blocking mutating tool calls before they reach the policy/confirmation/execution pipeline.
Details
New: User Directive Detection Service (
userDirectiveService.ts)EXPLICIT_WAIT,EXPLAIN_ONLY,NO_CHANGES_YET,REVIEW_FIRSTNew: Hold Directive Guard (
holdDirectiveGuard.ts)write_file,replace,run_shell_command,write_todos,invoke_agent) are blockedread_file,grep,glob,ls,web_search) remain available for continued investigationModified: Scheduler Integration (
scheduler.ts)_startBatch()before tool validation[HOLD DIRECTIVE ACTIVE]messagesetActiveHoldDirective()for runtime directive updatesModified: CLI Integration (
nonInteractiveCli.ts,nonInteractiveCliAgentSession.ts)Modified: System Prompt (
snippets.ts)New Error Type (
tool-error.ts)HOLD_DIRECTIVE_VIOLATIONfor precise error categorizationContext Extension (
agent-loop-context.ts,agent-scheduler.ts)activeHoldDirectivefield propagated through the execution contextTest Coverage
How to Validate
Manual validation with mock API:
Expected: model
replacecall returns[HOLD DIRECTIVE ACTIVE]error, file unchanged.Pre-Merge Checklist
tsc --noEmitpasses)Related Issues
Resolves #26390