mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
fix(harness): anchor extractPlanFilePath path captures on /Users|~|/home|/var|/tmp
Adversarial-tightened gate sweep surfaced a real bug in the path
extraction: stripAnsi collapses whitespace via cursor-positioning escape
removal, so "yet at /Users/..." in the visible buffer becomes
"yetat/Users/..." with no space between. The previous fallback pattern
`(~?\/?\S*\.claude\/plans\/[\w-]+\.md)` greedily matched non-whitespace
characters BEFORE the path, producing `yetat/Users/garrytan/.claude/...`
which then fails fs.readFileSync.
Fix: every regex now requires the path to START at a known path-anchor:
`~/`, `/Users/`, `/home/`, `/var/`, `/tmp/`, or `./`. Earlier
non-whitespace runs can't be glommed in.
Verified against the failing fixture (`yetat/Users/...`) plus the four
canonical render forms ("Plan saved to:", "Plan file:", `·`-decorated
ctrl-g hint, and the bare fallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -183,21 +183,24 @@ export function isAutoDecidedVisible(visible: string): boolean {
|
||||
*/
|
||||
export function extractPlanFilePath(visible: string): string | null {
|
||||
// Patterns checked in order of specificity. Each captures the .md path.
|
||||
// The visible buffer may have stripAnsi-collapsed whitespace, so we
|
||||
// accept space-or-not separators in the prompts and accept paths
|
||||
// without intervening whitespace (e.g. `editinVSCode·~/.claude/...`).
|
||||
// The visible buffer may have stripAnsi-collapsed whitespace ("yet at" can
|
||||
// become "yetat"), so the captured path MUST start at a clear path-anchor
|
||||
// character: `~/`, `/Users/`, `/home/`, `/var/`, or `/tmp/`. Anchoring on
|
||||
// these prefixes prevents earlier non-whitespace characters from being
|
||||
// glommed into the path (real bug seen in the wild: `yetat/Users/...`).
|
||||
const PATH_ANCHOR = '(~\\/|\\/Users\\/|\\/home\\/|\\/var\\/|\\/tmp\\/|\\.\\/)';
|
||||
const patterns: RegExp[] = [
|
||||
/Plan\s*saved\s*to\s*:?\s*(\S+\.md)/i,
|
||||
/Plan\s*file\s*:?\s*(\S+\.md)/i,
|
||||
/·\s*(\S+\.claude\/plans\/\S+\.md)/i,
|
||||
// Fallback: any reference to a .claude/plans path in the buffer.
|
||||
/(~?\/?\S*\.claude\/plans\/[\w-]+\.md)/i,
|
||||
new RegExp(`Plan\\s*saved\\s*to\\s*:?\\s*(${PATH_ANCHOR}\\S+\\.md)`, 'i'),
|
||||
new RegExp(`Plan\\s*file\\s*:?\\s*(${PATH_ANCHOR}\\S+\\.md)`, 'i'),
|
||||
new RegExp(`·\\s*(${PATH_ANCHOR}\\S*\\.claude\\/plans\\/\\S+\\.md)`, 'i'),
|
||||
// Fallback: any path-anchored reference to a .claude/plans .md file.
|
||||
new RegExp(`(${PATH_ANCHOR}\\S*\\.claude\\/plans\\/[\\w-]+\\.md)`, 'i'),
|
||||
];
|
||||
for (const p of patterns) {
|
||||
const m = visible.match(p);
|
||||
if (m && m[1]) {
|
||||
let raw = m[1];
|
||||
// Some patterns capture trailing punctuation; strip a trailing dot.
|
||||
// Strip trailing punctuation that some patterns may capture.
|
||||
raw = raw.replace(/\.+$/, '.md').replace(/\.md\.+$/, '.md');
|
||||
// Tilde expansion to absolute path.
|
||||
if (raw.startsWith('~')) {
|
||||
|
||||
Reference in New Issue
Block a user