fix(bughunter): simplify shell commands to single git commands, narrow catch to surface interruptions

This commit is contained in:
Gravirei
2026-06-16 02:46:38 +06:00
parent c6dcf00668
commit 73d0bcb440
5 changed files with 57 additions and 52 deletions
+5 -7
View File
@@ -140,13 +140,11 @@ describe('builtInCommandNames', () => {
'git rev-parse --git-dir',
'git rev-parse --git-dir 2>&1',
'git diff HEAD -- . 2> /dev/null',
'head -400',
'head -50',
'echo "(no diff available or not a git repo)"',
'echo "(no git history or not a git repo)"',
'echo "(no unstaged changes or not a git repo)"',
'echo "(no staged changes or not a git repo)"',
'git diff --name-only HEAD~10..HEAD --diff-filter=AM 2>/dev/null || git ls-files 2>/dev/null | head -50',
'git status 2>/dev/null',
'git diff --name-only --diff-filter=AM 2>/dev/null',
'git diff --cached --name-only --diff-filter=AM 2>/dev/null',
'git diff --name-only HEAD~10..HEAD --diff-filter=AM 2>/dev/null',
'git diff HEAD -- . 2>/dev/null',
]
const createMockToolContext = (cwd: string, commands: string[]) =>
+17 -14
View File
@@ -2,6 +2,7 @@ import { parseFrontmatter } from '../../utils/frontmatterParser.js'
import { createGetAppStateWithAllowedTools } from '../../utils/forkedAgent.js'
import { parseSlashCommandToolsFromFrontmatter } from '../../utils/markdownConfigLoader.js'
import { executeShellCommandsInPrompt } from '../../utils/promptShellExecution.js'
import { ShellError } from '../../utils/errors.js'
import { createMovedToPluginCommand } from '../createMovedToPluginCommand.js'
const BUGHUNTER_PERF_PROMPT = `---
@@ -18,33 +19,37 @@ SCOPE: {{ARGS}}
GIT CONTEXT (auto-collected, may be empty if not a git repo):
\`\`\`
!\`git status 2>/dev/null || echo "(If empty: not a git repository or git unavailable)"\`
!\`git status 2>/dev/null\`
\`\`\`
(If empty: not a git repository or git unavailable)
UNSTAGED CHANGES (working tree):
\`\`\`
!\`git diff --name-only --diff-filter=AM 2>/dev/null || echo "(If empty: no unstaged changes or not a git repo)"\`
!\`git diff --name-only --diff-filter=AM 2>/dev/null\`
\`\`\`
(If empty: no unstaged changes or not a git repo)
STAGED CHANGES (index):
\`\`\`
!\`git diff --cached --name-only --diff-filter=AM 2>/dev/null || echo "(If empty: no staged changes or not a git repo)"\`
!\`git diff --cached --name-only --diff-filter=AM 2>/dev/null\`
\`\`\`
(If empty: no staged changes or not a git repo)
RECENTLY COMMITTED FILES (last 10 commits):
\`\`\`
!\`git diff --name-only HEAD~10..HEAD --diff-filter=AM 2>/dev/null || git ls-files 2>/dev/null | head -50\`
!\`git diff --name-only HEAD~10..HEAD --diff-filter=AM 2>/dev/null\`
\`\`\`
(If empty: no git history or not a git repo)
DIFF OF UNSTAGED + STAGED CHANGES (first 400 lines):
\`\`\`
!\`git diff HEAD -- . 2>/dev/null | head -400 || echo "(If empty: no diff available or not a git repo)"\`
!\`git diff HEAD -- . 2>/dev/null\`
\`\`\`
(If empty: no diff available or not a git repo)
---
@@ -235,16 +240,14 @@ const bughunterPerf = createMovedToPluginCommand({
},
'bughunter-perf',
)
} catch {
} catch (e) {
// Surface interruptions — don't convert a cancellation into a normal prompt.
if (e instanceof ShellError && e.interrupted) {
throw e
}
// Shell unavailable (e.g. Windows without Git Bash).
// Extract fallback text from || echo "..." in each command.
processedContent = parsed.content.replace(
/!`([^`]+)`/g,
(_, cmd: string) => {
const m = cmd.match(/\|\|\s*echo\s+"([^"]*)"/)
return m ? m[1] : ''
},
)
// Static fallback text outside the code blocks will show in the prompt.
processedContent = parsed.content.replace(/!`[^`]+`/g, '')
}
const finalContent = processedContent.replace('{{ARGS}}', () => scope)
+17 -14
View File
@@ -2,6 +2,7 @@ import { parseFrontmatter } from '../../utils/frontmatterParser.js'
import { createGetAppStateWithAllowedTools } from '../../utils/forkedAgent.js'
import { parseSlashCommandToolsFromFrontmatter } from '../../utils/markdownConfigLoader.js'
import { executeShellCommandsInPrompt } from '../../utils/promptShellExecution.js'
import { ShellError } from '../../utils/errors.js'
import { createMovedToPluginCommand } from '../createMovedToPluginCommand.js'
const BUGHUNTER_SECURITY_PROMPT = `---
@@ -18,33 +19,37 @@ SCOPE: {{ARGS}}
GIT CONTEXT (auto-collected, may be empty if not a git repo):
\`\`\`
!\`git status 2>/dev/null || echo "(If empty: not a git repository or git unavailable)"\`
!\`git status 2>/dev/null\`
\`\`\`
(If empty: not a git repository or git unavailable)
UNSTAGED CHANGES (working tree):
\`\`\`
!\`git diff --name-only --diff-filter=AM 2>/dev/null || echo "(If empty: no unstaged changes or not a git repo)"\`
!\`git diff --name-only --diff-filter=AM 2>/dev/null\`
\`\`\`
(If empty: no unstaged changes or not a git repo)
STAGED CHANGES (index):
\`\`\`
!\`git diff --cached --name-only --diff-filter=AM 2>/dev/null || echo "(If empty: no staged changes or not a git repo)"\`
!\`git diff --cached --name-only --diff-filter=AM 2>/dev/null\`
\`\`\`
(If empty: no staged changes or not a git repo)
RECENTLY COMMITTED FILES (last 10 commits):
\`\`\`
!\`git diff --name-only HEAD~10..HEAD --diff-filter=AM 2>/dev/null || git ls-files 2>/dev/null | head -50\`
!\`git diff --name-only HEAD~10..HEAD --diff-filter=AM 2>/dev/null\`
\`\`\`
(If empty: no git history or not a git repo)
DIFF OF UNSTAGED + STAGED CHANGES (first 400 lines):
\`\`\`
!\`git diff HEAD -- . 2>/dev/null | head -400 || echo "(If empty: no diff available or not a git repo)"\`
!\`git diff HEAD -- . 2>/dev/null\`
\`\`\`
(If empty: no diff available or not a git repo)
---
@@ -255,16 +260,14 @@ const bughunterSecurity = createMovedToPluginCommand({
},
'bughunter-security',
)
} catch {
} catch (e) {
// Surface interruptions — don't convert a cancellation into a normal prompt.
if (e instanceof ShellError && e.interrupted) {
throw e
}
// Shell unavailable (e.g. Windows without Git Bash).
// Extract fallback text from || echo "..." in each command.
processedContent = parsed.content.replace(
/!`([^`]+)`/g,
(_, cmd: string) => {
const m = cmd.match(/\|\|\s*echo\s+"([^"]*)"/)
return m ? m[1] : ''
},
)
// Static fallback text outside the code blocks will show in the prompt.
processedContent = parsed.content.replace(/!`[^`]+`/g, '')
}
const finalContent = processedContent.replace('{{ARGS}}', () => scope)
+17 -14
View File
@@ -2,6 +2,7 @@ import { parseFrontmatter } from '../../utils/frontmatterParser.js'
import { createGetAppStateWithAllowedTools } from '../../utils/forkedAgent.js'
import { parseSlashCommandToolsFromFrontmatter } from '../../utils/markdownConfigLoader.js'
import { executeShellCommandsInPrompt } from '../../utils/promptShellExecution.js'
import { ShellError } from '../../utils/errors.js'
import { createMovedToPluginCommand } from '../createMovedToPluginCommand.js'
const BUGHUNTER_PROMPT = `---
@@ -16,33 +17,37 @@ SCOPE: {{ARGS}}
GIT CONTEXT (auto-collected, may be empty if not a git repo):
\`\`\`
!\`git status 2>/dev/null || echo "(If empty: not a git repository or git unavailable)"\`
!\`git status 2>/dev/null\`
\`\`\`
(If empty: not a git repository or git unavailable)
UNSTAGED CHANGES (working tree):
\`\`\`
!\`git diff --name-only --diff-filter=AM 2>/dev/null || echo "(If empty: no unstaged changes or not a git repo)"\`
!\`git diff --name-only --diff-filter=AM 2>/dev/null\`
\`\`\`
(If empty: no unstaged changes or not a git repo)
STAGED CHANGES (index):
\`\`\`
!\`git diff --cached --name-only --diff-filter=AM 2>/dev/null || echo "(If empty: no staged changes or not a git repo)"\`
!\`git diff --cached --name-only --diff-filter=AM 2>/dev/null\`
\`\`\`
(If empty: no staged changes or not a git repo)
RECENTLY COMMITTED FILES (last 10 commits):
\`\`\`
!\`git diff --name-only HEAD~10..HEAD --diff-filter=AM 2>/dev/null || git ls-files 2>/dev/null | head -50\`
!\`git diff --name-only HEAD~10..HEAD --diff-filter=AM 2>/dev/null\`
\`\`\`
(If empty: no git history or not a git repo)
DIFF OF UNSTAGED + STAGED CHANGES (first 400 lines):
\`\`\`
!\`git diff HEAD -- . 2>/dev/null | head -400 || echo "(If empty: no diff available or not a git repo)"\`
!\`git diff HEAD -- . 2>/dev/null\`
\`\`\`
(If empty: no diff available or not a git repo)
---
@@ -206,16 +211,14 @@ const bughunter = createMovedToPluginCommand({
},
'bughunter',
)
} catch {
} catch (e) {
// Surface interruptions — don't convert a cancellation into a normal prompt.
if (e instanceof ShellError && e.interrupted) {
throw e
}
// Shell unavailable (e.g. Windows without Git Bash).
// Extract fallback text from || echo "..." in each command.
processedContent = parsed.content.replace(
/!`([^`]+)`/g,
(_, cmd: string) => {
const m = cmd.match(/\|\|\s*echo\s+"([^"]*)"/)
return m ? m[1] : ''
},
)
// Static fallback text outside the code blocks will show in the prompt.
processedContent = parsed.content.replace(/!`[^`]+`/g, '')
}
const finalContent = processedContent.replace('{{ARGS}}', () => scope)
+1 -3
View File
@@ -181,9 +181,7 @@ function formatBashOutput(
function formatBashError(e: unknown, pattern: string, inline = false): never {
if (e instanceof ShellError) {
if (e.interrupted) {
throw new MalformedCommandError(
`Shell command interrupted for pattern "${pattern}": [Command interrupted]`,
)
throw e
}
const output = formatBashOutput(e.stdout, e.stderr, inline)
throw new MalformedCommandError(