@sideboard-ai/core 0.1.81 → 0.1.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/agents/cursor-runner.cjs +106 -53
  2. package/dist/agents/cursor-runner.js +96 -55
  3. package/dist/{agents-UQ7D6U6L.js → agents-JFXM66QA.js} +7 -6
  4. package/dist/{agents-EBKJ55PN.js → agents-OPTHS4IX.js} +5 -5
  5. package/dist/{app-settings-6IOQYGBK.js → app-settings-K6RFCRF6.js} +2 -1
  6. package/dist/{app-settings-NILNWNNQ.js → app-settings-PEXK5VEX.js} +1 -1
  7. package/dist/chunk-5KLC2MWZ.js +40 -0
  8. package/dist/{chunk-XKNBSYA7.js → chunk-AY53MPDE.js} +39 -1
  9. package/dist/{chunk-4AVU4QXO.js → chunk-BBQ6HXKS.js} +67 -17
  10. package/dist/{chunk-SEOICVGB.js → chunk-CBJSPTBG.js} +30 -6
  11. package/dist/{chunk-FUUVPN4A.js → chunk-CKBIQ54F.js} +78 -49
  12. package/dist/{chunk-IFPN6TER.js → chunk-I3FRXL7J.js} +4 -1
  13. package/dist/{chunk-CQBQWX73.js → chunk-JPHG2KCC.js} +8 -3
  14. package/dist/{chunk-4HJK57XU.js → chunk-NW7MEJHO.js} +8 -3
  15. package/dist/{chunk-K553XK7M.js → chunk-ODZ2ZOA4.js} +2 -2
  16. package/dist/{chunk-6F2TQFJE.js → chunk-OIEFHOP7.js} +3 -3
  17. package/dist/{chunk-XNECBC6T.js → chunk-R753UFSX.js} +39 -11
  18. package/dist/{chunk-TDJ43HUD.js → chunk-UN3LVQB2.js} +2 -2
  19. package/dist/{chunk-P5VDPGAN.js → chunk-WJ5TINR6.js} +3 -3
  20. package/dist/{chunk-YVVXWWCC.js → chunk-XW47PL6A.js} +81 -49
  21. package/dist/{coordinator-prompt-NYNEFKIV.js → coordinator-prompt-DOIOSLUN.js} +3 -3
  22. package/dist/{coordinator-prompt-JQF72JKX.js → coordinator-prompt-RCTIIZ6C.js} +4 -3
  23. package/dist/{global-workspace-NUMUJRWG.js → global-workspace-33BIG7KK.js} +4 -4
  24. package/dist/{global-workspace-DZM4ZFGU.js → global-workspace-73OAKD3C.js} +5 -4
  25. package/dist/index.cjs +235 -88
  26. package/dist/index.d.cts +36 -9
  27. package/dist/index.d.ts +36 -9
  28. package/dist/index.js +69 -45
  29. package/dist/mcp/run-stdio.cjs +233 -88
  30. package/dist/mcp/run-stdio.js +60 -43
  31. package/dist/{workspaces-QLRCOXAY.js → workspaces-SRITISKA.js} +6 -5
  32. package/dist/{workspaces-SUU2GO3A.js → workspaces-WDLY34MX.js} +5 -5
  33. package/dist/{worktree-5VLLFI5W.js → worktree-SGZVAWOQ.js} +3 -2
  34. package/dist/{worktree-UNSOCYZU.js → worktree-TUAO74SI.js} +2 -2
  35. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -385,6 +385,41 @@ var init_paths = __esm({
385
385
  }
386
386
  });
387
387
 
388
+ // src/hook/nested-electron-env.ts
389
+ function isNestedElectronEnvKey(key) {
390
+ return NESTED_ELECTRON_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
391
+ }
392
+ function stripNestedElectronEnv(env) {
393
+ const out = { ...env };
394
+ for (const key of Object.keys(out)) {
395
+ if (isNestedElectronEnvKey(key)) delete out[key];
396
+ }
397
+ return out;
398
+ }
399
+ function wrapElectronAsNodeLaunch(file, args) {
400
+ if (process.platform === "win32") return { file, args };
401
+ return {
402
+ file: "/bin/sh",
403
+ args: ["-c", STRIP_NESTED_ELECTRON_THEN_EXEC, "sh", file, ...args]
404
+ };
405
+ }
406
+ function isStrippedElectronLaunch(command, args) {
407
+ return command === "/bin/sh" && Boolean(args?.[1]?.includes("ELECTRON_RUN_AS_NODE") && args[1].includes("unset"));
408
+ }
409
+ var NESTED_ELECTRON_ENV_PREFIXES, STRIP_NESTED_ELECTRON_THEN_EXEC;
410
+ var init_nested_electron_env = __esm({
411
+ "src/hook/nested-electron-env.ts"() {
412
+ "use strict";
413
+ NESTED_ELECTRON_ENV_PREFIXES = ["ELECTRON_", "CHROME_"];
414
+ STRIP_NESTED_ELECTRON_THEN_EXEC = [
415
+ "vars=`printenv | awk -F= '/^(ELECTRON_|CHROME_)/{print $1}'`",
416
+ '[ -n "$vars" ] && unset $vars',
417
+ "export ELECTRON_RUN_AS_NODE=1",
418
+ 'exec "$@"'
419
+ ].join("; ");
420
+ }
421
+ });
422
+
388
423
  // src/store/private-file.ts
389
424
  function writePrivateFile(path2, contents) {
390
425
  (0, import_node_fs3.mkdirSync)((0, import_node_path4.dirname)(path2), { recursive: true });
@@ -1499,7 +1534,7 @@ function applyAppEnvironment(target = process.env, settings = loadAppSettings())
1499
1534
  }
