#3616 · Automation execution directory
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
The stored-script runner overrides the caller’s working directory with the plugin’s scripts root. A script referring to a file in a project directory therefore fails unless it explicitly selects that directory or uses an absolute path. The runner captures the missing-file diagnostic, but the outcome mapper puts only the exit code in the error summary. Two runs against unchanged trusted main reproduce this mechanism; neither uses the reporter’s code or branch.
2. Claims vs findings
| Claim | Finding | Evidence |
|---|---|---|
| Project-relative arguments fail because of cwd | Verified at the production runner boundary | Both probes print the plugin scripts directory and fail to read a project fixture. |
| Absolute path works | Verified | Control run reads the same fixture and exits zero. |
| Summary hides captured cause | Verified | Missing-file text is in output; mapped error contains only code 1. |
| Reported Python exit code and desktop flow | Unverified directly | This reproduction uses bash and cat, with exit code 1, and calls the runner without a desktop instance. |
| No execution cwd documentation | Supported in inspected runtime reference | The reference explains upload path resolution and stored copies, but does not specify execution cwd. |
| A locally existing default-source path is sufficient | Not established | Project source identity includes a host ID. Directory existence on the server cannot establish source-host identity. |
3. Environment
Trusted origin/main at the commit above; Darwin arm64; Node v22.22.3. Frozen dependency installation completed using Corepack. Full Turbo build passed in the first checkout (56 tasks). The initial bare pnpm launcher was broken; a temporary wrapper routing pnpm through Corepack resolved it. No provider, database, desktop, live server, user runtime data, or network-facing plugin instance was used. Each probe creates and removes its own temporary fixture directory; the unused server URL points to loopback port 1.
4. Minimal reproduction
- Use a clean checkout of the trusted base:
git clone https://github.com/get-bb/bb.git bb-repro cd bb-repro git checkout --detach d89160eb8c69c1e3ebc2ba2514f1711af8d7c506 corepack pnpm install --frozen-lockfile --prefer-offline corepack pnpm exec turbo run build
- Save the probe as
plugins/automations/repro-3616.mts. - Run from the repository root:
corepack pnpm exec tsx plugins/automations/repro-3616.mts
The probe creates a project fixture, changes the caller cwd to that project, stores its own shell script through the production writer, and invokes the production executor and mapper. It supplies a harmless local CLI stub so no installed BB CLI is invoked. It verifies the observed cwd, captured diagnostic, summary, and successful absolute-path control before asserting the desired relative-path behavior.
Expected: the relative fixture is readable and exits zero. Actual, in both runs:
{
"relative": {
"exitCode": 1,
"output": "<scratch>/plugin/scripts\ncat: fixture.txt: No such file or directory\n",
"timedOut": false
},
"summary": "Script exited with code 1",
"absolute": {
"exitCode": 0,
"output": "fixture found\n",
"timedOut": false
}
}
node:internal/modules/run_main:123
triggerUncaughtException(
^
AssertionError [ERR_ASSERTION]: project-relative fixture should be readable
1 !== 0
at <anonymous> (<checkout>/plugins/automations/repro-3616.mts:33:10)
at process.processTicksAndRejections (node:internal/process/task_queues:103:5) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
Node.js v22.22.3
Complete reproduction source
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, writeFile, rm, realpath } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const checkout = process.cwd();
const { executeStoredScript, mapScriptResultToRun } = await import(pathToFileURL(join(checkout, 'plugins/automations/src/script-runner.ts')).href);
const { writeInlineAutomationScript } = await import(pathToFileURL(join(checkout, 'plugins/automations/src/script-files.ts')).href);
const scratch = await realpath(await mkdtemp(join(tmpdir(), 'automation-cwd-probe-')));
const project = join(scratch, 'project');
const pluginDataDir = join(scratch, 'plugin');
try {
await mkdir(project);
const shim = join(scratch, 'bb');
await writeFile(shim, '#!/bin/sh\nexit 0\n', { mode: 0o700 });
process.env.BB_CLI = shim;
process.env.PATH = '/usr/bin:/bin';
await writeFile(join(project, 'fixture.txt'), 'fixture found\n');
process.chdir(project);
const scriptFile = await writeInlineAutomationScript({dataDir: pluginDataDir, automationId: 'probe', content: 'pwd\ncat fixture.txt\n'});
const args = {pluginDataDir, automationId: 'probe', runId: 'probe-run', projectId: 'probe-project', scriptFile, interpreter: 'bash', timeoutMs: 3000, serverUrl: 'http://127.0.0.1:1'};
const relative = await executeStoredScript(args);
const relativeRun = mapScriptResultToRun(relative);
assert.equal(relative.output.split('\n')[0], join(pluginDataDir, 'scripts'));
assert.match(relative.output, /fixture.txt: No such file or directory/);
assert.equal(relativeRun.error, 'Script exited with code 1');
const absoluteFile = await writeInlineAutomationScript({dataDir: pluginDataDir, automationId: 'probe', content: 'cat "$PROBE_PROJECT/fixture.txt"\n'});
const absolute = await executeStoredScript({...args, scriptFile: absoluteFile, env: {PROBE_PROJECT: project}});
assert.equal(absolute.exitCode, 0);
assert.equal(absolute.output, 'fixture found\n');
console.log(JSON.stringify({relative: {...relative, output: relative.output.replaceAll(scratch, '<scratch>')}, summary: relativeRun.error, absolute}, null, 2));
assert.equal(relative.exitCode, 0, 'project-relative fixture should be readable');
} finally {
process.chdir(checkout);
await rm(scratch, {recursive: true, force: true});
}
5. Root cause
executeScriptRun passes projectId, but no source directory or host identity, into the runner. In executeStoredScript, cwd is unconditionally computed from plugin storage and passed to spawn:
const cwd = scriptsRoot(args.pluginDataDir);
await mkdir(cwd, { recursive: true });
The project ID is injected into the environment but does not change cwd. mapScriptResultToRun constructs the failure summary from exitCode alone. The capture result concatenates stdout and stderr, preserving the diagnostic in output but losing its stream identity.
6. Proposed fix and simple-fix assessment
Define which host and source a project-scoped script should use, then resolve that host’s checkout before executing. Make behavior explicit for missing sources, personal projects, disconnected hosts, and paths that happen to exist on multiple hosts. The source contract binds each local path to a host; checking only server-side directory existence can select the wrong checkout. Separately, retain bounded stderr internally and include a useful failure diagnostic in summaries.
No fix branch or PR was created. Choosing source/host and fallback behavior requires a product decision, failing the rule’s simple-fix condition. The runner probe supplies a failing regression candidate but does not validate a full project resolver. No production changes were made. No linked open PR appeared in issue timeline metadata or the issue-number PR search.
7. Verification
The same agent repeated the probe in a second clean temporary Git worktree at the identical trusted commit, with a fresh dependency installation and freshly created fixture storage. Command: corepack pnpm exec tsx plugins/automations/repro-3616.mts. Exit status: 1 from the final intended assertion. All preceding observed-behavior assertions and the absolute-path control passed. Second run output. No correction was needed. This is a runner-level reproduction; project API selection, actual Python output, and the UI were not exercised.
8. Related issues
A small sample of existing automation issues was reviewed for classification patterns. No related issue is used as proof of this behavior.
9. Appendix
First run log · Second run log · Repeatable probe. Temporary checkout and fixture paths are redacted. Both probe runs clean up their temporary storage in finally blocks. No service was started.
Issue content, including its prototype and commands, was treated as untrusted evidence. No linked external URL, branch, patch, or script was fetched or executed.