@sideboard-ai/core 0.1.10 → 0.1.19

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.
package/dist/index.cjs CHANGED
@@ -1357,6 +1357,69 @@ var init_worktree_labels = __esm({
1357
1357
  }
1358
1358
  });
1359
1359
 
1360
+ // src/git/gh-errors.ts
1361
+ function isGhRateLimitError(text) {
1362
+ return /API rate limit (already )?exceeded/i.test(text) || /rate limit exceeded/i.test(text);
1363
+ }
1364
+ function formatRateLimitResetHint(resetEpochSec, nowMs = Date.now()) {
1365
+ const ms = resetEpochSec * 1e3 - nowMs;
1366
+ if (ms <= 0) return "soon";
1367
+ const mins = Math.max(1, Math.ceil(ms / 6e4));
1368
+ if (mins < 60) {
1369
+ return `in about ${mins} minute${mins === 1 ? "" : "s"}`;
1370
+ }
1371
+ const hours = Math.ceil(mins / 60);
1372
+ return `in about ${hours} hour${hours === 1 ? "" : "s"}`;
1373
+ }
1374
+ function extractGhErrorDetail(text) {
1375
+ const trimmed = text.trim();
1376
+ if (!trimmed) return "";
1377
+ const graphql = trimmed.match(/\bGraphQL:\s*(.+)$/im);
1378
+ if (graphql?.[1]) return `GraphQL: ${graphql[1].trim()}`;
1379
+ const http = trimmed.match(/\bHTTP\s+\d{3}:\s*(.+)$/im);
1380
+ if (http?.[1]) return `HTTP: ${http[1].trim()}`;
1381
+ const lines = trimmed.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
1382
+ if (lines.length > 1 && /^Command failed with exit code/i.test(lines[0])) {
1383
+ return lines.slice(1).join(" ").trim() || lines[0];
1384
+ }
1385
+ return trimmed;
1386
+ }
1387
+ function formatGhLandError(raw, opts) {
1388
+ const trimmed = raw.trim();
1389
+ if (trimmed.startsWith("GitHub API rate limit exceeded.")) {
1390
+ return trimmed;
1391
+ }
1392
+ const detail = extractGhErrorDetail(raw);
1393
+ if (/Head ref must be a branch|No commits between|Head sha can't be blank/i.test(
1394
+ raw
1395
+ ) || /Head ref must be a branch|No commits between|Head sha can't be blank/i.test(
1396
+ detail
1397
+ )) {
1398
+ const target = opts?.targetedRepo ? ` Targeted ${opts.targetedRepo}` : "";
1399
+ const head = opts?.headRef ? ` with head ${opts.headRef}` : "";
1400
+ const hint = opts?.targetedRepo ? ` Branch was pushed to origin \u2014 confirm it exists on GitHub and differs from the base branch.${target}${head}.` : " Often `gh` targeted upstream instead of origin. Branch was pushed \u2014 retry in the latest Sideboard, or run: gh pr create -R <owner/name> --base main --head <branch>.";
1401
+ return `Could not create the pull request.${hint}`;
1402
+ }
1403
+ if (isGhRateLimitError(raw) || isGhRateLimitError(detail)) {
1404
+ const when = opts?.resetAt ? ` Try again ${formatRateLimitResetHint(opts.resetAt, opts.nowMs)}.` : " Wait a few minutes and try again.";
1405
+ const pushNote = opts?.pushed === false ? "" : " Your branch was already pushed.";
1406
+ return `GitHub API rate limit exceeded.${pushNote}${when} Or create the pull request in the browser (Push & open on GitHub).`;
1407
+ }
1408
+ return detail || "Failed to create or update pull request";
1409
+ }
1410
+ function formatIpcInvokeError(err) {
1411
+ let msg = err instanceof Error ? err.message : String(err);
1412
+ msg = msg.replace(/^Error invoking remote method '[^']+':\s*/i, "");
1413
+ msg = msg.replace(/^ExecaError:\s*/i, "");
1414
+ msg = msg.replace(/^Error:\s*/i, "");
1415
+ return formatGhLandError(msg);
1416
+ }
1417
+ var init_gh_errors = __esm({
1418
+ "src/git/gh-errors.ts"() {
1419
+ "use strict";
1420
+ }
1421
+ });
1422
+
1360
1423
  // src/agents/path.ts