1500
1535
  function childEnvWithAppSettings(extra) {
1501
1536
  const settings = loadAppSettings();
1502
- const env = { ...process.env };
1537
+ const env = stripNestedElectronEnv({ ...process.env });
1503
1538
  applyAppEnvironment(env, settings);
1504
1539
  if (extra) {
1505
1540
  for (const [k, v] of Object.entries(extra)) {
@@ -1520,6 +1555,7 @@ var init_app_settings = __esm({
1520
1555
  import_node_os3 = require("os");
1521
1556
  import_node_path6 = require("path");
1522
1557
  init_thinking_effort();
1558
+ init_nested_electron_env();
1523
1559
  init_paths();
1524
1560
  init_private_file();
1525
1561
  init_secret_vault();
@@ -2852,11 +2888,25 @@ function formatGhLandError(raw, opts) {
2852
2888
  }
2853
2889
  return detail || "Failed to create or update pull request";
2854
2890
  }
2891
+ function isPrNotMergeableError(text3) {
2892
+ return /not mergeable|cannot be cleanly created|cannot merge cleanly|Merge conflict|\bCONFLICTING\b|must be (updated|rebased)|branch is out of date|needs? to be (updated|rebased)|Resolve conflicts or update the branch/i.test(
2893
+ text3
2894
+ );
2895
+ }
2896
+ function formatMergePrError(raw) {
2897
+ const trimmed = raw.trim();
2898
+ if (!trimmed) return "gh pr merge failed";
2899
+ if (isPrNotMergeableError(trimmed)) {
2900
+ return "This pull request cannot merge cleanly into the base branch. Resolve conflicts or update the branch, then retry.";
2901
+ }
2902
+ return extractGhErrorDetail(trimmed) || trimmed;
2903
+ }
2855
2904
  function formatIpcInvokeError(err) {
2856
2905
  let msg = err instanceof Error ? err.message : String(err);
2857
2906
  msg = msg.replace(/^Error invoking remote method '[^']+':\s*/i, "");
2858
2907
  msg = msg.replace(/^ExecaError:\s*/i, "");
2859
2908
  msg = msg.replace(/^Error:\s*/i, "");
2909
+ if (isPrNotMergeableError(msg)) return formatMergePrError(msg);
2860
2910
  return formatGhLandError(msg);
2861
2911
  }
2862
2912
  var init_gh_errors = __esm({
@@ -3839,6 +3889,41 @@ ${result.stderr}`;
3839
3889
  ].slice(0, 20) : [];
3840
3890
  return { conflicting, base: baseName, files };
3841
3891
  }
3892
+ async function probeLocalMergeGate(cwd, gate) {
3893
+ const mergeable = (gate?.mergeable ?? "").toUpperCase();
3894
+ const mergeState = (gate?.mergeStateStatus ?? "").toUpperCase();
3895
+ const alreadyConflicting = mergeable === "CONFLICTING" || mergeState === "DIRTY";
3896
+ const inQueue = gate ? prIsInMergeQueue(gate) : false;
3897
+ if (alreadyConflicting || inQueue) return { gate, files: [] };
3898
+ try {
3899
+ const local = await detectLocalMergeConflicts(cwd, gate?.baseRefName ?? null);
3900
+ if (local.conflicting) {
3901
+ return {
3902
+ gate: {
3903
+ mergeable: "CONFLICTING",
3904
+ mergeStateStatus: "DIRTY",
3905
+ reviewDecision: gate?.reviewDecision ?? null,
3906
+ baseRefName: local.base,
3907
+ url: gate?.url ?? null,
3908
+ isInMergeQueue: false
3909
+ },
3910
+ files: local.files
3911
+ };
3912
+ }
3913
+ if (gate && (mergeable === "UNKNOWN" || mergeState === "UNKNOWN")) {
3914
+ return {
3915
+ gate: {
3916
+ ...gate,
3917
+ mergeable: gate.mergeable === "UNKNOWN" ? "MERGEABLE" : gate.mergeable,
3918
+ mergeStateStatus: gate.mergeStateStatus === "UNKNOWN" ? "CLEAN" : gate.mergeStateStatus
3919
+ },
3920
+ files: []
3921
+ };
3922
+ }
3923
+ } catch {
3924
+ }
3925
+ return { gate, files: [] };
3926
+ }
3842
3927
  async function getPrChecks(cwd, selector) {
3843
3928
  const slug = await resolveGithubRepoSlug(cwd);
3844
3929
  const args = [
@@ -3874,47 +3959,9 @@ async function getPrChecks(cwd, selector) {
3874
3959
  }
3875
3960
  }
3876
3961
  let gate = await fetchPrMergeGate(cwd, selector, slug);
3877
- const mergeable = (gate?.mergeable ?? "").toUpperCase();
3878
- const mergeState = (gate?.mergeStateStatus ?? "").toUpperCase();
3879
- const alreadyConflicting = mergeable === "CONFLICTING" || mergeState === "DIRTY";
3880
- const inQueue = gate ? prIsInMergeQueue(gate) : false;
3881
- const needsLocalProbe = !alreadyConflicting && !inQueue;
3882
- if (needsLocalProbe) {
3883
- try {
3884
- const local = await detectLocalMergeConflicts(cwd, gate?.baseRefName ?? null);
3885
- if (local.conflicting) {
3886
- const fileHint = local.files.length > 0 ? ` Conflicting paths: ${local.files.slice(0, 8).join(", ")}${local.files.length > 8 ? "\u2026" : ""}.` : "";
3887
- gate = {
3888
- mergeable: "CONFLICTING",
3889
- mergeStateStatus: "DIRTY",
3890
- reviewDecision: gate?.reviewDecision ?? null,
3891
- baseRefName: local.base,
3892
- url: gate?.url ?? null,
3893
- isInMergeQueue: false
3894
- };
3895
- const ciFailed2 = ciChecks.some((c) => c.bucket === "fail");
3896
- const review2 = (gate.reviewDecision ?? "").toUpperCase();
3897
- const hasReviewRow2 = review2 === "CHANGES_REQUESTED" || review2 === "REVIEW_REQUIRED";
3898
- const gateRows2 = buildMergeGateChecks(gate, {
3899
- suppressGenericBlocked: ciFailed2 || hasReviewRow2
3900
- }).map(
3901
- (row) => row.kind === "mergeability" && row.name === "Merge conflicts" ? {
3902
- ...row,
3903
- description: `${row.description ?? ""}${fileHint}`.trim()
3904
- } : row
3905
- );
3906
- return [...gateRows2, ...ciChecks];
3907
- }
3908
- if (gate && (mergeable === "UNKNOWN" || mergeState === "UNKNOWN")) {
3909
- gate = {
3910
- ...gate,
3911
- mergeable: gate.mergeable === "UNKNOWN" ? "MERGEABLE" : gate.mergeable,
3912
- mergeStateStatus: gate.mergeStateStatus === "UNKNOWN" ? "CLEAN" : gate.mergeStateStatus
3913
- };
3914
- }
3915
- } catch {
3916
- }
3917
- }
3962
+ const probed = await probeLocalMergeGate(cwd, gate);
3963
+ gate = probed.gate;
3964
+ const fileHint = probed.files.length > 0 ? ` Conflicting paths: ${probed.files.slice(0, 8).join(", ")}${probed.files.length > 8 ? "\u2026" : ""}.` : "";
3918
3965
  if (!gate) {
3919
3966
  return ciChecks;
3920
3967
  }
@@ -3924,7 +3971,12 @@ async function getPrChecks(cwd, selector) {
3924
3971
  const gateRows = buildMergeGateChecks(gate, {
3925
3972
  // Avoid duplicating "blocked" when CI failures or review rows already explain it.
3926
3973
  suppressGenericBlocked: ciFailed || hasReviewRow
3927
- });
3974
+ }).map(
3975
+ (row) => fileHint && row.kind === "mergeability" && row.name === "Merge conflicts" ? {
3976
+ ...row,
3977
+ description: `${row.description ?? ""}${fileHint}`.trim()
3978
+ } : row
3979
+ );
3928
3980
  return [...gateRows, ...ciChecks];
3929
3981
  }
3930
3982
  async function getPrMeta(cwd, selector) {
@@ -3934,7 +3986,7 @@ async function getPrMeta(cwd, selector) {
3934
3986
  "view",
3935
3987
  selector,
3936
3988
  "--json",
3937
- "number,title,url,state,isDraft,reviewDecision,baseRefName,headRefName,isInMergeQueue,mergeStateStatus"
3989
+ "number,title,url,state,isDraft,reviewDecision,baseRefName,headRefName,isInMergeQueue,mergeStateStatus,mergeable"
3938
3990
  ];
3939
3991
  if (slug) viewArgs.push("--repo", slug);
3940
3992
  let { stdout, exitCode, stderr } = await gh(viewArgs, cwd, { reject: false });
@@ -3944,7 +3996,7 @@ async function getPrMeta(cwd, selector) {
3944
3996
  "view",
3945
3997
  selector,
3946
3998
  "--json",
3947
- "number,title,url,state,isDraft,reviewDecision,baseRefName,headRefName,mergeStateStatus"
3999
+ "number,title,url,state,isDraft,reviewDecision,baseRefName,headRefName,mergeStateStatus,mergeable"
3948
4000
  ];
3949
4001
  if (slug) retry.push("--repo", slug);
3950
4002
  ({ stdout, exitCode, stderr } = await gh(retry, cwd, { reject: false }));
@@ -3955,16 +4007,28 @@ async function getPrMeta(cwd, selector) {
3955
4007
  }
3956
4008
  try {
3957
4009
  const view = JSON.parse(stdout);
4010
+ const gate = {
4011
+ mergeable: typeof view.mergeable === "string" && view.mergeable ? view.mergeable : null,
4012
+ mergeStateStatus: typeof view.mergeStateStatus === "string" && view.mergeStateStatus ? view.mergeStateStatus : null,
4013
+ reviewDecision: typeof view.reviewDecision === "string" && view.reviewDecision ? view.reviewDecision : null,
4014
+ baseRefName: String(view.baseRefName ?? ""),
4015
+ url: String(view.url ?? ""),
4016
+ isInMergeQueue: parseInMergeQueue(view)
4017
+ };
4018
+ const probed = await probeLocalMergeGate(cwd, gate);
4019
+ const resolved = probed.gate ?? gate;
3958
4020
  return {
3959
4021
  number: Number(view.number),
3960
4022
  title: String(view.title ?? ""),
3961
4023
  url: String(view.url ?? ""),
3962
4024
  state: String(view.state ?? ""),
3963
4025
  isDraft: Boolean(view.isDraft),
3964
- reviewDecision: typeof view.reviewDecision === "string" && view.reviewDecision ? view.reviewDecision : null,
3965
- baseRefName: String(view.baseRefName ?? ""),
4026
+ reviewDecision: resolved.reviewDecision,
4027
+ baseRefName: resolved.baseRefName || String(view.baseRefName ?? ""),
3966
4028
  headRefName: String(view.headRefName ?? ""),
3967
- isInMergeQueue: parseInMergeQueue(view)
4029
+ isInMergeQueue: Boolean(resolved.isInMergeQueue),
4030
+ mergeable: resolved.mergeable,
4031
+ mergeStateStatus: resolved.mergeStateStatus
3968
4032
  };
3969
4033
  } catch {
3970
4034
  return null;
@@ -4432,7 +4496,9 @@ async function mergePr(cwd, selector, opts) {
4432
4496
  if (slug) args.push("--repo", slug);
4433
4497
  const { exitCode, stderr, stdout } = await gh(args, cwd, { reject: false });
4434
4498
  if (exitCode !== 0) {
4435
- throw new Error(stderr.trim() || stdout.trim() || "gh pr merge failed");
4499
+ throw new Error(
4500
+ formatMergePrError(stderr.trim() || stdout.trim() || "gh pr merge failed")
4501
+ );
4436
4502
  }
4437
4503
  const after = await gh(viewArgs, cwd, { reject: false });
4438
4504
  if (after.exitCode === 0 && after.stdout.trim()) {
@@ -4769,6 +4835,7 @@ var init_coordinator_prompt = __esm({
4769
4835
  "- get_pr_stack / open_pr_stack_layers / add_stack_layer / create_pr_stack \u2014 GitHub stacked PRs (`gh stack`); one worktree per layer",
4770
4836
  "- list_models \u2014 only when you need a specific model (rare); otherwise omit model so Account defaults apply",
4771
4837
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
4838
+ "- ask_user \u2014 multiple-choice questions in the composer (any mode, not only Plan). Explain options in chat first, include a description on every option, then wait for answers.",
4772
4839
  "- set_caffeinate \u2014 keep this Mac awake across turns (macOS caffeinate). Turn on for Slack / away-from-keyboard work. Turn OFF when the user says they are done, wrapping up, going to sleep, or no longer need the machine awake. Closing this chat also releases it.",
4773
4840
  "Workspaces:",
4774
4841
  "- add_workspace / remove_workspace \u2014 register or unregister a git repo",
@@ -4788,7 +4855,11 @@ var init_coordinator_prompt = __esm({
4788
4855
  '- request_review \u2014 open a Review chat tab on a worktree thread (attaches .sideboard/review.md when present, else local guidelines; sends "Review changes in this workspace."); then wait_for_turn / get_turn_result on the returned id',
4789
4856
  "- ask_git \u2014 commit & push, open a draft PR, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it queues the worktree agent \u2014 then wait_for_turn. Prefer this over paraphrasing.",
4790
4857
  '- Merge (`ask_git` action=merge / send_to_thread "Merge PR.") only when the user explicitly asked to merge that PR. Do not merge because the work looks done, CI is green, or a typical flow includes it.',
4791
- '- Or send_to_thread with those exact phrases: "Commit and push.", "Commit, push, and open a draft PR.", "Fix merge conflicts.", "Merge PR." (draft PRs: `gh pr create --draft -R <origin-owner/name>` using the workspace `github:` slug \u2014 never upstream). Never run git/gh from this orchestration cwd, and never merge the PR yourself.',
4858
+ '- Or send_to_thread with those exact phrases: "Commit and push.", "Commit, push, and open a draft PR.", "Merge the remote branch (main) into your branch and resolve conflicts. Then, commit and push your changes.", "Merge PR." (draft PRs: `gh pr create --draft -R <origin-owner/name>` using the workspace `github:` slug \u2014 never upstream). Never run git/gh from this orchestration cwd, and never merge the PR yourself.',
4859
+ "Process guides:",
4860
+ "- Recurring multi-item / fan-out: if the child worktree has `.claude/skills/graph-engineering/SKILL.md`, tell the worker to follow it (`/graph-engineering`). Judge first; state on disk; grow the rulebook; do not patch three threads.",
4861
+ "- Recurring shapes: have the worktree agent write `.claude/skills/<kebab-name>/SKILL.md` (commit it) so later threads and native Claude Code / attach see it. Do not use `.sideboard/skills` for new guides. Codex/OpenCode: one line in AGENTS.md pointing at that file.",
4862
+ "- One-offs: no guide. Same miss across threads: edit the skill or `.sideboard/review.md`, then rerun the batch \u2014 do not patch three threads.",
4792
4863
  "Human-only (do not attempt): ready-for-review land (confirm_land), purge_thread.",
4793
4864
  "Thread links in replies: when mentioning a chat/thread for the user, include a markdown link `[Title](sideboard://thread/<id>)` using the full id (or the link field from create_thread / list_threads). Sideboard renders these as clickable opens.",
4794
4865
  "Bash / Read / etc: allowed for (1) inspecting target worktrees / registered repo paths from MCP, and (2) greenfield setup under ~/sideboard/repos (git clone, gh repo create, git init+remote). Never git init/clone *inside* this synthetic home cwd \u2014 emptiness here is expected, not a bug."
@@ -5620,20 +5691,36 @@ function pushTurnStderr(tail, line, maxLines = 12) {
5620
5691
  tail.push(trimmed);
5621
5692
  while (tail.length > maxLines) tail.shift();
5622
5693
  }
5694
+ function looksLikeMinifiedJsDump(line) {
5695
+ if (line.length < 200) return false;
5696
+ return /yield Promise\.all/.test(line) || /\(0,[A-Za-z$]\.\w+\)/.test(line) || /CURSOR_RIPGREP_PATH/.test(line);
5697
+ }
5698
+ function looksLikeNestedElectronCrash(line) {
5699
+ return /HasCustomHostObject|ElectronInitializeICUandStartNode/i.test(line);
5700
+ }
5701
+ function clipStderr(text3, maxChars) {
5702
+ const trimmed = text3.trim();
5703
+ if (trimmed.length <= maxChars) return trimmed;
5704
+ return trimmed.slice(0, maxChars);
5705
+ }
5623
5706
  function summarizeTurnStderr(tail, maxChars = 500) {
5624
5707
  if (tail.length === 0) return "";
5708
+ const cursorStartup = [...tail].reverse().find((line) => /cursor startup failed:/i.test(line));
5709
+ if (cursorStartup) return clipStderr(cursorStartup, maxChars);
5710
+ if (tail.some(looksLikeNestedElectronCrash)) return NESTED_ELECTRON_SUMMARY;
5625
5711
  const moduleMissing = [...tail].reverse().find((line) => /cannot find module/i.test(line));
5626
- if (moduleMissing) {
5627
- return moduleMissing.length <= maxChars ? moduleMissing : moduleMissing.slice(0, maxChars);
5712
+ if (moduleMissing) return clipStderr(moduleMissing, maxChars);
5713
+ const useful = tail.filter((line) => !looksLikeMinifiedJsDump(line));
5714
+ if (useful.length === 0 && tail.some(looksLikeMinifiedJsDump)) {
5715
+ return MINIFIED_DUMP_SUMMARY;
5628
5716
  }
5629
- const joined = tail.slice(-6).join("\n").trim();
5630
- if (joined.length <= maxChars) return joined;
5631
- return joined.slice(joined.length - maxChars);
5717
+ const joined = (useful.length ? useful : tail).slice(-6).join("\n").trim();
5718
+ return clipStderr(joined, maxChars);
5632
5719
  }
5633
5720
  function looksLikeInvalidAgentSession(text3) {
5634
5721
  const lower = text3.trim().toLowerCase();
5635
5722
  if (!lower) return false;
5636
- return /session not found/.test(lower) || /no conversation found/.test(lower) || /conversation .+ not found/.test(lower) || /thread .+ not found/.test(lower) || /unknown session/.test(lower) || /invalid session/.test(lower) || /session .+ (missing|expired|deleted|gone)/.test(lower) || /cannot resume/.test(lower) || /failed to (load|resume|open) session/.test(lower);
5723
+ return /session not found/.test(lower) || /no conversation found/.test(lower) || /conversation .+ not found/.test(lower) || /thread .+ not found/.test(lower) || /unknown session/.test(lower) || /invalid session/.test(lower) || /session .+ (missing|expired|deleted|gone)/.test(lower) || /cannot resume/.test(lower) || /failed to (load|resume|open) session/.test(lower) || /corrupt local agent checkpoint/.test(lower) || /missing root blob/.test(lower) || /\bagent\b.{0,120}\bnot found\b/.test(lower);
5637
5724
  }
5638
5725
  function looksLikeAgentFailureMessage(text3) {
5639
5726
  const lower = text3.trim().toLowerCase();
@@ -5673,6 +5760,12 @@ function humanizeAgentFailDetail(detail) {
5673
5760
  if (/context.*(too long|exceed)|prompt is too long|conversation too long/.test(lower)) {
5674
5761
  return `${raw} \u2014 start a new chat or compact context, then retry.`;
5675
5762
  }
5763
+ if (/hascustomhostobject|electroninitializeicuandstartnode|nested chromium/i.test(lower)) {
5764
+ return `${raw} \u2014 retry the turn; if it keeps failing, pick another agent.`;
5765
+ }
5766
+ if (/corrupt local agent checkpoint|missing root blob|truncated crash dump/.test(lower)) {
5767
+ return `${raw} \u2014 retry the turn (Sideboard will start a fresh Cursor session).`;
5768
+ }
5676
5769
  return raw;
5677
5770
  }
5678
5771
  function formatTurnExitError(exitCode, stderrSummary) {
@@ -5688,11 +5781,13 @@ function formatTurnExitError(exitCode, stderrSummary) {
5688
5781
  if (looksLikeAgentFailureMessage(raw)) return detail;
5689
5782
  return `exit ${code}: ${detail}`;
5690
5783
  }
5691
- var NODE_VERSION_FOOTER;
5784
+ var NODE_VERSION_FOOTER, NESTED_ELECTRON_SUMMARY, MINIFIED_DUMP_SUMMARY;
5692
5785
  var init_error_detail = __esm({
5693
5786
  "src/agents/error-detail.ts"() {
5694
5787
  "use strict";
5695
5788
  NODE_VERSION_FOOTER = /^Node\.js v\d+/i;
5789
+ NESTED_ELECTRON_SUMMARY = "Cursor local agent crashed at Electron startup (nested Chromium / HasCustomHostObject)";
5790
+ MINIFIED_DUMP_SUMMARY = "Cursor local agent crashed during startup (truncated crash dump)";
5696
5791
  }
5697
5792
  });
5698
5793
 
@@ -6136,6 +6231,18 @@ var init_profile = __esm({
6136
6231
  function isAsarPath(filePath) {
6137
6232
  return /\.asar([/\\]|$)/.test(filePath);
6138
6233
  }
6234
+ function applyNodeLaunch(launch, args) {
6235
+ if (!launch.env.ELECTRON_RUN_AS_NODE) {
6236
+ return { file: launch.file, args, env: launch.env };
6237
+ }
6238
+ const wrapped = wrapElectronAsNodeLaunch(launch.file, args);
6239
+ if (process.platform === "win32") {
6240
+ return { file: wrapped.file, args: wrapped.args, env: launch.env };
6241
+ }
6242
+ const env = { ...launch.env };
6243
+ delete env.ELECTRON_RUN_AS_NODE;
6244
+ return { file: wrapped.file, args: wrapped.args, env };
6245
+ }
6139
6246
  async function resolveNodeLaunch(scriptPath) {
6140
6247
  if (isAsarPath(scriptPath)) {
6141
6248
  return {
@@ -6156,6 +6263,7 @@ async function resolveNodeLaunch(scriptPath) {
6156
6263
  var init_node_launch = __esm({
6157
6264
  "src/agents/node-launch.ts"() {
6158
6265
  "use strict";
6266
+ init_nested_electron_env();
6159
6267
  init_run();
6160
6268
  }
6161
6269
  });
@@ -6282,11 +6390,11 @@ async function resolveSideboardMcpServer() {
6282
6390
  if (entry) {
6283
6391
  const isCli = /[/\\]cli[/\\]dist[/\\]index\.js$/.test(entry);
6284
6392
  const scriptArgs = isCli ? [entry, "mcp"] : [entry];
6285
- const launch = await resolveNodeLaunch(entry);
6393
+ const launch = applyNodeLaunch(await resolveNodeLaunch(entry), scriptArgs);
6286
6394
  return {
6287
6395
  name: "sideboard",
6288
6396
  command: launch.file,
6289
- args: scriptArgs,
6397
+ args: launch.args,
6290
6398
  ...Object.keys(launch.env).length > 0 ? { env: launch.env } : {}
6291
6399
  };
6292
6400
  }
@@ -6332,10 +6440,19 @@ async function buildInjectedMcpServers(opts) {
6332
6440
  function toCursorMcpServers(servers) {
6333
6441
  const out = {};
6334
6442
  for (const s of servers) {
6443
+ const env = s.env ? { ...s.env } : void 0;
6444
+ if (env) delete env.ELECTRON_RUN_AS_NODE;
6445
+ let command = s.command;
6446
+ let args = s.args;
6447
+ if (process.platform !== "win32" && !isStrippedElectronLaunch(command, args)) {
6448
+ const wrapped = wrapElectronAsNodeLaunch(command, args ?? []);
6449
+ command = wrapped.file;
6450
+ args = wrapped.args;
6451
+ }
6335
6452
  out[s.name] = {
6336
- command: s.command,
6337
- ...s.args ? { args: s.args } : {},
6338
- ...s.env ? { env: s.env } : {}
6453
+ command,
6454
+ ...args && args.length > 0 ? { args } : {},
6455
+ ...env && Object.keys(env).length > 0 ? { env } : {}
6339
6456
  };
6340
6457
  }
6341
6458
  return out;
@@ -6405,6 +6522,7 @@ var init_injected_mcp = __esm({
6405
6522
  init_app_settings();
6406
6523
  init_paths();
6407
6524
  init_node_launch();
6525
+ init_nested_electron_env();
6408
6526
  import_meta = {};
6409
6527
  SIDEBOARD_MCP_ALLOWED_TOOLS = [
6410
6528
  "mcp__sideboard",
@@ -7498,10 +7616,13 @@ var init_cursor = __esm({
7498
7616
  };
7499
7617
  const runner = cursorRunnerPath();
7500
7618
  const isTs = runner.endsWith(".ts");
7501
- const launch = await resolveNodeLaunch(runner);
7619
+ const launch = applyNodeLaunch(
7620
+ await resolveNodeLaunch(runner),
7621
+ isTs ? ["--import", "tsx", runner] : [runner]
7622
+ );
7502
7623
  return {
7503
7624
  file: launch.file,
7504
- args: isTs ? ["--import", "tsx", runner] : [runner],
7625
+ args: launch.args,
7505
7626
  cwd: thread.worktreePath,
7506
7627
  stdin: JSON.stringify(req),
7507
7628
  env: {
@@ -8575,6 +8696,7 @@ __export(index_exports, {
8575
8696
  REVIEW_REQUEST_NAME: () => REVIEW_REQUEST_NAME,
8576
8697
  REVIEW_REQUEST_PATH: () => REVIEW_REQUEST_PATH,
8577
8698
  REVIEW_REQUEST_PREFILL: () => REVIEW_REQUEST_PREFILL,
8699
+ REVIEW_REQUEST_TEMPLATE: () => REVIEW_REQUEST_TEMPLATE,
8578
8700
  SIDEBOARD_FORCE_STOP: () => SIDEBOARD_FORCE_STOP,
8579
8701
  SIDEBOARD_MCP_ALLOWED_TOOLS: () => SIDEBOARD_MCP_ALLOWED_TOOLS,
8580
8702
  SIDEBOARD_MCP_PROFILE_ENV: () => SIDEBOARD_MCP_PROFILE_ENV,
@@ -8723,9 +8845,11 @@ __export(index_exports, {
8723
8845
  formatGhLandError: () => formatGhLandError,
8724
8846
  formatGitAuthModeDirective: () => formatGitAuthModeDirective,
8725
8847
  formatIpcInvokeError: () => formatIpcInvokeError,
8848
+ formatMergePrError: () => formatMergePrError,
8726
8849
  formatMessagesAsTranscript: () => formatMessagesAsTranscript,
8727
8850
  formatPlanQuestionAnswers: () => formatPlanQuestionAnswers,
8728
8851
  formatPlanQuestionsForChat: () => formatPlanQuestionsForChat,
8852
+ formatProcessGuideDirective: () => formatProcessGuideDirective,
8729
8853
  formatRateLimitResetHint: () => formatRateLimitResetHint,
8730
8854
  formatRenameBranchDirective: () => formatRenameBranchDirective,
8731
8855
  formatSlackExternalReplyPrompt: () => formatSlackExternalReplyPrompt,
@@ -8733,6 +8857,7 @@ __export(index_exports, {
8733
8857
  formatSlackRepliesForTurn: () => formatSlackRepliesForTurn,
8734
8858
  formatSlackSignedReply: () => formatSlackSignedReply,
8735
8859
  formatTranscriptMarkdown: () => formatTranscriptMarkdown,
8860
+ formatUiReminder: () => formatUiReminder,
8736
8861
  formatWorkspaceInventory: () => formatWorkspaceInventory,
8737
8862
  formatWorktreeDirective: () => formatWorktreeDirective,
8738
8863
  formatWorktreeReminder: () => formatWorktreeReminder,
@@ -8807,6 +8932,7 @@ __export(index_exports, {
8807
8932
  isOrchestratorThread: () => isOrchestratorThread,
8808
8933
  isPidAlive: () => isPidAlive,
8809
8934
  isPlaceholderBranch: () => isPlaceholderBranch,
8935
+ isPrNotMergeableError: () => isPrNotMergeableError,
8810
8936
  isPresentPlanToolName: () => isPresentPlanToolName,
8811
8937
  isSessionQuotaLimit: () => isSessionQuotaLimit,
8812
8938
  isSideboardScratchPath: () => isSideboardScratchPath,
@@ -9073,7 +9199,8 @@ function agentGitPrompt(action, opts) {
9073
9199
  return "Commit, push, and open a PR in the browser.";
9074
9200
  case "resolve-conflicts": {
9075
9201
  const base = opts?.prBase?.trim().replace(/^refs\/heads\//, "");
9076
- return base ? `Merge origin/${base} into this branch. Then push.` : "Fix merge conflicts.";
9202
+ const named = base ? ` (${base})` : "";
9203
+ return `Merge the remote branch${named} into your branch and resolve conflicts. Then, commit and push your changes.`;
9077
9204
  }
9078
9205
  case "merge":
9079
9206
  return "Merge PR.";
@@ -10249,7 +10376,7 @@ function formatWorktreeDirective(thread, opts) {
10249
10376
  '- "Fix CI: <name>." \u2192 investigate that failing check, fix it, commit, and push.'
10250
10377
  );
10251
10378
  lines.push(
10252
- '- "Update the branch." / "Fix merge conflicts." / "Merge origin/<base> into this branch. Then push." \u2192 sync with the PR base (merge or rebase), resolve conflicts carefully, commit, and push until the PR is mergeable.'
10379
+ '- "Merge the remote branch (<base>) into your branch and resolve conflicts. Then, commit and push your changes." \u2192 fetch the PR base, merge it into this branch, resolve conflicts carefully, commit, and push until the PR is mergeable.'
10253
10380
  );
10254
10381
  lines.push(
10255
10382
  '- "Address review comments." \u2192 read PR review feedback, make the requested changes, commit, and push.'
@@ -10257,11 +10384,22 @@ function formatWorktreeDirective(thread, opts) {
10257
10384
  lines.push(
10258
10385
  '- "Merge PR." \u2192 merge this thread\'s open pull request on GitHub (this phrase is the explicit ask). If `gh stack view` shows a stack, use `gh stack merge`; otherwise `gh pr merge` (respect repo defaults / squash vs merge). Do not force-push main/master or merge locally into the main checkout.'
10259
10386
  );
10387
+ lines.push("");
10388
+ lines.push(formatProcessGuideDirective());
10260
10389
  return lines.join("\n");
10261
10390
  }
10262
10391
  function formatWorktreeReminder() {
10263
10392
  return "Sideboard worktree: stay in this cwd for all file and git work. Push and open PRs against origin, never upstream. Do not edit the main repo checkout.";
10264
10393
  }
10394
+ function formatProcessGuideDirective() {
10395
+ return [
10396
+ "Process guides (recurring work only):",
10397
+ "- If `.claude/skills/graph-engineering/SKILL.md` exists, follow it (`/graph-engineering`) for migrations, ports, batch fixes, and other fan-out. Judge first; state on disk; grow the rulebook; do not patch around it.",
10398
+ "- If this same shape of work will happen again, write `.claude/skills/<kebab-name>/SKILL.md` in this worktree (Claude Code project skill). Sideboard `/name`, Claude Code, and `attach` all load that path. Do not leave the method only in chat.",
10399
+ "- Do not write new skills under `.sideboard/skills` \u2014 native agents do not scan it. Point Codex/OpenCode at the file from `AGENTS.md`. Optional: symlink `.cursor/skills/<name>` to the Claude skill.",
10400
+ "- Skip a guide for a one-off. If a matching skill exists, follow it. Same miss twice \u2192 edit the skill (or `.sideboard/review.md`), do not patch around it."
10401
+ ].join("\n");
10402
+ }
10265
10403
  function formatArtifactDirective() {
10266
10404
  return [
10267
10405
  "Sideboard side column (desktop UI):",
@@ -10279,9 +10417,22 @@ function formatArtifactDirective() {
10279
10417
  "Files / media browser (CMS file manager column):",
10280
10418
  "4) Call Sideboard MCP `present_files` with optional title, path, and datasource (brightsy|memory).",
10281
10419
  " Opens the Files column for browse/upload/pick. Do NOT say a file manager UI is missing.",
10282
- "Never say artifacts, CMS UI, or the Files column are unavailable. Prefer present_schema for list/edit/publish; present_files for storage UI; html fences for standalone pages."
10420
+ "Multiple-choice questions (any mode \u2014 not only Plan):",
10421
+ "5) Call Sideboard MCP `ask_user` when the user should pick from predefined options (approach forks, requirements, which API). First write a short chat message that explains the decision and what each option means (tradeoffs, when to pick it). Include a description on every option. After calling, stop and wait for their next message with answers. Do not ask multiple-choice questions as plain chat bullets \u2014 use ask_user so Sideboard shows the composer picker.",
10422
+ "Never say artifacts, CMS UI, or the Files column are unavailable. Prefer present_schema for list/edit/publish; present_files for storage UI; html fences for standalone pages; ask_user for predefined-option questions."
10283
10423
  ].join("\n");
10284
10424
  }
10425
+ function formatUiReminder() {
10426
+ return [
10427
+ "Sideboard side column (important):",
10428
+ "There is no claude.ai Artifact tool here \u2014 that is normal.",
10429
+ "HTML/SVG/markdown: ```html fence or MCP present_artifact.",
10430
+ "CMS forms/tables: MCP present_schema (Brightsy resource_id or inline schema+schemaUi).",
10431
+ "Files column: MCP present_files (brightsy account storage or memory).",
10432
+ "Do not say artifacts/CMS UI are unavailable.",
10433
+ "Multiple-choice questions: MCP ask_user (composer picker, any mode). Explain options in chat first, then wait for answers."
10434
+ ].join(" ");
10435
+ }
10285
10436
  var FILES_BY_AGENT = {
10286
10437
  claude: [
10287
10438
  "CLAUDE.md",
@@ -10395,6 +10546,8 @@ var import_node_path18 = require("path");
10395
10546
  var import_execa3 = require("execa");
10396
10547
  var import_node_readline2 = require("readline");
10397
10548
  init_settings();
10549
+ init_nested_electron_env();
10550
+ init_nested_electron_env();
10398
10551
  var PORT_RANGE_SIZE = 10;
10399
10552
  function matchSimpleGlob(pattern, name) {
10400
10553
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
@@ -10478,16 +10631,6 @@ async function captureLoginEnv() {
10478
10631
  cachedLoginEnv = { ...process.env };
10479
10632
  return { ...cachedLoginEnv };
10480
10633
  }
10481
- var NESTED_ELECTRON_ENV_PREFIXES = ["ELECTRON_", "CHROME_"];
10482
- function stripNestedElectronEnv(env) {
10483
- const out = { ...env };
10484
- for (const key of Object.keys(out)) {
10485
- if (NESTED_ELECTRON_ENV_PREFIXES.some((prefix) => key.startsWith(prefix))) {
10486
- delete out[key];
10487
- }
10488
- }
10489
- return out;
10490
- }
10491
10634
  function buildWorkspaceScriptEnv(opts, baseEnv) {
10492
10635
  const env = stripNestedElectronEnv({
10493
10636
  ...baseEnv ?? process.env
@@ -14278,6 +14421,10 @@ File: src/client/frontends/desktop/core/UserData.ts
14278
14421
 
14279
14422
  **Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
14280
14423
  </example>
14424
+
14425
+ ## Growing the rules
14426
+
14427
+ If a blocking issue is a missing or ambiguous repo rule that will recur, say so and propose one sentence for \`.sideboard/review.md\` or a \`.claude/skills/<name>/SKILL.md\`. Do not only patch this diff when the same miss will happen again. New skills go under \`.claude/skills\` (Claude Code / attach) \u2014 not \`.sideboard/skills\`.
14281
14428
  `;
14282
14429
 
14283
14430
  // src/review/request-review.ts
@@ -15078,14 +15225,7 @@ var Orchestrator = class {
15078
15225
  SLACK_REPLY_FORMATTING
15079
15226
  ].join("\n") : null
15080
15227
  ].filter(Boolean).join("\n") : null;
15081
- const artifactReminder = thread.agent !== "brightsy" ? [
15082
- "Sideboard side column (important):",
15083
- "There is no claude.ai Artifact tool here \u2014 that is normal.",
15084
- "HTML/SVG/markdown: ```html fence or MCP present_artifact.",
15085
- "CMS forms/tables: MCP present_schema (Brightsy resource_id or inline schema+schemaUi).",
15086
- "Files column: MCP present_files (brightsy account storage or memory).",
15087
- "Do not say artifacts/CMS UI are unavailable."
15088
- ].join(" ") : null;
15228
+ const artifactReminder = thread.agent !== "brightsy" ? formatUiReminder() : null;
15089
15229
  const worktreeReminder = thread.agent !== "brightsy" && !isOrchestratorThread(thread) ? formatWorktreeReminder() : null;
15090
15230
  const slackReplyContext = formatSlackRepliesForTurn(
15091
15231
  pendingSlackExternalReplies(thread.messages)
@@ -15224,7 +15364,7 @@ var Orchestrator = class {
15224
15364
  }
15225
15365
  let lastStderr = summarizeTurnStderr(stderrTail);
15226
15366
  let detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
15227
- if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && looksLikeInvalidAgentSession(detail) && this.requireThread(threadId).sessionId && this.requireThread(threadId).agent !== "cursor" && this.requireThread(threadId).agent !== "brightsy") {
15367
+ if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && looksLikeInvalidAgentSession(detail) && this.requireThread(threadId).sessionId && this.requireThread(threadId).agent !== "brightsy") {
15228
15368
  updateThread(threadId, { sessionId: null });
15229
15369
  pushTurnStderr(
15230
15370
  stderrTail,
@@ -15779,7 +15919,9 @@ var Orchestrator = class {
15779
15919
  reviewDecision: null,
15780
15920
  baseRefName: "",
15781
15921
  headRefName: "",
15782
- isInMergeQueue: false
15922
+ isInMergeQueue: false,
15923
+ mergeable: null,
15924
+ mergeStateStatus: null
15783
15925
  };
15784
15926
  await this.persistPrMetaAndMaybeArchive(thread, metaLike);
15785
15927
  return { url: metaLike.url, state };
@@ -17141,7 +17283,7 @@ async function startMcpServer() {
17141
17283
  );
17142
17284
  server.tool(
17143
17285
  "ask_user",
17144
- "Ask the user clarifying multiple-choice questions in Sideboard\u2019s composer (plan mode). Before calling, write a short chat message explaining the decision and what each option means. Include a description on every option. Use for approach forks and requirements \u2014 not for \u201Cis the plan ready?\u201D. After calling, stop and wait for their next message with answers.",
17286
+ "Ask the user clarifying multiple-choice questions in Sideboard\u2019s composer (any mode \u2014 not only Plan). Before calling, write a short chat message explaining the decision and what each option means. Include a description on every option. Use when the user should pick from predefined options (approach forks, requirements, which API) \u2014 not for \u201Cis the plan ready?\u201D. After calling, stop and wait for their next message with answers.",
17145
17287
  {
17146
17288
  questions: import_zod3.z.array(
17147
17289
  import_zod3.z.object({
@@ -20018,6 +20160,7 @@ async function startSlackRelayServer(opts) {
20018
20160
  REVIEW_REQUEST_NAME,
20019
20161
  REVIEW_REQUEST_PATH,
20020
20162
  REVIEW_REQUEST_PREFILL,
20163
+ REVIEW_REQUEST_TEMPLATE,
20021
20164
  SIDEBOARD_FORCE_STOP,
20022
20165
  SIDEBOARD_MCP_ALLOWED_TOOLS,
20023
20166
  SIDEBOARD_MCP_PROFILE_ENV,
@@ -20166,9 +20309,11 @@ async function startSlackRelayServer(opts) {
20166
20309
  formatGhLandError,
20167
20310
  formatGitAuthModeDirective,
20168
20311
  formatIpcInvokeError,
20312
+ formatMergePrError,
20169
20313
  formatMessagesAsTranscript,
20170
20314
  formatPlanQuestionAnswers,
20171
20315
  formatPlanQuestionsForChat,
20316
+ formatProcessGuideDirective,
20172
20317
  formatRateLimitResetHint,
20173
20318
  formatRenameBranchDirective,
20174
20319
  formatSlackExternalReplyPrompt,
@@ -20176,6 +20321,7 @@ async function startSlackRelayServer(opts) {
20176
20321
  formatSlackRepliesForTurn,
20177
20322
  formatSlackSignedReply,
20178
20323
  formatTranscriptMarkdown,
20324
+ formatUiReminder,
20179
20325
  formatWorkspaceInventory,
20180
20326
  formatWorktreeDirective,
20181
20327
  formatWorktreeReminder,
@@ -20250,6 +20396,7 @@ async function startSlackRelayServer(opts) {
20250
20396
  isOrchestratorThread,
20251
20397
  isPidAlive,
20252
20398
  isPlaceholderBranch,
20399
+ isPrNotMergeableError,
20253
20400
  isPresentPlanToolName,
20254
20401
  isSessionQuotaLimit,
20255
20402
  isSideboardScratchPath,