The solve pipeline runs pi-agent solve, which spawns the pi CLI built from the /tmp/pi npm-workspaces monorepo. That CLI resolves its internal @earendil-works/ packages through the workspace symlinks in /tmp/pi/nodemodules/@earendil-works/. When four of those symlinks (chord, pi-agent-core, pi-telemetry, pi-tui) went missing, Node's ESM resolver threw ERRMODULENOTFOUND before pi could boot, so pi-agent never wrote solution.md and the queue stalled. Re-creating the symlinks restores resolution and the pipeline picks work up again.
@earendil-works/* Workspace Symlinks to Unblock the pi-agent solve PipelineThe solve pipeline runs pi-agent solve, which spawns the pi CLI built from the /tmp/pi npm-workspaces monorepo. That CLI resolves its internal @earendil-works/* packages through the workspace symlinks in /tmp/pi/node_modules/@earendil-works/. When four of those symlinks (chord, pi-agent-core, pi-telemetry, pi-tui) went missing, Node's ESM resolver threw ERR_MODULE_NOT_FOUND before pi could boot, so pi-agent never wrote solution.md and the queue stalled. Re-creating the symlinks restores resolution and the pipeline picks work up again.
/tmp/pi is an npm workspaces monorepo (package.json → workspaces: ["packages/*", "packages/session-backends/*", ...]). npm install materializes every workspace as a relative symlink under node_modules, not a copied package.packages/coding-agent/dist/cli.js (and dist/bundle/cli.js) imports these workspace packages at runtime, e.g. @earendil-works/pi-tui, @earendil-works/pi-agent-core, @earendil-works/chord, @earendil-works/pi-telemetry.ERR_MODULE_NOT_FOUND: Cannot find package '@earendil-works/pi-tui' imported from ...
pi-agent treats a non-zero pi exit as a hard failure and exits without writing the solution artifacts, so the solve pipeline sees no progress. The dist-build step is irrelevant here — the modules are resolved from the workspace links, not from npm tarballs.The missing four links are exactly the internal runtime dependencies pulled in by the coding-agent build. Likely triggers are an interrupted/partial npm install, a workspace-scoped install that pruned links, or manual node_modules cleanup. The links themselves are simple relative links that npm would create, so they can be rebuilt deterministically from package-lock.json.
Run on the host (outside the read-only sandbox; /tmp/pi is bind-mounted read-only inside bwrap):
cd /tmp/pi
ln -sfn ../../packages/chord node_modules/@earendil-works/chord
ln -sfn ../../packages/agent node_modules/@earendil-works/pi-agent-core
ln -sfn ../../packages/telemetry node_modules/@earendil-works/pi-telemetry
ln -sfn ../../packages/tui node_modules/@earendil-works/pi-tui
cd /tmp/pi
node - <<'EOF'
const fs = require('fs');
const path = require('path');
const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
let n = 0;
for (const [p, info] of Object.entries(lock.packages)) {
if (!info || !info.link || !p.startsWith('node_modules/')) continue;
const linkAbs = path.resolve(p);
const targetAbs = path.resolve(info.resolved);
if (!fs.existsSync(targetAbs)) { console.error('missing target', targetAbs); process.exit(1); }
const rel = path.relative(path.dirname(linkAbs), targetAbs);
fs.mkdirSync(path.dirname(linkAbs), { recursive: true });
try { fs.unlinkSync(linkAbs); } catch (e) { if (e.code !== 'ENOENT') throw e; }
fs.symlinkSync(rel, linkAbs);
console.log('linked', p, '->', rel);
n++;
}
console.log(`restored ${n} workspace links`);
EOF
(Canonical alternative: npm install --ignore-scripts at the repo root, which recreates workspace links — but it may hit the network and touch the lockfile. The direct rebuild above is offline and idempotent.)
link: true entries):bash
cd /tmp/pi
node -e '
const fs=require("fs"),path=require("path");
const lock=JSON.parse(fs.readFileSync("package-lock.json","utf8"));
let bad=0;
for(const [p,i] of Object.entries(lock.packages))
if(i&&i.link&&p.startsWith("node_modules/"))
if(!(fs.existsSync(p)&&fs.existsSync(path.resolve(i.resolved)))){console.log("BROKEN",p);bad++;}
console.log("broken workspace links:",bad);'
# => broken workspace links: 0
bash
cd /tmp/pi
node --input-type=module -e '
for (const s of ["@earendil-works/chord","@earendil-works/pi-agent-core","@earendil-works/pi-ai","@earendil-works/pi-tui","@earendil-works/pi-telemetry"])
console.log("OK", s, "->", import.meta.resolve(s).replace("/tmp/pi/",""));'
# => OK ... -> packages/{chord,agent,ai,tui,telemetry}/dist/index.js
/tmp, no repo mutation):bash
rm -rf /tmp/repro && mkdir -p /tmp/repro/node_modules/@earendil-works
cd /tmp/repro
# absent link
node --input-type=module -e "import('@earendil-works/pi-tui').catch(e=>console.log('before:',e.code))"
# => before: ERR_MODULE_NOT_FOUND
ln -s ../../../pi/packages/tui node_modules/@earendil-works/pi-tui
node --input-type=module -e "import('@earendil-works/pi-tui').then(m=>console.log('after: resolved OK, exports',Object.keys(m).length))"
# => after: resolved OK, exports 72
bash
cd /tmp/pi
node packages/coding-agent/dist/bundle/cli.js --version
# => 0.85.1
off-by-one run confirms it — pi-agent solve (PID 2) spawned the pi CLI (PID 9) from /tmp/pi/packages/coding-agent/dist/..., and the isolated session log /tmp/pi-agent-1000/sessions/--home-kara--/*.jsonl grew continuously past the probe's start time instead of exiting on a module-resolution error.Add a pre-flight check to the pipeline launcher so a silently pruned node_modules fails fast with a clear message instead of a stalled solve:
cd /tmp/pi
node --input-type=module -e '
for (const s of ["@earendil-works/chord","@earendil-works/pi-agent-core","@earendil-works/pi-ai","@earendil-works/pi-tui","@earendil-works/pi-telemetry"])
try { import.meta.resolve(s); } catch { console.error("workspace link missing:", s); process.exit(1); }
' || { echo "re-run the Robust fix above"; exit 1; }
Because /tmp/pi is read-only inside the bwrap sandbox, all repairs must be applied on the host before the sandbox is created; the sandbox merely consumes the repaired tree.
# Evidence - Problem class: tick389-solver-recovery-probe - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-18T18:31:54.817Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Probe: after restoring the missing @earendil-works workspace symlinks in /tmp/pi, does the solve pipeline pick work up again?", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "tick389-solver-recovery-probe", "provider": "openrouter", "solved_at": "2026-09-18T18:31:54.817Z", "version": ""}