paperclipai/paperclip · error
${options.errorLabel} decision predicate exited 0 (expected
Error message
${options.errorLabel} decision predicate exited 0 (expected 10, 20, 21, or 22) What it means
If execFile resolves, the predicate exited 0 — but the contract requires it to always exit 10, 20, 21, or 22. This throw fails loud on a contract violation: an exit-0 predicate cannot be mapped to any merge decision, so treating it as "keep destination" would silently corrupt credential freshness logic.
Source
Thrown at packages/adapters/grok-local/src/server/grok-auth-merge-decision.ts:74
await execFile("node", [DECISION_SCRIPT_PATH, sourcePath, destinationPath]);
} catch (error) {
const code = (error as { code?: unknown }).code;
if (typeof code === "number" && KNOWN_EXIT_CODES.has(code)) {
return code;
}
const detail =
typeof code === "string"
? `node could not be executed (${code})`
: typeof code === "number"
? `unexpected predicate exit code ${code}`
: error instanceof Error
? error.message
: String(error);
throw new Error(`${options.errorLabel} decision predicate failed: ${detail}`);
}
// `execFile` resolved, so the predicate exited 0. The predicate always
// exits 10, 20, 21, or 22, so a clean exit 0 is unexpected; fail loud.
throw new Error(
`${options.errorLabel} decision predicate exited 0 (expected 10, 20, 21, or 22)`,
);
}
View on GitHub (pinned to 5716fe907e)
Solutions
- Inspect grok-auth-merge-decision.cjs and restore exits 10/20/21/22 on every code path.
- Verify nothing wraps the script (shell wrappers, supervisors) that could normalize the exit code to 0.
- Reinstall/restore the original file from the package or repository.
- Add a local test invoking the predicate on fixture files and asserting the exit code is one of 10/20/21/22.
Example fix
// before (broken predicate) if (fresh) process.exit(10); // falls through and exits 0 // after classify(); process.exit(USE_SOURCE_EXIT); // every path exits 10, 20, 21, or 22
Defensive patterns
Strategy: validation
Validate before calling
// verify the predicate honors its contract on fixtures before relying on it
const { exitCode } = await execFile("node", [DECISION_SCRIPT_PATH, fixtureSrc, fixtureDst]).catch(e => e);
if (![10, 20, 21, 22].includes(Number(exitCode))) {
throw new Error(`predicate contract broken: exited ${exitCode}`);
} Type guard
function isKnownDecisionCode(code: unknown): code is 10 | 20 | 21 | 22 {
return code === 10 || code === 20 || code === 21 || code === 22;
} Try / catch
try {
const decision = await decideGrokAuthMerge(src, dst, { errorLabel: "grok auth copy-out" });
} catch (e) {
if (e instanceof Error && e.message.includes("exited 0")) {
failClosed(); // refuse the merge — do not guess a decision
}
throw e;
} Prevention
- Restore the original .cjs if it was patched or stubbed by a build step.
- Add a unit test asserting every code path exits 10/20/21/22.
- Avoid wrappers/supervisors that can rewrite child exit codes to 0.
- Fail closed on any non-contract exit; never default to a merge decision.
When it happens
Trigger: The predicate script was replaced/patched to `process.exit(0)` or to fall off the end of main without exiting a decision code; a wrapper (shell alias, supervisor) swallows the real exit code and returns 0; an edited .cjs hits a code path that returns normally.
Common situations: Someone hand-edited the decision script for testing and left exit 0 in place; a monorepo build overwrote the .cjs with a stub; running under a tool that rewrites exit codes (some shims/wrappers normalize to 0).
Related errors
- ${options.errorLabel} decision predicate failed: ${detail}
- Workspace database repair command timed out during ${repairP
- Workspace database repair command failed during ${repairPhas
- [paperclip] shapePaperclipWorkspaceEnvForExecution called wi
- PAPERCLIP_BRIDGE_TOKEN is required.
AI-assisted analysis of paperclipai/paperclip@5716fe907e (2026-09-02).
Data as JSON: /api/errors/069124191efde07b.
Report an issue: GitHub.