diff --git a/src/ralph-fix-plan-watcher.ts b/src/ralph-fix-plan-watcher.ts index 305f3b39..5347c107 100644 --- a/src/ralph-fix-plan-watcher.ts +++ b/src/ralph-fix-plan-watcher.ts @@ -203,6 +203,9 @@ export class RalphFixPlanWatcher extends EventEmitter { /** Path to the @fix_plan.md file being watched */ private _fixPlanPath: string | null = null; + /** The path is on a REMOTE host (`setRemoteWorkingDir`): never read or watch it locally. */ + private _remote = false; + /** File watcher for @fix_plan.md */ private _fixPlanWatcher: FSWatcher | null = null; @@ -238,6 +241,7 @@ export class RalphFixPlanWatcher extends EventEmitter { * @param workingDir - The session's working directory */ setWorkingDir(workingDir: string): void { + this._remote = false; this._workingDir = workingDir; this._fixPlanPath = join(workingDir, '@fix_plan.md'); @@ -258,16 +262,18 @@ export class RalphFixPlanWatcher extends EventEmitter { */ setRemoteWorkingDir(workingDir: string): void { this.stopWatchingFixPlan(); + this._remote = true; this._workingDir = workingDir; this._fixPlanPath = join(workingDir, '@fix_plan.md'); } /** * Load @fix_plan.md from disk if it exists. - * Called on initialization and when file changes are detected. + * Called on initialization and when file changes are detected. A remote path is never read here: + * the local disk would hold a missing path or an unrelated same-named file. */ async loadFixPlanFromDisk(): Promise { - if (!this._fixPlanPath) return 0; + if (!this._fixPlanPath || this._remote) return 0; try { if (!existsSync(this._fixPlanPath)) { @@ -295,7 +301,7 @@ export class RalphFixPlanWatcher extends EventEmitter { * Reloads todos when the file is modified. */ private startWatchingFixPlan(): void { - if (!this._fixPlanPath || !this._workingDir) return; + if (!this._fixPlanPath || !this._workingDir || this._remote) return; // Stop existing watcher if any this.stopWatchingFixPlan(); diff --git a/test/ralph-fix-plan-remote.test.ts b/test/ralph-fix-plan-remote.test.ts index c499b65e..661b7e5e 100644 --- a/test/ralph-fix-plan-remote.test.ts +++ b/test/ralph-fix-plan-remote.test.ts @@ -18,7 +18,7 @@ * Drives the real `WebServer.setupSessionListeners()`, like * discard-partially-built-session.test.ts. */ -import { mkdirSync } from 'node:fs'; +import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; @@ -100,6 +100,38 @@ describe('Ralph @fix_plan.md watcher and the session location', () => { expect(session.ralphTracker.isFileAuthoritative).toBe(true); }); + it('never reads a same-named local file for a remote session', async () => { + // The remote path exists locally too (an sshfs mount, a leftover checkout) and holds a plan. + writeFileSync(join(WORKSPACE, '@fix_plan.md'), '- [ ] a local task the remote session never had\n'); + try { + const session = new Session({ + id: SESSION_ID, + workingDir: WORKSPACE, + mode: 'claude', + name: 'remote session over a local twin', + mux, + useMux: true, + remote: { + hostId: 'proxx', + label: 'proxx', + host: 'proxx.example.lan', + username: 'user', + remotePath: WORKSPACE, + owned: true, + }, + }); + vi.spyOn(console, 'log').mockImplementation(() => {}); + + await internals.registerSessionWithLayout(session); + await internals.setupSessionListeners(session); + + expect(await session.ralphTracker.loadFixPlanFromDisk()).toBe(0); + expect(session.ralphTracker.todos).toEqual([]); + } finally { + rmSync(join(WORKSPACE, '@fix_plan.md'), { force: true }); + } + }); + it('keeps a remote session todo list out of the prose completion guessers', async () => { const session = buildSession(true); vi.spyOn(console, 'log').mockImplementation(() => {});