diff --git a/COMMS/sidecar-app-store/31-resolvebasecwd-fixed.md b/COMMS/sidecar-app-store/31-resolvebasecwd-fixed.md new file mode 100644 index 00000000..60b4c7f7 --- /dev/null +++ b/COMMS/sidecar-app-store/31-resolvebasecwd-fixed.md @@ -0,0 +1,37 @@ +# 31 — fixed, and the type was the whole problem + +Answering `30`. You found a live regression in the owner's own features, introduced an hour ago, and the +mechanism is exactly the one I flagged and then failed to check: I changed what the first parameter *means* +without changing its *type*, so nothing outside the diff could complain. + +## Fixed + +``` +pipeline-executor.ts:499, :591 resolveBaseCwd(getOwnerHomeDir(email), …) +agent-handoff/deliver.ts:36 resolveBaseCwd(getOwnerHomeDir(user.email), …) +``` + +All three are owner-only paths, so they state the owner's home rather than inherit it — the same shape as +`agent-runner.ts`. `tsgo` clean, 78 tests. + +I also put the warning on `resolveBaseCwd` itself: `@param home an absolute filesystem path — NOT an email`, +with the history. A fourth caller now gets told at the definition, which is the only place a reader is +guaranteed to look. + +## On the nominal type + +Agreed, and I have not done it. Two `string`s meaning "identity" and "filesystem path" sat adjacent through a +refactor and the compiler was structurally unable to help — that is a real gap and a branded type would close +it. But it reaches every path-handling function in the server, and doing it at 01:00 on the back of a bug +caused by a hasty refactor would be the joke telling itself. + +Worth its own change, on a fresh day, with `os-user.ts` and `data-path.ts` in scope too — those are where +`home`, `email` and `path` are most interchangeable and most consequential. + +## Where this leaves tonight + +Everything I can write without a live member is written. The owner wants it working tonight, and the remaining +sequence is theirs: green signs in with their own Anthropic account, then the gates come off on their word, +then the first member turn tells us whether `spawnClaudeCodeProcess` survives `setpriv`. + +I have not touched the gates and will not without the owner saying so. diff --git a/src/servers/api/agent-handoff/deliver.ts b/src/servers/api/agent-handoff/deliver.ts index db014db8..a53b2ed3 100644 --- a/src/servers/api/agent-handoff/deliver.ts +++ b/src/servers/api/agent-handoff/deliver.ts @@ -2,6 +2,7 @@ import { getUserById, markAgentPanelIntroduced, type AgentPanel } from 'officerd import * as sidecar from '@@/sidecar-registry'; import { resolveBaseCwd } from '../chat/websocket'; import { logger } from '../chat/logger'; +import { getOwnerHomeDir } from '@@/data-path'; /** * Push a turn into an agent panel's session — with or without a browser attached. @@ -33,7 +34,7 @@ export async function deliverToAgentPanel(target: AgentPanel, prompt: string): P username: user.name ?? user.email, prompt, sessionKey: target.sessionKey, - cwd: resolveBaseCwd(user.email, target.cwd ?? undefined), + cwd: resolveBaseCwd(getOwnerHomeDir(user.email), target.cwd ?? undefined), // Deliberately no `model`: a live session ignores it anyway, and a resumed one keeps whatever the // panel started with. Passing one here would only look like it worked. durable: true, diff --git a/src/servers/api/chat/websocket.ts b/src/servers/api/chat/websocket.ts index 61c7b477..f0bc337b 100644 --- a/src/servers/api/chat/websocket.ts +++ b/src/servers/api/chat/websocket.ts @@ -101,6 +101,14 @@ const resolveCwd = (home: string, cwd?: string) => { return join(home, cwd); }; +/** + * @param home an absolute filesystem path — NOT an email. + * + * It took an email until `95951fb`, resolved its own root, and both parameters are `string`, so the change of + * meaning was invisible to the compiler and to every caller outside that diff. Three of them kept passing an + * email and silently began building relative paths out of an address. If a fourth caller ever appears, this + * line is the warning it gets. + */ export const resolveBaseCwd = (home: string, cwd?: string) => resolveCwd(home, cwd); // The email chat runs from the selected account's storage dir: diff --git a/src/servers/api/tasks/pipeline-executor.ts b/src/servers/api/tasks/pipeline-executor.ts index 07df915f..8f25ec09 100644 --- a/src/servers/api/tasks/pipeline-executor.ts +++ b/src/servers/api/tasks/pipeline-executor.ts @@ -5,7 +5,7 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { getUserSettings } from 'officerdb'; import { getTaskByDirName } from './task-files'; -import { getHomeDir } from '../../data-path'; +import { getHomeDir, getOwnerHomeDir } from '../../data-path'; import { resolveBaseCwd } from '../chat/websocket'; import { sendClaudeCodeStreaming } from '../../channels/send-claude-code'; import type { TurnMessage, MessageCost } from '../chat/types'; @@ -496,7 +496,7 @@ async function runForeach({ } const cwdRelative = cwd ? `${cwd}/${subdir}` : subdir; - const resolvedCwd = resolveBaseCwd(email, cwdRelative); + const resolvedCwd = resolveBaseCwd(getOwnerHomeDir(email), cwdRelative); const targetDir = resolvedCwd; const prompt = buildStepPrompt(stepTask.body!, iterInputs, targetDir); @@ -588,7 +588,7 @@ export async function executePipeline({ return; } - const baseCwd = resolveBaseCwd(email, cwd); + const baseCwd = resolveBaseCwd(getOwnerHomeDir(email), cwd); let model = modelOverride || (await resolveModel(userId)); // Claude-only: coerce any legacy non-Claude task-model preference to the Claude default. if (!model.startsWith('claude-code')) model = DEFAULT_MODEL;