From bdc4818bf5942bfea2c9c3992d66c0fdf03152d2 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 1 May 2026 07:36:59 -0700 Subject: [PATCH] fix(harness): anchor extractPlanFilePath path captures on /Users|~|/home|/var|/tmp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- test/helpers/claude-pty-runner.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/test/helpers/claude-pty-runner.ts b/test/helpers/claude-pty-runner.ts index d3602b12..58a17b51 100644 --- a/test/helpers/claude-pty-runner.ts +++ b/test/helpers/claude-pty-runner.ts @@ -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('~')) {