1361
1424
  function ensureAgentPath(env = process.env) {
1362
1425
  const home = env.HOME || env.USERPROFILE || (0, import_node_os4.homedir)();
@@ -1547,10 +1610,14 @@ __export(worktree_exports, {
1547
1610
  createThreadWorktree: () => createThreadWorktree,
1548
1611
  currentBranch: () => currentBranch,
1549
1612
  detectLocalMergeConflicts: () => detectLocalMergeConflicts,
1613
+ ensureGhPreferOrigin: () => ensureGhPreferOrigin,
1550
1614
  fetchPrHead: () => fetchPrHead,
1551
1615
  getPr: () => getPr,
1552
1616
  getPrChecks: () => getPrChecks,
1553
1617
  getPrDetails: () => getPrDetails,
1618
+ getPrMeta: () => getPrMeta,
1619
+ ghHeadRef: () => ghHeadRef,
1620
+ ghRepoSelectArgs: () => ghRepoSelectArgs,
1554
1621
  isDirty: () => isDirty,
1555
1622
  isPlaceholderBranch: () => isPlaceholderBranch,
1556
1623
  listBranches: () => listBranches,
@@ -1558,6 +1625,7 @@ __export(worktree_exports, {
1558
1625
  listWorktrees: () => listWorktrees,
1559
1626
  mergePr: () => mergePr,
1560
1627
  normalizeWorktreePath: () => normalizeWorktreePath,
1628
+ originGhRepoEnv: () => originGhRepoEnv,
1561
1629
  parseGithubSlugFromRemoteUrl: () => parseGithubSlugFromRemoteUrl,
1562
1630
  pushBranch: () => pushBranch,
1563
1631
  removeWorktree: () => removeWorktree,
@@ -1574,6 +1642,31 @@ __export(worktree_exports, {
1574
1642
  worktreeDisplayLabelForGroup: () => worktreeDisplayLabelForGroup,
1575
1643
  worktreeNameFromPath: () => worktreeNameFromPath
1576
1644
  });
1645
+ async function lookupGithubGraphqlReset(cwd) {
1646
+ const result = await gh(["api", "rate_limit"], cwd, { reject: false });
1647
+ if (result.exitCode !== 0 || !result.stdout.trim()) return void 0;
1648
+ try {
1649
+ const data = JSON.parse(result.stdout);
1650
+ const reset = data.resources?.graphql?.reset;
1651
+ return typeof reset === "number" ? reset : void 0;
1652
+ } catch {
1653
+ return void 0;
1654
+ }
1655
+ }
1656
+ async function formatPrCreateFailure(raw, cwd, ctx) {
1657
+ if (!isGhRateLimitError(raw)) {
1658
+ return formatGhLandError(raw, {
1659
+ targetedRepo: ctx?.slug,
1660
+ headRef: ctx?.head
1661
+ });
1662
+ }
1663
+ const resetAt = await lookupGithubGraphqlReset(cwd);
1664
+ return formatGhLandError(raw, {
1665
+ resetAt,
1666
+ targetedRepo: ctx?.slug,
1667
+ headRef: ctx?.head
1668
+ });
1669
+ }
1577
1670
  function slugify(input) {
1578
1671
  return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
1579
1672
  }
@@ -1582,10 +1675,13 @@ async function resolveRepoRoot(cwd) {
1582
1675
  return stdout.trim();
1583
1676
  }
1584
1677
  function parseGithubSlugFromRemoteUrl(url) {
1585
- const trimmed = url.trim();
1586
- const match = trimmed.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/i) ?? trimmed.match(/github\.com[:/]([^/]+)\/([^/.]+)/i);
1587
- if (!match?.[1] || !match[2]) return null;
1588
- return `${match[1]}/${match[2]}`;
1678
+ const trimmed = url.trim().replace(/\.git$/i, "");
1679
+ if (!trimmed) return null;
1680
+ const github = trimmed.match(/github\.com[:/]([^/]+)\/([^/]+)$/i);
1681
+ if (github?.[1] && github[2]) return `${github[1]}/${github[2]}`;
1682
+ const sshAlias = trimmed.match(/^git@[^:]+:([^/]+)\/([^/]+)$/i);
1683
+ if (sshAlias?.[1] && sshAlias[2]) return `${sshAlias[1]}/${sshAlias[2]}`;
1684
+ return null;
1589
1685
  }
1590
1686
  async function slugFromGitRemote(repoPath, remote) {
1591
1687
  const result = await git(["remote", "get-url", remote], repoPath, {
@@ -1597,13 +1693,16 @@ async function slugFromGitRemote(repoPath, remote) {
1597
1693
  async function resolveGithubRepoSlug(repoPath) {
1598
1694
  const fromOrigin = await slugFromGitRemote(repoPath, "origin");
1599
1695
  if (fromOrigin) return fromOrigin;
1600
- const viaGh = await gh(
1601
- ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"],
1602
- repoPath,
1603
- { reject: false }
1604
- );
1605
- if (viaGh.exitCode === 0 && viaGh.stdout.trim()) {
1606
- return viaGh.stdout.trim();
1696
+ const hasAlt = Boolean(await slugFromGitRemote(repoPath, "upstream")) || Boolean(await slugFromGitRemote(repoPath, "github"));
1697
+ if (!hasAlt) {
1698
+ const viaGh = await gh(
1699
+ ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"],
1700
+ repoPath,
1701
+ { reject: false }
1702
+ );
1703
+ if (viaGh.exitCode === 0 && viaGh.stdout.trim()) {
1704
+ return viaGh.stdout.trim();
1705
+ }
1607
1706
  }
1608
1707
  for (const remote of ["upstream", "github"]) {
1609
1708
  const slug = await slugFromGitRemote(repoPath, remote);
@@ -1611,15 +1710,35 @@ async function resolveGithubRepoSlug(repoPath) {
1611
1710
  }
1612
1711
  return null;
1613
1712
  }
1713
+ function ghRepoSelectArgs(slug) {
1714
+ return ["-R", slug];
1715
+ }
1716
+ function ghHeadRef(slug, branch) {
1717
+ const owner = slug.split("/")[0];
1718
+ const head = branch.trim().replace(/^refs\/heads\//, "");
1719
+ if (!owner || !head) return head || branch;
1720
+ if (head.includes(":")) return head;
1721
+ return `${owner}:${head}`;
1722
+ }
1723
+ async function ensureGhPreferOrigin(cwd) {
1724
+ const originSlug = await slugFromGitRemote(cwd, "origin");
1725
+ if (!originSlug) return;
1726
+ const hasAlt = Boolean(await slugFromGitRemote(cwd, "upstream")) || Boolean(await slugFromGitRemote(cwd, "github"));
1727
+ if (!hasAlt) return;
1728
+ const view = await gh(["repo", "set-default", "--view"], cwd, {
1729
+ reject: false
1730
+ });
1731
+ const current = `${view.stdout}
1732
+ ${view.stderr}`;
1733
+ if (view.exitCode === 0 && current.includes(originSlug)) return;
1734
+ await gh(["repo", "set-default", "origin"], cwd, { reject: false });
1735
+ }
1736
+ async function originGhRepoEnv(cwd) {
1737
+ await ensureGhPreferOrigin(cwd);
1738
+ const slug = await resolveGithubRepoSlug(cwd);
1739
+ return slug ? { GH_REPO: slug } : {};
1740
+ }
1614
1741
  async function resolveDefaultBranch(repoPath) {
1615
- const viaGh = await gh(
1616
- ["repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"],
1617
- repoPath,
1618
- { reject: false }
1619
- );
1620
- if (viaGh.exitCode === 0 && viaGh.stdout.trim()) {
1621
- return viaGh.stdout.trim();
1622
- }
1623
1742
  const viaOrigin = await git(
1624
1743
  ["symbolic-ref", "refs/remotes/origin/HEAD"],
1625
1744
  repoPath,
@@ -1628,6 +1747,31 @@ async function resolveDefaultBranch(repoPath) {
1628
1747
  if (viaOrigin.exitCode === 0 && viaOrigin.stdout.trim()) {
1629
1748
  return viaOrigin.stdout.trim().replace(/^refs\/remotes\/origin\//, "");
1630
1749
  }
1750
+ const slug = await resolveGithubRepoSlug(repoPath);
1751
+ const viaGh = await gh(
1752
+ [
1753
+ "repo",
1754
+ "view",
1755
+ ...slug ? ["--repo", slug] : [],
1756
+ "--json",
1757
+ "defaultBranchRef",
1758
+ "--jq",
1759
+ ".defaultBranchRef.name"
1760
+ ],
1761
+ repoPath,
1762
+ { reject: false }
1763
+ );
1764
+ if (viaGh.exitCode === 0 && viaGh.stdout.trim()) {
1765
+ return viaGh.stdout.trim();
1766
+ }
1767
+ for (const candidate of ["main", "master"]) {
1768
+ const check = await git(
1769
+ ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${candidate}`],
1770
+ repoPath,
1771
+ { reject: false }
1772
+ );
1773
+ if (check.exitCode === 0) return candidate;
1774
+ }
1631
1775
  for (const candidate of ["main", "master"]) {
1632
1776
  const check = await git(
1633
1777
  ["show-ref", "--verify", "--quiet", `refs/heads/${candidate}`],
@@ -1888,6 +2032,37 @@ async function getPrChecks(cwd, selector) {
1888
2032
  });
1889
2033
  return [...gateRows, ...ciChecks];
1890
2034
  }
2035
+ async function getPrMeta(cwd, selector) {
2036
+ const slug = await resolveGithubRepoSlug(cwd);
2037
+ const viewArgs = [
2038
+ "pr",
2039
+ "view",
2040
+ selector,
2041
+ "--json",
2042
+ "number,title,url,state,isDraft,reviewDecision,baseRefName,headRefName"
2043
+ ];
2044
+ if (slug) viewArgs.push("--repo", slug);
2045
+ const { stdout, exitCode, stderr } = await gh(viewArgs, cwd, { reject: false });
2046
+ if (exitCode !== 0 || !stdout.trim()) {
2047
+ if (/no pull requests found/i.test(stderr)) return null;
2048
+ return null;
2049
+ }
2050
+ try {
2051
+ const view = JSON.parse(stdout);
2052
+ return {
2053
+ number: Number(view.number),
2054
+ title: String(view.title ?? ""),
2055
+ url: String(view.url ?? ""),
2056
+ state: String(view.state ?? ""),
2057
+ isDraft: Boolean(view.isDraft),
2058
+ reviewDecision: typeof view.reviewDecision === "string" && view.reviewDecision ? view.reviewDecision : null,
2059
+ baseRefName: String(view.baseRefName ?? ""),
2060
+ headRefName: String(view.headRefName ?? "")
2061
+ };
2062
+ } catch {
2063
+ return null;
2064
+ }
2065
+ }
1891
2066
  async function getPrDetails(cwd, selector) {
1892
2067
  const slug = await resolveGithubRepoSlug(cwd);
1893
2068
  const viewArgs = [
@@ -1909,7 +2084,6 @@ async function getPrDetails(cwd, selector) {
1909
2084
  "additions",
1910
2085
  "deletions",
1911
2086
  "changedFiles",
1912
- "commits",
1913
2087
  "comments",
1914
2088
  "reviews"
1915
2089
  ].join(",")
@@ -1928,14 +2102,7 @@ async function getPrDetails(cwd, selector) {
1928
2102
  } catch {
1929
2103
  throw new Error(stderr.trim() || "gh pr view returned invalid JSON");
1930
2104
  }
1931
- let checks = [];
1932
- try {
1933
- checks = await getPrChecks(cwd, selector) ?? [];
1934
- } catch {
1935
- checks = [];
1936
- }
1937
2105
  const author = view.author ?? {};
1938
- const commits = Array.isArray(view.commits) ? view.commits : [];
1939
2106
  const comments = Array.isArray(view.comments) ? view.comments : [];
1940
2107
  const reviews = Array.isArray(view.reviews) ? view.reviews : [];
1941
2108
  return {
@@ -1952,19 +2119,8 @@ async function getPrDetails(cwd, selector) {
1952
2119
  additions: Number(view.additions ?? 0),
1953
2120
  deletions: Number(view.deletions ?? 0),
1954
2121
  changedFiles: Number(view.changedFiles ?? 0),
1955
- commits: commits.map((c) => {
1956
- const row = c;
1957
- const authors = Array.isArray(row.authors) ? row.authors.map((a) => {
1958
- const actor = a;
1959
- return { login: actor.login ?? "unknown", name: actor.name ?? null };
1960
- }) : [];
1961
- return {
1962
- oid: String(row.oid ?? ""),
1963
- messageHeadline: String(row.messageHeadline ?? ""),
1964
- committedDate: String(row.committedDate ?? ""),
1965
- authors
1966
- };
1967
- }),
2122
+ // Commits live in Changes; omit from GraphQL to save rate-limit points.
2123
+ commits: [],
1968
2124
  comments: comments.map((c) => {
1969
2125
  const row = c;
1970
2126
  const a = row.author ?? {};
@@ -1984,7 +2140,8 @@ async function getPrDetails(cwd, selector) {
1984
2140
  submittedAt: normalizeGhTime(row.submittedAt)
1985
2141
  };
1986
2142
  }),
1987
- checks
2143
+ // CI lives in Checks tab via getPrChecks — nesting burned GraphQL points.
2144
+ checks: []
1988
2145
  };
1989
2146
  }
1990
2147
  async function fetchPrHead(repoPath, number, localBranch) {
@@ -2020,6 +2177,7 @@ async function createThreadWorktree(opts) {
2020
2177
  if ((0, import_node_fs5.existsSync)(worktreePath)) {
2021
2178
  throw new Error(`Worktree already exists at ${worktreePath}`);
2022
2179
  }
2180
+ await ensureGhPreferOrigin(opts.repoPath);
2023
2181
  await git(["fetch", "origin", "--prune"], opts.repoPath, { reject: false });
2024
2182
  if (!opts.sourceRef.startsWith("origin/") && !opts.sourceRef.startsWith("refs/")) {
2025
2183
  await git(["fetch", "origin", opts.sourceRef], opts.repoPath, {
@@ -2048,6 +2206,7 @@ ${add.stdout}`;
2048
2206
  );
2049
2207
  if (retry.exitCode === 0) {
2050
2208
  branchName = alt;
2209
+ await ensureGhPreferOrigin(worktreePath);
2051
2210
  return { branchName, worktreePath };
2052
2211
  }
2053
2212
  }
@@ -2056,6 +2215,7 @@ ${add.stdout}`;
2056
2215
  `Failed to create worktree: ${add.stderr.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
2057
2216
  );
2058
2217
  }
2218
+ await ensureGhPreferOrigin(worktreePath);
2059
2219
  return { branchName, worktreePath };
2060
2220
  }
2061
2221
  async function removeWorktree(repoPath, worktreePath, opts) {
@@ -2156,8 +2316,18 @@ async function mergePr(cwd, selector, opts) {
2156
2316
  return { url, state: "MERGED" };
2157
2317
  }
2158
2318
  async function createOrUpdatePr(worktreePath, opts) {
2319
+ await ensureGhPreferOrigin(worktreePath);
2320
+ const slug = await resolveGithubRepoSlug(worktreePath);
2321
+ if (!slug) {
2322
+ throw new Error(
2323
+ "Could not resolve the origin GitHub repo (owner/name) for this worktree. Check that `git remote get-url origin` points at github.com."
2324
+ );
2325
+ }
2326
+ const repoArgs = ghRepoSelectArgs(slug);
2327
+ const headRef = ghHeadRef(slug, opts.head);
2328
+ const branchOnly = opts.head.trim().replace(/^refs\/heads\//, "");
2159
2329
  const existing = await gh(
2160
- ["pr", "view", opts.head, "--json", "url", "--jq", ".url"],
2330
+ [...repoArgs, "pr", "view", branchOnly, "--json", "url", "--jq", ".url"],
2161
2331
  worktreePath,
2162
2332
  { reject: false }
2163
2333
  );
@@ -2165,9 +2335,10 @@ async function createOrUpdatePr(worktreePath, opts) {
2165
2335
  const url2 = existing.stdout.trim();
2166
2336
  await gh(
2167
2337
  [
2338
+ ...repoArgs,
2168
2339
  "pr",
2169
2340
  "edit",
2170
- opts.head,
2341
+ branchOnly,
2171
2342
  "--title",
2172
2343
  opts.title,
2173
2344
  "--body",
@@ -2180,6 +2351,7 @@ async function createOrUpdatePr(worktreePath, opts) {
2180
2351
  }
2181
2352
  if (opts.web) {
2182
2353
  const args2 = [
2354
+ ...repoArgs,
2183
2355
  "pr",
2184
2356
  "create",
2185
2357
  "--web",
@@ -2190,18 +2362,19 @@ async function createOrUpdatePr(worktreePath, opts) {
2190
2362
  "--base",
2191
2363
  opts.base,
2192
2364
  "--head",
2193
- opts.head
2365
+ headRef
2194
2366
  ];
2195
2367
  if (opts.draft) args2.push("--draft");
2196
2368
  await gh(args2, worktreePath, { reject: false });
2197
2369
  const again = await gh(
2198
- ["pr", "view", opts.head, "--json", "url", "--jq", ".url"],
2370
+ [...repoArgs, "pr", "view", branchOnly, "--json", "url", "--jq", ".url"],
2199
2371
  worktreePath,
2200
2372
  { reject: false }
2201
2373
  );
2202
2374
  return again.stdout.trim() || "";
2203
2375
  }
2204
2376
  const args = [
2377
+ ...repoArgs,
2205
2378
  "pr",
2206
2379
  "create",
2207
2380
  "--title",
@@ -2211,11 +2384,15 @@ async function createOrUpdatePr(worktreePath, opts) {
2211
2384
  "--base",
2212
2385
  opts.base,
2213
2386
  "--head",
2214
- opts.head
2387
+ headRef
2215
2388
  ];
2216
2389
  if (opts.draft) args.push("--draft");
2217
- const { stdout } = await gh(args, worktreePath);
2218
- const url = stdout.trim().split("\n").find((l) => l.startsWith("http")) ?? stdout.trim();
2390
+ const created = await gh(args, worktreePath, { reject: false });
2391
+ if (created.exitCode !== 0) {
2392
+ const raw = created.stderr.trim() || created.stdout.trim() || "gh pr create failed";
2393
+ throw new Error(await formatPrCreateFailure(raw, worktreePath, { slug, head: headRef }));
2394
+ }
2395
+ const url = created.stdout.trim().split("\n").find((l) => l.startsWith("http")) ?? created.stdout.trim();
2219
2396
  return url;
2220
2397
  }
2221
2398
  function suggestSlug(source) {
@@ -2276,6 +2453,7 @@ var init_worktree = __esm({
2276
2453
  init_thread_store();
2277
2454
  init_teams();
2278
2455
  init_worktree_labels();
2456
+ init_gh_errors();
2279
2457
  init_run();
2280
2458
  init_pr_gates();
2281
2459
  init_teams();
@@ -2316,7 +2494,8 @@ function coordinatorGreenfieldPlaybook(reposDir) {
2316
2494
  "- Examples:",
2317
2495
  ` - Clone: \`git clone <url> ${reposDir}/<name>\``,
2318
2496
  ` - New GitHub repo: \`gh repo create <owner>/<name> --private --clone -- ${reposDir}/<name>\` (or mkdir + git init + gh repo create + remote add + push)`,
2319
- "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn \u2192 send_to_thread (`gh pr create --draft`) or create_draft_pr.",
2497
+ "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn \u2192 send_to_thread (`gh pr create --draft -R <origin-owner/name>`) or create_draft_pr.",
2498
+ "- Always target the child worktree's **origin** (`github:` slug from list_workspaces / `git remote get-url origin` in that worktree). Never open PRs against `upstream`.",
2320
2499
  "- Do coding work in the child worktree thread, not by editing files in this home cwd."
2321
2500
  ].join("\n");
2322
2501
  }
@@ -2381,7 +2560,7 @@ function coordinatorSystemPrompt(opts) {
2381
2560
  COORDINATOR_TOOL_PLAYBOOK,
2382
2561
  coordinatorGreenfieldPlaybook(reposDir),
2383
2562
  "When creating threads, pass the correct repoPath for the target workspace and parentThreadId for children.",
2384
- "Typical flow (existing): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread (implement) \u2192 wait_for_turn \u2192 send_to_thread (ask for `gh pr create --draft`) \u2192 wait_for_turn. Use create_draft_pr only if the child cannot open the PR.",
2563
+ "Typical flow (existing): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread (implement) \u2192 wait_for_turn \u2192 send_to_thread (ask for `gh pr create --draft -R <origin-owner/name>` using the workspace github slug) \u2192 wait_for_turn. Use create_draft_pr only if the child cannot open the PR. Never target upstream.",
2385
2564
  "Typical flow (new app): Bash under repos dir (clone or gh repo create) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn \u2192 draft PR.",
2386
2565
  `Goal: ${opts.goal}`,
2387
2566
  `Parent thread id (pass as parentThreadId when creating children): ${opts.parentId}`,
@@ -2418,8 +2597,8 @@ var init_coordinator_prompt = __esm({
2418
2597
  "Inspect / PRs:",
2419
2598
  "- get_diff \u2014 compact diff summary",
2420
2599
  "- preview_land \u2014 preview push+PR (does not push)",
2421
- "- Prefer asking the worktree agent via send_to_thread to open a draft PR (`gh pr create --draft`) so it owns title/body from the diff.",
2422
- "- create_draft_pr \u2014 fallback: commit (if dirty), push, open/update a DRAFT PR from the orchestrator",
2600
+ "- Prefer asking the worktree agent via send_to_thread to open a draft PR with `gh pr create --draft -R <origin-owner/name>` (use the workspace `github:` slug / that worktree's origin \u2014 never upstream) so it owns title/body from the diff.",
2601
+ "- create_draft_pr \u2014 fallback: commit (if dirty), push to origin, open/update a DRAFT PR from the orchestrator (always against origin)",
2423
2602
  "Human-only (do not attempt): ready-for-review confirm_land, purge_thread.",
2424
2603
  "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.",
2425
2604
  "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."
@@ -2596,6 +2775,9 @@ __export(workspaces_exports, {
2596
2775
  function workspacesFile() {
2597
2776
  return (0, import_node_path8.join)(appDataDir(), "workspaces.json");
2598
2777
  }
2778
+ function removedWorkspacesFile() {
2779
+ return (0, import_node_path8.join)(appDataDir(), "removed-workspaces.json");
2780
+ }
2599
2781
  function readAll() {
2600
2782
  const path = workspacesFile();
2601
2783
  if (!(0, import_node_fs7.existsSync)(path)) return [];
@@ -2610,12 +2792,44 @@ function writeAll(list) {
2610
2792
  (0, import_node_fs7.mkdirSync)(appDataDir(), { recursive: true });
2611
2793
  (0, import_node_fs7.writeFileSync)(workspacesFile(), JSON.stringify(list, null, 2), "utf8");
2612
2794
  }
2795
+ function readRemoved() {
2796
+ const path = removedWorkspacesFile();
2797
+ if (!(0, import_node_fs7.existsSync)(path)) return /* @__PURE__ */ new Set();
2798
+ try {
2799
+ const raw = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
2800
+ return new Set(Array.isArray(raw) ? raw.filter((p) => typeof p === "string") : []);
2801
+ } catch {
2802
+ return /* @__PURE__ */ new Set();
2803
+ }
2804
+ }
2805
+ function writeRemoved(paths) {
2806
+ (0, import_node_fs7.mkdirSync)(appDataDir(), { recursive: true });
2807
+ (0, import_node_fs7.writeFileSync)(removedWorkspacesFile(), JSON.stringify([...paths].sort(), null, 2), "utf8");
2808
+ }
2809
+ function rememberRemoved(repoPath) {
2810
+ const next = readRemoved();
2811
+ next.add(repoPath);
2812
+ writeRemoved(next);
2813
+ }
2814
+ function forgetRemoved(repoPath) {
2815
+ const next = readRemoved();
2816
+ if (!next.delete(repoPath)) return;
2817
+ writeRemoved(next);
2818
+ }
2613
2819
  function listWorkspaces() {
2614
- return readAll().sort((a, b) => a.name.localeCompare(b.name));
2820
+ const all = readAll();
2821
+ const valid = all.filter(
2822
+ (w) => Boolean(w.path) && w.path !== "/" && w.path !== "." && !isGlobalRepoPath(w.path)
2823
+ );
2824
+ if (valid.length !== all.length) writeAll(valid);
2825
+ return valid.sort((a, b) => a.name.localeCompare(b.name));
2615
2826
  }
2616
2827
  async function addWorkspace(repoPath) {
2617
2828
  const root = await resolveRepoRoot(repoPath);
2829
+ if (!root || root === "/") throw new Error(`Invalid repo path: ${repoPath}`);
2618
2830
  if (!(0, import_node_fs7.existsSync)(root)) throw new Error(`Repo not found: ${root}`);
2831
+ forgetRemoved(root);
2832
+ await ensureGhPreferOrigin(root);
2619
2833
  const current = readAll();
2620
2834
  const existing = current.find((w) => w.path === root);
2621
2835
  if (existing) return existing;
@@ -2629,16 +2843,20 @@ async function addWorkspace(repoPath) {
2629
2843
  }
2630
2844
  function removeWorkspace(repoPath) {
2631
2845
  writeAll(readAll().filter((w) => w.path !== repoPath));
2846
+ rememberRemoved(repoPath);
2632
2847
  }
2633
2848
  async function ensureWorkspace(repoPath) {
2634
2849
  return addWorkspace(repoPath);
2635
2850
  }
2636
2851
  function syncWorkspacesFromThreads(repoPaths) {
2637
2852
  const current = readAll();
2853
+ const removed = readRemoved();
2638
2854
  const byPath = new Map(current.map((w) => [w.path, w]));
2639
2855
  let dirty = false;
2640
2856
  for (const path of repoPaths) {
2641
- if (!path || isGlobalRepoPath(path) || byPath.has(path)) continue;
2857
+ if (!path || path === "/" || isGlobalRepoPath(path) || byPath.has(path) || removed.has(path)) {
2858
+ continue;
2859
+ }
2642
2860
  if (!(0, import_node_fs7.existsSync)(path)) continue;
2643
2861
  const ws = {
2644
2862
  path,
@@ -4683,11 +4901,13 @@ __export(index_exports, {
4683
4901
  enrichWorkspacesWithGithub: () => enrichWorkspacesWithGithub,
4684
4902
  ensureAgentPath: () => ensureAgentPath,
4685
4903
  ensureCloudCoordinator: () => ensureCloudCoordinator,
4904
+ ensureGhPreferOrigin: () => ensureGhPreferOrigin,
4686
4905
  ensureGlobalCoordinatorCwd: () => ensureGlobalCoordinatorCwd,
4687
4906
  ensureWorkspace: () => ensureWorkspace,
4688
4907
  estimateMessageChars: () => estimateMessageChars,
4689
4908
  estimateThreadChars: () => estimateThreadChars,
4690
4909
  expandComposerPrompt: () => expandComposerPrompt,
4910
+ extractGhErrorDetail: () => extractGhErrorDetail,
4691
4911
  extractiveSummary: () => extractiveSummary,
4692
4912
  fetchPrHead: () => fetchPrHead,
4693
4913
  finalizeParts: () => finalizeParts,
@@ -4700,7 +4920,10 @@ __export(index_exports, {
4700
4920
  forkThreadWorktree: () => forkThreadWorktree,
4701
4921
  formatAgentInstructions: () => formatAgentInstructions,
4702
4922
  formatBrightsyFetchError: () => formatBrightsyFetchError,
4923
+ formatGhLandError: () => formatGhLandError,
4924
+ formatIpcInvokeError: () => formatIpcInvokeError,
4703
4925
  formatMessagesAsTranscript: () => formatMessagesAsTranscript,
4926
+ formatRateLimitResetHint: () => formatRateLimitResetHint,
4704
4927
  formatRenameBranchDirective: () => formatRenameBranchDirective,
4705
4928
  formatTranscriptMarkdown: () => formatTranscriptMarkdown,
4706
4929
  formatWorkspaceInventory: () => formatWorkspaceInventory,
@@ -4717,10 +4940,13 @@ __export(index_exports, {
4717
4940
  getPr: () => getPr,
4718
4941
  getPrChecks: () => getPrChecks,
4719
4942
  getPrDetails: () => getPrDetails,
4943
+ getPrMeta: () => getPrMeta,
4720
4944
  getRepoSetupInfo: () => getRepoSetupInfo,
4721
4945
  getRunMode: () => getRunMode,
4722
4946
  getRunScript: () => getRunScript,
4723
4947
  gh: () => gh,
4948
+ ghHeadRef: () => ghHeadRef,
4949
+ ghRepoSelectArgs: () => ghRepoSelectArgs,
4724
4950
  git: () => git,
4725
4951
  globalAgentCwd: () => globalAgentCwd,
4726
4952
  harnessEnvKey: () => harnessEnvKey,
@@ -4737,6 +4963,7 @@ __export(index_exports, {
4737
4963
  isBrightsyNdjsonLine: () => isBrightsyNdjsonLine,
4738
4964
  isCloudCoordinatorThread: () => isCloudCoordinatorThread,
4739
4965
  isDirty: () => isDirty,
4966
+ isGhRateLimitError: () => isGhRateLimitError,
4740
4967
  isGlobalRepoPath: () => isGlobalRepoPath,
4741
4968
  isGlobalThread: () => isGlobalThread,
4742
4969
  isLinearConnected: () => isLinearConnected,
@@ -4779,6 +5006,7 @@ __export(index_exports, {
4779
5006
  opencodeAdapter: () => opencodeAdapter,
4780
5007
  orchestrationTitleNeedsSoccerNickname: () => orchestrationTitleNeedsSoccerNickname,
4781
5008
  orchestratorSessionPoisonedByBuiltins: () => orchestratorSessionPoisonedByBuiltins,
5009
+ originGhRepoEnv: () => originGhRepoEnv,
4782
5010
  parseCursorRunnerLine: () => parseCursorRunnerLine,
4783
5011
  parseForceStopMessage: () => parseForceStopMessage,
4784
5012
  parseGithubSlugFromRemoteUrl: () => parseGithubSlugFromRemoteUrl,
@@ -4870,6 +5098,7 @@ init_thread_store();
4870
5098
  init_workspaces();
4871
5099
  init_global_workspace();
4872
5100
  init_run();
5101
+ init_gh_errors();
4873
5102
  init_worktree();
4874
5103
 
4875
5104
  // src/integrations/github.ts
@@ -5053,7 +5282,9 @@ init_agents();
5053
5282
  // src/agents/spawn.ts
5054
5283
  var import_node_readline = require("readline");
5055
5284
  var import_execa2 = require("execa");
5285
+ init_worktree();
5056
5286
  init_app_settings();
5287
+ init_global_workspace();
5057
5288
  init_brightsy();
5058
5289
  init_agents();
5059
5290
 
@@ -5287,9 +5518,14 @@ async function spawnAgentTurn(thread, input, onEvent) {
5287
5518
  `Agent cwd must be the thread worktree (got ${cmd.cwd}, expected ${thread.worktreePath})`
5288
5519
  );
5289
5520
  }
5521
+ const env = childEnvWithAppSettings(cmd.env);
5522
+ if (!isOrchestratorThread(thread)) {
5523
+ const originEnv = await originGhRepoEnv(thread.worktreePath).catch(() => ({}));
5524
+ Object.assign(env, originEnv);
5525
+ }
5290
5526
  const child = (0, import_execa2.execa)(cmd.file, cmd.args, {
5291
5527
  cwd: cmd.cwd,
5292
- env: childEnvWithAppSettings(cmd.env),
5528
+ env,
5293
5529
  reject: false,
5294
5530
  stdout: "pipe",
5295
5531
  stderr: "pipe",
@@ -5396,7 +5632,7 @@ function formatRenameBranchDirective(thread, opts) {
5396
5632
  }
5397
5633
  return lines.join("\n");
5398
5634
  }
5399
- function formatWorktreeDirective(thread) {
5635
+ function formatWorktreeDirective(thread, opts) {
5400
5636
  const worktree = normPath2(thread.worktreePath);
5401
5637
  const repo = normPath2(thread.repoPath);
5402
5638
  const dir = worktreeNameFromPath(thread.worktreePath);
@@ -5417,7 +5653,13 @@ function formatWorktreeDirective(thread) {
5417
5653
  );
5418
5654
  }
5419
5655
  lines.push("");
5420
- lines.push("Pull requests (when the work is ready to share):");
5656
+ lines.push("Git remotes + pull requests (when the work is ready to share):");
5657
+ lines.push(
5658
+ "- Always use this worktree's `origin` remote (`git remote get-url origin` from this cwd). Never push to or open PRs against `upstream` (template remotes)."
5659
+ );
5660
+ lines.push(
5661
+ "- Push with `git push -u origin HEAD` (or the current branch name). Do not `git push upstream`."
5662
+ );
5421
5663
  lines.push(
5422
5664
  "- Derive the PR title and body from what the changes actually do and why \u2014 inspect the diff/commits and the user request. Do not use the soccer-team worktree nickname or placeholder branch as the PR title."
5423
5665
  );
@@ -5431,9 +5673,13 @@ function formatWorktreeDirective(thread) {
5431
5673
  lines.push(
5432
5674
  `- A PR already exists (${thread.prUrl}). Update it (push + edit title/body if the purpose drifted) instead of opening a duplicate.`
5433
5675
  );
5676
+ } else if (opts?.githubSlug) {
5677
+ lines.push(
5678
+ `- Prefer a draft PR first: \`gh pr create --draft -R ${opts.githubSlug}\` (or update via \`gh pr edit -R ${opts.githubSlug}\`) once the change set is coherent. Always pass \`-R ${opts.githubSlug}\` (this worktree's origin). Bare \`gh pr create\` may target upstream instead of origin on dual-remote checkouts. Mark ready for review only when asked. Title/body must reflect the change purpose, not the worktree name.`
5679
+ );
5434
5680
  } else {
5435
5681
  lines.push(
5436
- "- Prefer a draft PR first: `gh pr create --draft` (or update via `gh pr edit`) once the change set is coherent. Mark ready for review only when asked. Title/body must reflect the change purpose, not the worktree name."
5682
+ "- Prefer a draft PR first: `gh pr create --draft -R <origin-owner/name>` (or update via `gh pr edit -R \u2026`) once the change set is coherent. Resolve `<origin-owner/name>` with `git remote get-url origin` in this worktree \u2014 never from `upstream`. Mark ready for review only when asked. Title/body must reflect the change purpose, not the worktree name."
5437
5683
  );
5438
5684
  }
5439
5685
  return lines.join("\n");
@@ -6954,6 +7200,7 @@ async function suggestPrMetadata(worktreePath, opts) {
6954
7200
  }
6955
7201
 
6956
7202
  // src/land/land.ts
7203
+ init_gh_errors();
6957
7204
  async function previewLand(thread) {
6958
7205
  if (thread.sourceIsFork) {
6959
7206
  return {
@@ -7017,15 +7264,24 @@ async function confirmLand(thread, opts) {
7017
7264
  const head = headOut.trim();
7018
7265
  const branch = head && head !== "HEAD" ? head : thread.branchName;
7019
7266
  await pushBranch(thread.worktreePath, branch);
7020
- const prUrl = await createOrUpdatePr(thread.worktreePath, {
7021
- title: meta.title,
7022
- body: meta.body,
7023
- base: preview.target,
7024
- head: branch,
7025
- draft: opts?.draft,
7026
- web: opts?.web
7027
- });
7028
- return { prUrl, pushed: true, committed };
7267
+ try {
7268
+ const prUrl = await createOrUpdatePr(thread.worktreePath, {
7269
+ title: meta.title,
7270
+ body: meta.body,
7271
+ base: preview.target,
7272
+ head: branch,
7273
+ draft: opts?.draft,
7274
+ web: opts?.web
7275
+ });
7276
+ return { prUrl, pushed: true, committed };
7277
+ } catch (err) {
7278
+ const raw = err instanceof Error ? err.message : String(err);
7279
+ if (raw.startsWith("GitHub API rate limit exceeded.")) throw err;
7280
+ if (/Command failed with exit code|API rate limit/i.test(raw)) {
7281
+ throw new Error(formatGhLandError(raw));
7282
+ }
7283
+ throw err;
7284
+ }
7029
7285
  }
7030
7286
 
7031
7287
  // src/threads/create.ts
@@ -7923,7 +8179,7 @@ var Orchestrator = class {
7923
8179
  return thread;
7924
8180
  }
7925
8181
  listWorkspaces() {
7926
- const fromThreads = listThreads({ includeArchived: true }).map((t) => t.repoPath);
8182
+ const fromThreads = listThreads({ includeArchived: false }).map((t) => t.repoPath);
7927
8183
  return syncWorkspacesFromThreads(fromThreads);
7928
8184
  }
7929
8185
  async addWorkspace(repoPath) {
@@ -8069,7 +8325,11 @@ var Orchestrator = class {
8069
8325
  }
8070
8326
  const isBrightsy = fresh.agent === "brightsy";
8071
8327
  const isOrchestration = isOrchestratorThread(fresh);
8072
- const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh);
8328
+ const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
8329
+ githubSlug: await resolveGithubRepoSlug(fresh.worktreePath).catch(
8330
+ () => null
8331
+ )
8332
+ });
8073
8333
  const settings = loadWorkspaceSettings(fresh.worktreePath, fresh.repoPath);
8074
8334
  const { autoRenameBranchEnabled: autoRenameBranchEnabled2 } = await Promise.resolve().then(() => (init_app_settings(), app_settings_exports));
8075
8335
  const renameBranchDirective = !isBrightsy && !isOrchestration && autoRenameBranchEnabled2() ? formatRenameBranchDirective(fresh, {
@@ -8540,8 +8800,8 @@ var Orchestrator = class {
8540
8800
  if (result.prUrl) {
8541
8801
  const patch = { prUrl: result.prUrl };
8542
8802
  try {
8543
- const details = await getPrDetails(thread.worktreePath, result.prUrl);
8544
- if (details?.title) patch.prTitle = details.title;
8803
+ const meta = await getPrMeta(thread.worktreePath, result.prUrl);
8804
+ if (meta?.title) patch.prTitle = meta.title;
8545
8805
  } catch {
8546
8806
  }
8547
8807
  updateThread(thread.id, patch);
@@ -8574,6 +8834,24 @@ var Orchestrator = class {
8574
8834
  if (!selector) return null;
8575
8835
  return getPrChecks(cwd, selector);
8576
8836
  }
8837
+ async getPrMeta(threadRef) {
8838
+ const { thread, selector, cwd } = await this.withPrSelector(threadRef);
8839
+ if (!selector) return null;
8840
+ const meta = await getPrMeta(cwd, selector);
8841
+ if (meta) {
8842
+ const patch = {};
8843
+ if (meta.url && meta.url !== thread.prUrl) patch.prUrl = meta.url;
8844
+ if (meta.title && meta.title !== thread.prTitle) patch.prTitle = meta.title;
8845
+ if (Object.keys(patch).length > 0) {
8846
+ updateThread(thread.id, patch);
8847
+ const latest = this.requireThread(thread.id);
8848
+ if (!latest.userSetTitle && meta.title && latest.title !== meta.title) {
8849
+ updateThread(thread.id, { title: meta.title });
8850
+ }
8851
+ }
8852
+ }
8853
+ return meta;
8854
+ }
8577
8855
  async getPrDetails(threadRef) {
8578
8856
  const { thread, selector, cwd } = await this.withPrSelector(threadRef);
8579
8857
  if (!selector) return null;
@@ -9232,7 +9510,7 @@ async function startMcpServer() {
9232
9510
  );
9233
9511
  server.tool(
9234
9512
  "create_draft_pr",
9235
- "Commit dirty changes if needed, push the thread branch, and create/update a DRAFT GitHub PR. Ready-for-review / non-draft land stays human-only (CLI/app confirm_land). Prefer this when the orchestrator should open a PR itself; alternatively send_to_thread asking the worktree agent to run `gh pr create --draft`.",
9513
+ "Commit dirty changes if needed, push the thread branch to origin, and create/update a DRAFT GitHub PR against that worktree's origin (never upstream). Ready-for-review / non-draft land stays human-only (CLI/app confirm_land). Prefer this when the orchestrator should open a PR itself; alternatively send_to_thread asking the worktree agent to run `gh pr create --draft -R <origin-owner/name>`.",
9236
9514
  { ref: import_zod.z.string() },
9237
9515
  async ({ ref }) => {
9238
9516
  try {
@@ -9758,11 +10036,13 @@ init_injected_mcp();
9758
10036
  enrichWorkspacesWithGithub,
9759
10037
  ensureAgentPath,
9760
10038
  ensureCloudCoordinator,
10039
+ ensureGhPreferOrigin,
9761
10040
  ensureGlobalCoordinatorCwd,
9762
10041
  ensureWorkspace,
9763
10042
  estimateMessageChars,
9764
10043
  estimateThreadChars,
9765
10044
  expandComposerPrompt,
10045
+ extractGhErrorDetail,
9766
10046
  extractiveSummary,
9767
10047
  fetchPrHead,
9768
10048
  finalizeParts,
@@ -9775,7 +10055,10 @@ init_injected_mcp();
9775
10055
  forkThreadWorktree,
9776
10056
  formatAgentInstructions,
9777
10057
  formatBrightsyFetchError,
10058
+ formatGhLandError,
10059
+ formatIpcInvokeError,
9778
10060
  formatMessagesAsTranscript,
10061
+ formatRateLimitResetHint,
9779
10062
  formatRenameBranchDirective,
9780
10063
  formatTranscriptMarkdown,
9781
10064
  formatWorkspaceInventory,
@@ -9792,10 +10075,13 @@ init_injected_mcp();
9792
10075
  getPr,
9793
10076
  getPrChecks,
9794
10077
  getPrDetails,
10078
+ getPrMeta,
9795
10079
  getRepoSetupInfo,
9796
10080
  getRunMode,
9797
10081
  getRunScript,
9798
10082
  gh,
10083
+ ghHeadRef,
10084
+ ghRepoSelectArgs,
9799
10085
  git,
9800
10086
  globalAgentCwd,
9801
10087
  harnessEnvKey,
@@ -9812,6 +10098,7 @@ init_injected_mcp();
9812
10098
  isBrightsyNdjsonLine,
9813
10099
  isCloudCoordinatorThread,
9814
10100
  isDirty,
10101
+ isGhRateLimitError,
9815
10102
  isGlobalRepoPath,
9816
10103
  isGlobalThread,
9817
10104
  isLinearConnected,
@@ -9854,6 +10141,7 @@ init_injected_mcp();
9854
10141
  opencodeAdapter,
9855
10142
  orchestrationTitleNeedsSoccerNickname,
9856
10143
  orchestratorSessionPoisonedByBuiltins,
10144
+ originGhRepoEnv,
9857
10145
  parseCursorRunnerLine,
9858
10146
  parseForceStopMessage,
9859
10147
  parseGithubSlugFromRemoteUrl,