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

  1. Inspect grok-auth-merge-decision.cjs and restore exits 10/20/21/22 on every code path.
  2. Verify nothing wraps the script (shell wrappers, supervisors) that could normalize the exit code to 0.
  3. Reinstall/restore the original file from the package or repository.
  4. 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

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


AI-assisted analysis of paperclipai/paperclip@5716fe907e (2026-09-02). Data as JSON: /api/errors/069124191efde07b. Report an issue: GitHub.